Skip to main content

spin_factor_outbound_mqtt/
lib.rs

1mod allowed_hosts;
2mod host;
3pub mod runtime_config;
4
5use std::sync::Arc;
6use std::time::Duration;
7
8use host::InstanceState;
9use rumqttc::{AsyncClient, Event, Incoming, Outgoing, QoS};
10use spin_core::async_trait;
11use spin_factor_otel::OtelFactorState;
12use spin_factor_outbound_networking::{
13    ConnectionSemaphore, OutboundNetworkingFactor, build_connection_semaphore,
14};
15use spin_factors::{
16    ConfigureAppContext, Factor, FactorData, PrepareContext, RuntimeFactors, SelfInstanceBuilder,
17    anyhow,
18};
19use spin_world::spin::mqtt::mqtt as v3;
20use spin_world::v2::mqtt as v2;
21use tokio::sync::Mutex;
22
23pub use host::MqttClient;
24
25use crate::host::other_error_v3;
26use crate::runtime_config::RuntimeConfig;
27
28pub struct OutboundMqttFactor {
29    create_client: Arc<dyn ClientCreator>,
30}
31
32impl OutboundMqttFactor {
33    pub fn new(create_client: Arc<dyn ClientCreator>) -> Self {
34        Self { create_client }
35    }
36}
37
38pub struct AppState {
39    /// Optional maximum payload size in bytes for MQTT messages. If `None`, no limit is enforced.
40    max_payload_size_bytes: Option<usize>,
41    /// Semaphore to limit concurrent outbound MQTT connections.
42    pub semaphore: ConnectionSemaphore,
43}
44
45impl Factor for OutboundMqttFactor {
46    type RuntimeConfig = RuntimeConfig;
47    type AppState = AppState;
48    type InstanceBuilder = InstanceState;
49
50    fn init<T: spin_factors::InitContext<Self>>(&mut self, ctx: &mut T) -> anyhow::Result<()> {
51        ctx.link_bindings(v2::add_to_linker::<_, FactorData<Self>>)?;
52        ctx.link_bindings(v3::add_to_linker::<_, MqttFactorData>)?;
53        Ok(())
54    }
55
56    fn configure_app<T: RuntimeFactors>(
57        &self,
58        mut ctx: ConfigureAppContext<T, Self>,
59    ) -> anyhow::Result<Self::AppState> {
60        let config = ctx.take_runtime_config().unwrap_or_default();
61        let networking = ctx.app_state::<OutboundNetworkingFactor>().ok();
62
63        Ok(AppState {
64            semaphore: build_connection_semaphore(
65                networking,
66                "mqtt",
67                config.max_connections,
68                config.wait_timeout,
69            ),
70            max_payload_size_bytes: config.max_payload_size_bytes,
71        })
72    }
73
74    fn prepare<T: RuntimeFactors>(
75        &self,
76        mut ctx: PrepareContext<T, Self>,
77    ) -> anyhow::Result<Self::InstanceBuilder> {
78        let allowed_hosts = ctx
79            .instance_builder::<OutboundNetworkingFactor>()?
80            .allowed_hosts();
81        let otel = OtelFactorState::from_prepare_context(&mut ctx)?;
82
83        Ok(InstanceState::new(
84            allowed_hosts,
85            self.create_client.clone(),
86            ctx.app_state().semaphore.clone(),
87            otel,
88            ctx.app_state().max_payload_size_bytes,
89        ))
90    }
91}
92
93impl SelfInstanceBuilder for InstanceState {}
94
95struct MqttFactorData;
96
97impl spin_core::wasmtime::component::HasData for MqttFactorData {
98    type Data<'a> = &'a mut InstanceState;
99}
100
101// This is a concrete implementation of the MQTT client using rumqttc.
102pub struct NetworkedMqttClient {
103    inner: rumqttc::AsyncClient,
104    event_loop: Mutex<rumqttc::EventLoop>,
105}
106
107const MQTT_CHANNEL_CAP: usize = 1000;
108
109impl NetworkedMqttClient {
110    /// Create a [`ClientCreator`] that creates a [`NetworkedMqttClient`].
111    pub fn creator() -> Arc<dyn ClientCreator> {
112        Arc::new(|address, username, password, keep_alive_interval| {
113            Ok(Arc::new(NetworkedMqttClient::create(
114                address,
115                username,
116                password,
117                keep_alive_interval,
118            )?) as _)
119        })
120    }
121
122    /// Create a new [`NetworkedMqttClient`] with the given address, username, password, and keep alive interval.
123    pub fn create(
124        address: String,
125        username: String,
126        password: String,
127        keep_alive_interval: Duration,
128    ) -> Result<Self, v3::Error> {
129        let mut conn_opts = rumqttc::MqttOptions::parse_url(address).map_err(|e| {
130            tracing::error!("MQTT URL parse error: {e:?}");
131            v3::Error::InvalidAddress
132        })?;
133        conn_opts.set_credentials(username, password);
134        conn_opts.set_keep_alive(keep_alive_interval);
135        let (client, event_loop) = AsyncClient::new(conn_opts, MQTT_CHANNEL_CAP);
136        Ok(Self {
137            inner: client,
138            event_loop: Mutex::new(event_loop),
139        })
140    }
141}
142
143#[async_trait]
144impl MqttClient for NetworkedMqttClient {
145    async fn publish_bytes(
146        &self,
147        topic: String,
148        qos: v3::Qos,
149        payload: Vec<u8>,
150    ) -> Result<(), v3::Error> {
151        let qos = match qos {
152            v3::Qos::AtMostOnce => rumqttc::QoS::AtMostOnce,
153            v3::Qos::AtLeastOnce => rumqttc::QoS::AtLeastOnce,
154            v3::Qos::ExactlyOnce => rumqttc::QoS::ExactlyOnce,
155        };
156        // Message published to EventLoop (not MQTT Broker)
157        self.inner
158            .publish_bytes(topic, qos, false, payload.into())
159            .await
160            .map_err(other_error_v3)?;
161
162        // Poll event loop until outgoing publish event is iterated over to send the message to MQTT broker or capture/throw error.
163        // We may revisit this later to manage long running connections, high throughput use cases and their issues in the connection pool.
164        let mut lock = self.event_loop.lock().await;
165        loop {
166            let event = lock
167                .poll()
168                .await
169                .map_err(|err| v3::Error::ConnectionFailed(err.to_string()))?;
170
171            match (qos, event) {
172                (QoS::AtMostOnce, Event::Outgoing(Outgoing::Publish(_)))
173                | (QoS::AtLeastOnce, Event::Incoming(Incoming::PubAck(_)))
174                | (QoS::ExactlyOnce, Event::Incoming(Incoming::PubComp(_))) => break,
175
176                (_, _) => continue,
177            }
178        }
179        Ok(())
180    }
181}
182
183/// A trait for creating MQTT client.
184#[async_trait]
185pub trait ClientCreator: Send + Sync {
186    fn create(
187        &self,
188        address: String,
189        username: String,
190        password: String,
191        keep_alive_interval: Duration,
192    ) -> Result<Arc<dyn MqttClient>, v3::Error>;
193}
194
195impl<F> ClientCreator for F
196where
197    F: Fn(String, String, String, Duration) -> Result<Arc<dyn MqttClient>, v3::Error> + Send + Sync,
198{
199    fn create(
200        &self,
201        address: String,
202        username: String,
203        password: String,
204        keep_alive_interval: Duration,
205    ) -> Result<Arc<dyn MqttClient>, v3::Error> {
206        self(address, username, password, keep_alive_interval)
207    }
208}