Skip to main content

openai_api_dispatch/queue/nats/
mod.rs

1//! JSON request/reply on Core NATS, without JetStream persistence or task retries.
2//!
3//! Subjects concatenate a prefix and model verbatim; no separator is inserted.
4//! Each task needs a reply subject. Ensure a worker subscription is active before
5//! publishing: constructors do not flush subscriptions as a readiness barrier.
6
7use std::env;
8
9use alloc::sync::Arc;
10use async_nats::{Client, Message, Subscriber};
11use tokio::sync::Mutex;
12use tokio_stream::StreamExt as _;
13
14use crate::{
15    queue::{QueueProducer, QueueWorker, Response, Task, WrappedTask},
16    utils,
17};
18
19#[cfg(test)]
20mod tests;
21
22#[derive(Debug, Clone)]
23/// Publishes JSON tasks with a dedicated inbox for each response.
24///
25/// Explicit task models override the configured default. Sending fails if
26/// neither exists. Receiving waits for the first inbox message and decodes it
27/// as a [`Response`]; no built-in response timeout or NATS-status handling is
28/// provided. Use [`Task::send_and_wait`] with a timeout to bound the reply wait.
29pub struct NatsProducer {
30    client: Client,
31    default_model: Arc<Option<String>>,
32    prefix: String,
33}
34
35#[derive(Debug, Clone)]
36/// Consumes one model subject in the workers queue group.
37///
38/// Clones share one subscription. Replies require the incoming message's reply
39/// subject; malformed JSON and missing reply subjects return errors.
40pub struct NatsWorker {
41    client: Client,
42    subscriber: Arc<Mutex<Subscriber>>,
43}
44
45fn format_subject(prefix: &str, model: &str) -> String {
46    format!("{prefix}{model}")
47}
48
49impl NatsProducer {
50    /// Connects using `OPENAI_API_NATS_URL` (default `nats://localhost:4222`).
51    ///
52    /// Snapshots `OPENAI_API_NATS_PREFIX` (default `openai-api-queue/`) and the
53    /// optional `OPENAI_API_DEFAULT_MODEL`. Connection errors are returned.
54    pub async fn from_env_or_default() -> anyhow::Result<Self> {
55        let default_model = utils::get_default_model();
56        let prefix =
57            env::var("OPENAI_API_NATS_PREFIX").unwrap_or_else(|_| "openai-api-queue/".into());
58        let url =
59            env::var("OPENAI_API_NATS_URL").unwrap_or_else(|_| "nats://localhost:4222".into());
60        let client = async_nats::ConnectOptions::new()
61            .request_timeout(None)
62            .connect(url)
63            .await?;
64
65        tracing::info!(
66            "nats client connected to `{:?}`",
67            client.server_info().connect_urls
68        );
69
70        Ok(Self {
71            client,
72            default_model,
73            prefix,
74        })
75    }
76
77    fn subject(&self, task: &Task) -> anyhow::Result<String> {
78        let model = task.model(&self.default_model)?;
79
80        Ok(format_subject(&self.prefix, &model))
81    }
82}
83
84impl NatsWorker {
85    /// Connects and subscribes to the prefix plus `OPENAI_API_DEFAULT_MODEL`.
86    ///
87    /// Requires the model variable. URL/prefix defaults match
88    /// [`NatsProducer::from_env_or_default`]; missing configuration, connection,
89    /// and subscription errors are returned. No subscription flush is performed.
90    pub async fn from_env_or_default() -> anyhow::Result<Self> {
91        let model = utils::get_default_model()
92            .as_ref()
93            .clone()
94            .ok_or_else(|| anyhow::anyhow!("no model provided for the nats worker."))?;
95        let prefix =
96            env::var("OPENAI_API_NATS_PREFIX").unwrap_or_else(|_| "openai-api-queue/".into());
97        let subject = format_subject(&prefix, &model);
98
99        let url =
100            env::var("OPENAI_API_NATS_URL").unwrap_or_else(|_| "nats://localhost:4222".into());
101        let client = async_nats::ConnectOptions::new()
102            .request_timeout(None)
103            .connect(url)
104            .await?;
105
106        let workers_group =
107            env::var("OPENAI_API_NATS_WORKERS_GROUP").unwrap_or_else(|_| "task_workers".into());
108        let subscriber = client
109            .queue_subscribe(subject.clone(), workers_group)
110            .await?;
111        let subscriber = Arc::new(Mutex::new(subscriber));
112
113        tracing::info!(
114            "nats worker connected to `{:?}` with subject `{subject}`",
115            client.server_info().connect_urls
116        );
117
118        Ok(Self { client, subscriber })
119    }
120}
121
122impl QueueProducer for NatsProducer {
123    type Message = Subscriber;
124
125    async fn send_task(&self, task: Task) -> anyhow::Result<Self::Message> {
126        tracing::debug!("nats queue; sending task `{}`", task.id,);
127
128        let subject = self.subject(&task)?;
129
130        tracing::debug!("nats queue; task `{}` subject `{subject}`", task.id,);
131
132        let payload = serde_json::to_vec(&task)?;
133
134        let inbox = self.client.new_inbox();
135        let subscriber = self.client.subscribe(inbox.clone()).await?;
136
137        self.client
138            .publish_with_reply(subject.to_string(), inbox, payload.into())
139            .await?;
140
141        tracing::debug!("nats queue; task `{}` sent", task.id,);
142
143        Ok(subscriber)
144    }
145
146    async fn receive_response(
147        &self,
148        mut message: Self::Message,
149    ) -> anyhow::Result<Option<Response>> {
150        match message.next().await {
151            Some(m) => Ok(Some(serde_json::from_slice(&m.payload)?)),
152            None => Ok(None),
153        }
154    }
155}
156
157impl QueueWorker for NatsWorker {
158    type Message = Message;
159
160    async fn receive_task(&self) -> anyhow::Result<Option<WrappedTask<Self::Message>>> {
161        let message = match self.subscriber.lock().await.next().await {
162            Some(m) => m,
163            None => return Ok(None),
164        };
165        let task = serde_json::from_slice(&message.payload)?;
166
167        Ok(Some(WrappedTask { message, task }))
168    }
169
170    async fn send_response(
171        &self,
172        message: Self::Message,
173        response: Response,
174    ) -> anyhow::Result<()> {
175        let payload = serde_json::to_vec(&response)?;
176        let reply_to = message
177            .reply
178            .ok_or_else(|| anyhow::anyhow!("message reply recipient empty"))?;
179
180        self.client.publish(reply_to, payload.into()).await?;
181
182        Ok(())
183    }
184}