1use super::wire;
10use crate::llm::actions::{
11 protocol_trait::{ActionResult, Protocol, Server},
12 ActionDefinition, Parameter,
13};
14use crate::protocol::log_template::LogTemplate;
15use crate::protocol::EventType;
16use crate::state::app_state::AppState;
17use anyhow::{anyhow, Context, Result};
18use serde_json::{json, Value};
19use std::sync::LazyLock;
20
21pub struct NsqProtocol;
22
23impl NsqProtocol {
24 pub fn new() -> Self {
25 Self
26 }
27}
28
29impl Default for NsqProtocol {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35pub const ANSWER: &str = "nsq_answer";
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Answer {
41 Ok,
42 Error { code: String, message: String },
43 Deliver(Vec<Delivery>),
44 Close,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Delivery {
50 pub body: String,
51 pub attempts: u16,
52}
53
54impl Answer {
55 pub fn from_result(result: &ActionResult) -> Option<Answer> {
57 match result {
58 ActionResult::CloseConnection => Some(Answer::Close),
59 ActionResult::Custom { name, data } if name == ANSWER => {
60 match data.get("kind")?.as_str()? {
61 "ok" => Some(Answer::Ok),
62 "error" => Some(Answer::Error {
63 code: data.get("code")?.as_str()?.to_string(),
64 message: data
65 .get("message")
66 .and_then(Value::as_str)
67 .unwrap_or("")
68 .to_string(),
69 }),
70 "deliver" => Some(Answer::Deliver(
71 data.get("messages")?
72 .as_array()?
73 .iter()
74 .filter_map(|m| {
75 Some(Delivery {
76 body: m.get("body")?.as_str()?.to_string(),
77 attempts: m
78 .get("attempts")
79 .and_then(Value::as_u64)
80 .and_then(|a| u16::try_from(a).ok())
81 .unwrap_or(1),
82 })
83 })
84 .collect(),
85 )),
86 _ => None,
87 }
88 }
89 _ => None,
90 }
91 }
92}
93
94impl Protocol for NsqProtocol {
95 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
96 vec![
97 crate::llm::actions::ParameterDefinition {
98 name: "first_byte_timeout_secs".to_string(),
99 type_hint: "number".to_string(),
100 description: "Seconds a new connection may send nothing (not even the ' V2' \
101 magic) before the server closes it. Default 300, the window a \
102 `manual` rule gives a human - the peer may be NetGet's own TCP \
103 client parked for its operator."
104 .to_string(),
105 required: false,
106 example: json!(300),
107 default: Some(json!(super::FIRST_BYTE_TIMEOUT.as_secs())),
108 },
109 crate::llm::actions::ParameterDefinition {
110 name: "heartbeat_interval_secs".to_string(),
111 type_hint: "number".to_string(),
112 description: "Seconds between the _heartbeat_ frames sent to a client that did \
113 not ask for its own interval in IDENTIFY. A client silent for two \
114 intervals is closed, as nsqd does. Default 30 (nsqd's)."
115 .to_string(),
116 required: false,
117 example: json!(30),
118 default: Some(json!(super::HEARTBEAT_INTERVAL.as_secs())),
119 },
120 crate::llm::actions::ParameterDefinition {
121 name: "idle_timeout_secs".to_string(),
122 type_hint: "number".to_string(),
123 description: "Seconds of silence after which a client that disabled heartbeats \
124 (heartbeat_interval -1 in IDENTIFY) is closed. Default 300. A \
125 client waiting for the model's answer is not idle."
126 .to_string(),
127 required: false,
128 example: json!(300),
129 default: Some(json!(super::IDLE_TIMEOUT.as_secs())),
130 },
131 ]
132 }
133 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
134 Vec::new()
135 }
136 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
137 vec![
138 ok_action(),
139 error_action(),
140 deliver_action(),
141 close_connection_action(),
142 ]
143 }
144 fn protocol_name(&self) -> &'static str {
145 "NSQ"
146 }
147 fn get_event_types(&self) -> Vec<EventType> {
148 vec![
149 NSQ_PUBLISH_EVENT.clone(),
150 NSQ_SUBSCRIBE_EVENT.clone(),
151 NSQ_READY_EVENT.clone(),
152 NSQ_FINISH_EVENT.clone(),
153 NSQ_REQUEUE_EVENT.clone(),
154 ]
155 }
156 fn stack_name(&self) -> &'static str {
157 "ETH>IP>TCP>NSQ"
158 }
159 fn keywords(&self) -> Vec<&'static str> {
160 vec!["nsq", "nsqd", "message queue", "nsq_tail", "to_nsq"]
161 }
162 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
163 use crate::protocol::metadata::{
164 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
165 };
166
167 ProtocolMetadataV2::builder()
168 .state(DevelopmentState::Beta)
173 .well_known_port(4150)
174 .privilege_requirement(PrivilegeRequirement::None)
176 .implementation(
177 "Hand-written nsqd TCP protocol (V2) over tokio: the ' V2' magic, \
178 newline-terminated commands with size-prefixed bodies, and size/type framed \
179 responses, errors and messages, all rendered by NetGet; message ids and \
180 timestamps generated by NetGet",
181 )
182 .llm_control(
183 "Whether each PUB/MPUB/DPUB and SUB is accepted (OK) or refused (an nsqd error \
184 code), and which messages a subscriber receives when it signals RDY, finishes \
185 or requeues one",
186 )
187 .e2e_testing(
188 "tests/server/nsq/real_client_test.rs drives the NSQ project's own to_nsq \
189 (publishes stdin lines) and nsq_tail (subscribes and prints what it receives) - \
190 go-nsq, Go; Homebrew `nsq`, the release tarball on Ubuntu - asserting that \
191 to_nsq's lines reach the handler as nsq_publish and that nsq_tail prints exactly \
192 the bodies delivered. It fails, never skips, when the binaries are absent. \
193 tests/server/nsq/e2e_test.rs covers the mocked-model path on a raw socket.",
194 )
195 .notes(
196 "Implements the V2 magic; IDENTIFY (plain OK, or the feature-negotiation JSON \
197 with TLS, deflate, snappy and auth off); SUB; RDY; FIN; REQ; TOUCH; PUB, MPUB \
198 and DPUB; NOP; CLS (CLOSE_WAIT); heartbeats (_heartbeat_ on the negotiated \
199 interval, and a client silent for two intervals is closed). AUTH answers \
200 E_AUTH_DISABLED. NetGet is not a queue: nothing published is stored or routed \
201 to other connections - the model decides what each subscriber receives. \
202 In-flight messages never time out and are not redelivered unless the model \
203 redelivers a requeued one. Bodies are capped at nsqd's defaults (1 MiB per \
204 message, 5 MiB per MPUB/IDENTIFY body), judged from the declared size before \
205 allocation. On backend failure a PUB gets E_PUB_FAILED and a SUB E_INVALID, \
206 both with a fixed text.",
207 )
208 .max_inbound_bytes(wire::MAX_BODY_SIZE)
209 .answers_on_failure()
212 .build()
213 }
214 fn description(&self) -> &'static str {
215 "NSQ message broker (nsqd TCP protocol) - the model decides what is accepted and delivered"
216 }
217 fn example_prompt(&self) -> &'static str {
218 "NSQ broker on port 4150 - accept every publish, and give subscribers of topic 'orders' \
219 the message 'order 1 shipped'"
220 }
221 fn group_name(&self) -> &'static str {
222 "Application"
223 }
224 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
225 use crate::llm::actions::StartupExamples;
226 StartupExamples::new(
227 json!({
228 "type": "open_server",
229 "port": 4150,
230 "base_stack": "nsq",
231 "instruction": "NSQ broker. Accept every publish and every subscription. The \
232 topic 'orders' holds two messages: 'order 1 shipped' and \
233 'order 2 packed'. Deliver them once each to a subscriber."
234 }),
235 json!({
236 "type": "open_server",
237 "port": 4150,
238 "base_stack": "nsq",
239 "event_handlers": [{
240 "event_pattern": "*",
241 "handler": {
242 "type": "script",
243 "language": "python",
244 "code": "import json, sys\ni = json.load(sys.stdin)\nt = i['event_type_id']\ne = i['event']\nif t in ('nsq_publish', 'nsq_subscribe'):\n a = [{'type': 'send_nsq_ok'}]\nelif t == 'nsq_ready':\n a = [{'type': 'deliver_nsq_messages', 'messages': [{'body': 'hello from ' + e.get('topic', '')}]}]\nelse:\n a = []\nprint(json.dumps({'actions': a}))"
245 }
246 }]
247 }),
248 json!({
249 "type": "open_server",
250 "port": 4150,
251 "base_stack": "nsq",
252 "event_handlers": [
253 {
254 "event_pattern": "nsq_publish",
255 "handler": {"type": "static", "actions": [{"type": "send_nsq_ok"}]}
256 },
257 {
258 "event_pattern": "nsq_subscribe",
259 "handler": {"type": "static", "actions": [{"type": "send_nsq_ok"}]}
260 },
261 {
262 "event_pattern": "nsq_ready",
263 "handler": {"type": "static", "actions": [
264 {"type": "deliver_nsq_messages", "messages": [{"body": "hello"}]}
265 ]}
266 }
267 ]
268 }),
269 )
270 }
271}
272
273impl Server for NsqProtocol {
274 fn spawn(
275 &self,
276 ctx: crate::protocol::SpawnContext,
277 ) -> std::pin::Pin<
278 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
279 > {
280 Box::pin(async move {
281 let secs = |name: &str| -> anyhow::Result<Option<u64>> {
282 Ok(ctx
283 .startup_params
284 .as_ref()
285 .map(|p| p.get_optional_u64(name))
286 .transpose()?
287 .flatten())
288 };
289 let config = super::NsqConfig {
290 first_byte_timeout: secs("first_byte_timeout_secs")?
291 .map(std::time::Duration::from_secs)
292 .unwrap_or(super::FIRST_BYTE_TIMEOUT),
293 heartbeat_interval: secs("heartbeat_interval_secs")?
294 .map(|s| std::time::Duration::from_secs(s.max(1)))
295 .unwrap_or(super::HEARTBEAT_INTERVAL),
296 idle_timeout: secs("idle_timeout_secs")?
297 .map(std::time::Duration::from_secs)
298 .unwrap_or(super::IDLE_TIMEOUT),
299 };
300
301 crate::server::nsq::NsqServer::spawn_with_llm_actions(
302 ctx.legacy_listen_addr(),
303 ctx.llm_client,
304 ctx.state,
305 ctx.status_tx,
306 ctx.server_id,
307 config,
308 )
309 .await
310 })
311 }
312
313 fn execute_action(&self, action: Value) -> Result<ActionResult> {
314 let action_type = action
315 .get("type")
316 .and_then(|v| v.as_str())
317 .context("Missing 'type' field in action")?;
318
319 let data = match action_type {
320 "send_nsq_ok" => json!({"kind": "ok"}),
321 "send_nsq_error" => {
322 let code = action
323 .get("code")
324 .and_then(Value::as_str)
325 .context("send_nsq_error needs 'code'")?
326 .trim()
327 .to_ascii_uppercase();
328 if !wire::ERROR_CODES.contains(&code.as_str()) {
329 return Err(anyhow!(
330 "send_nsq_error: unknown code {code:?}; use one of {}",
331 wire::ERROR_CODES.join(", ")
332 ));
333 }
334 let message = action.get("message").and_then(Value::as_str).unwrap_or("");
335 let message = crate::utils::truncate_for_log(
336 &crate::utils::sanitize::line_field(message),
337 256,
338 );
339 json!({"kind": "error", "code": code, "message": message})
340 }
341 "deliver_nsq_messages" => {
342 let messages = action
343 .get("messages")
344 .and_then(Value::as_array)
345 .context("deliver_nsq_messages needs 'messages', a list of {body}")?;
346 let mut out = Vec::with_capacity(messages.len());
347 for (i, m) in messages.iter().enumerate() {
348 let body = match m {
349 Value::String(s) => s.clone(),
350 Value::Object(o) => match o.get("body") {
351 Some(Value::String(s)) => s.clone(),
352 Some(Value::Null) | None => {
353 return Err(anyhow!("message {i} has no 'body'"))
354 }
355 Some(other) => other.to_string(),
356 },
357 other => other.to_string(),
358 };
359 if body.len() > wire::MAX_MSG_SIZE {
360 return Err(anyhow!(
361 "message {i} is {} bytes, over the {} byte limit",
362 body.len(),
363 wire::MAX_MSG_SIZE
364 ));
365 }
366 let attempts = match m.get("attempts") {
367 None | Some(Value::Null) => 1,
368 Some(v) => v
369 .as_u64()
370 .or_else(|| v.as_str().and_then(|s| s.trim().parse().ok()))
371 .filter(|a| (1..=u16::MAX as u64).contains(a))
372 .with_context(|| {
373 format!("message {i}: 'attempts' must be 1 to 65535")
374 })?,
375 };
376 out.push(json!({"body": body, "attempts": attempts}));
377 }
378 json!({"kind": "deliver", "messages": out})
379 }
380 "close_connection" => return Ok(ActionResult::CloseConnection),
381 _ => return Err(anyhow!("Unknown NSQ action: {}", action_type)),
382 };
383 Ok(ActionResult::Custom {
384 name: ANSWER.to_string(),
385 data,
386 })
387 }
388}
389
390fn ok_action() -> ActionDefinition {
391 ActionDefinition {
392 name: "send_nsq_ok".to_string(),
393 description: "Accept a publish (PUB, MPUB, DPUB) or a subscription (SUB): the client \
394 receives OK."
395 .to_string(),
396 parameters: vec![],
397 example: json!({"type": "send_nsq_ok"}),
398 log_template: Some(
399 LogTemplate::new()
400 .with_info("-> NSQ OK")
401 .with_debug("NSQ send_nsq_ok"),
402 ),
403 }
404}
405
406fn error_action() -> ActionDefinition {
407 ActionDefinition {
408 name: "send_nsq_error".to_string(),
409 description: "Refuse the request with one of nsqd's error codes. E_PUB_FAILED, \
410 E_MPUB_FAILED or E_DPUB_FAILED refuse a publish; E_BAD_TOPIC, \
411 E_BAD_CHANNEL, E_UNAUTHORIZED or E_TOO_MANY_CHANNEL_CONSUMERS refuse a \
412 subscription. Every code except E_FIN_FAILED, E_REQ_FAILED and \
413 E_TOUCH_FAILED closes the connection, as nsqd does."
414 .to_string(),
415 parameters: vec![
416 Parameter {
417 name: "code".to_string(),
418 type_hint: "string".to_string(),
419 description: "The nsqd error code".to_string(),
420 required: true,
421 }
422 .with_choices(wire::ERROR_CODES.iter().copied()),
423 Parameter {
424 name: "message".to_string(),
425 type_hint: "string".to_string(),
426 description: "One line saying why, after the code".to_string(),
427 required: false,
428 },
429 ],
430 example: json!({"type": "send_nsq_error", "code": "E_PUB_FAILED", "message": "<why>"}),
431 log_template: Some(
432 LogTemplate::new()
433 .with_info("-> NSQ {code}")
434 .with_debug("NSQ send_nsq_error: code={code}"),
435 ),
436 }
437}
438
439fn deliver_action() -> ActionDefinition {
440 ActionDefinition {
441 name: "deliver_nsq_messages".to_string(),
442 description: "Deliver messages to this subscriber. NetGet gives each one its message id \
443 and timestamp, and sends no more than the client's RDY count allows; the \
444 rest wait in order until the client finishes one. Only a subscribed \
445 connection can receive messages."
446 .to_string(),
447 parameters: vec![Parameter {
448 name: "messages".to_string(),
449 type_hint: "array".to_string(),
450 description: "List of {body, attempts}: body is the message text, attempts \
451 (optional, default 1) how many times it has been delivered"
452 .to_string(),
453 required: true,
454 }],
455 example: json!({
456 "type": "deliver_nsq_messages",
457 "messages": [{"body": "<message body>"}]
458 }),
459 log_template: Some(
460 LogTemplate::new()
461 .with_info("-> NSQ deliver messages")
462 .with_debug("NSQ deliver_nsq_messages"),
463 ),
464 }
465}
466
467fn close_connection_action() -> ActionDefinition {
468 ActionDefinition {
469 name: "close_connection".to_string(),
470 description: "Close the NSQ connection after any reply".to_string(),
471 parameters: vec![],
472 example: json!({"type": "close_connection"}),
473 log_template: Some(
474 LogTemplate::new()
475 .with_info("NSQ connection closed")
476 .with_debug("NSQ close_connection"),
477 ),
478 }
479}
480
481fn param(name: &str, type_hint: &str, description: &str) -> Parameter {
482 Parameter {
483 name: name.to_string(),
484 type_hint: type_hint.to_string(),
485 description: description.to_string(),
486 required: true,
487 }
488}
489
490fn answer_with_param() -> Parameter {
491 param(
492 "answer_with",
493 "string",
494 "Which answer this request takes, and where its content comes from",
495 )
496}
497
498pub fn publish_answer_with(command: &str, topic: &str, count: usize) -> String {
500 let refusal = match command {
501 "MPUB" => "E_MPUB_FAILED",
502 "DPUB" => "E_DPUB_FAILED",
503 _ => "E_PUB_FAILED",
504 };
505 format!(
506 "exactly one action: send_nsq_ok to accept the {count} message(s) published to topic \
507 '{topic}', or send_nsq_error with code {refusal} only if your instructions say this \
508 topic refuses publishes"
509 )
510}
511
512pub fn subscribe_answer_with(topic: &str, channel: &str) -> String {
514 format!(
515 "exactly one action: send_nsq_ok to accept the subscription to topic '{topic}' on \
516 channel '{channel}', or send_nsq_error (E_BAD_TOPIC, E_BAD_CHANNEL or E_UNAUTHORIZED) \
517 only if your instructions say it is refused. No messages yet: the client has not sent \
518 RDY"
519 )
520}
521
522pub fn deliver_answer_with(topic: &str, can_deliver: u64, pending: usize) -> String {
524 let queued = if pending > 0 {
525 format!(" {pending} message(s) you gave earlier are already queued and will follow;")
526 } else {
527 String::new()
528 };
529 format!(
530 "deliver_nsq_messages with at most {can_deliver} message(s) your instructions say are \
531 waiting on topic '{topic}' that you have not delivered before, each body word for \
532 word as your instructions give it;{queued} no action at all when none are waiting"
533 )
534}
535
536pub fn requeue_answer_with(attempts: u16) -> String {
538 format!(
539 "deliver_nsq_messages with this message's body again and attempts {} if your \
540 instructions say a requeued message is retried; no action at all to drop it",
541 attempts.saturating_add(1)
542 )
543}
544
545fn deliver_actions() -> Vec<ActionDefinition> {
546 vec![deliver_action(), error_action(), close_connection_action()]
547}
548
549pub static NSQ_PUBLISH_EVENT: LazyLock<EventType> = LazyLock::new(|| {
551 EventType::new(
552 "nsq_publish",
553 "A client published messages to a topic. Accept them with send_nsq_ok or refuse them \
554 with send_nsq_error; exactly one of the two. answer_with says which fits.",
555 json!({"type": "send_nsq_ok"}),
556 )
557 .with_parameters(vec![
558 param("command", "string", "PUB, MPUB or DPUB").with_choices(["PUB", "MPUB", "DPUB"]),
559 param("topic", "string", "The topic published to"),
560 param(
561 "messages",
562 "array",
563 "The message bodies as text (invalid UTF-8 replaced), the first 20, each cut at \
564 1000 characters",
565 ),
566 param(
567 "message_count",
568 "number",
569 "How many messages were published",
570 ),
571 param("total_bytes", "number", "The bodies' total size in bytes"),
572 answer_with_param(),
573 ])
574 .with_log_template(
575 LogTemplate::new()
576 .with_info("NSQ {command} {topic} ({message_count} message(s))")
577 .with_debug("NSQ nsq_publish: topic={topic} bytes={total_bytes}"),
578 )
579 .with_actions(vec![
580 ok_action(),
581 error_action(),
582 deliver_action(),
583 close_connection_action(),
584 ])
585 .with_alternative_example(
586 json!({"type": "send_nsq_error", "code": "E_PUB_FAILED", "message": "<why>"}),
587 )
588});
589
590pub static NSQ_SUBSCRIBE_EVENT: LazyLock<EventType> =
592 LazyLock::new(|| {
593 EventType::new(
594 "nsq_subscribe",
595 "A client subscribed to a topic's channel. Accept with send_nsq_ok or refuse with \
596 send_nsq_error; exactly one of the two. Messages cannot be delivered yet.",
597 json!({"type": "send_nsq_ok"}),
598 )
599 .with_parameters(vec![
600 param("topic", "string", "The topic the client subscribes to"),
601 param(
602 "channel",
603 "string",
604 "The channel on that topic; each channel receives its own copy of the topic's messages",
605 ),
606 param("ephemeral", "boolean", "Whether the channel name ends in #ephemeral"),
607 param("client_id", "string", "The client_id from IDENTIFY (may be empty)"),
608 param("user_agent", "string", "The user_agent from IDENTIFY (may be empty)"),
609 answer_with_param(),
610 ])
611 .with_log_template(
612 LogTemplate::new()
613 .with_info("NSQ SUB {topic} {channel}")
614 .with_debug("NSQ nsq_subscribe: topic={topic} channel={channel}"),
615 )
616 .with_actions(vec![ok_action(), error_action(), close_connection_action()])
617 .with_alternative_example(
618 json!({"type": "send_nsq_error", "code": "E_UNAUTHORIZED", "message": "<why>"}),
619 )
620 });
621
622fn flow_parameters() -> Vec<Parameter> {
623 vec![
624 param("topic", "string", "The subscribed topic"),
625 param("channel", "string", "The subscribed channel"),
626 param(
627 "ready",
628 "number",
629 "The client's RDY count: how many messages it takes at once",
630 ),
631 param(
632 "in_flight",
633 "number",
634 "Messages delivered and not yet finished or requeued",
635 ),
636 param(
637 "pending",
638 "number",
639 "Messages you already gave that wait for RDY capacity; NetGet sends them first",
640 ),
641 param(
642 "can_deliver",
643 "number",
644 "How many more messages the client can receive right now",
645 ),
646 ]
647}
648
649pub static NSQ_READY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
651 let mut parameters = vec![param(
652 "count",
653 "number",
654 "The RDY count the client just sent",
655 )];
656 parameters.extend(flow_parameters());
657 parameters.push(answer_with_param());
658 EventType::new(
659 "nsq_ready",
660 "A subscriber is ready for messages (RDY). Deliver the messages your instructions say \
661 are waiting with deliver_nsq_messages, or answer with no action when there are none.",
662 json!({"type": "deliver_nsq_messages", "messages": [{"body": "<message body>"}]}),
663 )
664 .with_parameters(parameters)
665 .with_log_template(
666 LogTemplate::new()
667 .with_info("NSQ RDY {count} on {topic}/{channel}")
668 .with_debug("NSQ nsq_ready: in_flight={in_flight} can_deliver={can_deliver}"),
669 )
670 .with_actions(deliver_actions())
671});
672
673pub static NSQ_FINISH_EVENT: LazyLock<EventType> = LazyLock::new(|| {
675 let mut parameters = vec![param(
676 "message_id",
677 "string",
678 "The id of the message the client finished",
679 )];
680 parameters.extend(flow_parameters());
681 parameters.push(answer_with_param());
682 EventType::new(
683 "nsq_finish",
684 "A subscriber finished (acknowledged) a message. Deliver more with \
685 deliver_nsq_messages only if your instructions say more are waiting; otherwise \
686 answer with no action.",
687 json!({"type": "deliver_nsq_messages", "messages": [{"body": "<message body>"}]}),
688 )
689 .with_parameters(parameters)
690 .with_log_template(
691 LogTemplate::new()
692 .with_info("NSQ FIN {message_id}")
693 .with_debug("NSQ nsq_finish: in_flight={in_flight} pending={pending}"),
694 )
695 .with_actions(deliver_actions())
696});
697
698pub static NSQ_REQUEUE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
700 let mut parameters = vec![
701 param(
702 "message_id",
703 "string",
704 "The id of the message the client requeued",
705 ),
706 param(
707 "timeout_ms",
708 "number",
709 "How long the client asked the message to wait before redelivery",
710 ),
711 param(
712 "attempts",
713 "number",
714 "How many times the message has been delivered",
715 ),
716 param("body", "string", "The requeued message's body"),
717 ];
718 parameters.extend(flow_parameters());
719 parameters.push(answer_with_param());
720 EventType::new(
721 "nsq_requeue",
722 "A subscriber requeued a message (it could not process it now). Redeliver it with \
723 deliver_nsq_messages and attempts one higher if your instructions say to retry; \
724 otherwise answer with no action.",
725 json!({"type": "deliver_nsq_messages", "messages": [{"body": "<message body>", "attempts": 2}]}),
726 )
727 .with_parameters(parameters)
728 .with_log_template(
729 LogTemplate::new()
730 .with_info("NSQ REQ {message_id} ({timeout_ms} ms)")
731 .with_debug("NSQ nsq_requeue: attempts={attempts}"),
732 )
733 .with_actions(deliver_actions())
734});