spin_factor_outbound_mqtt/
lib.rs1mod 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 max_payload_size_bytes: Option<usize>,
41 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
101pub struct NetworkedMqttClient {
103 inner: rumqttc::AsyncClient,
104 event_loop: Mutex<rumqttc::EventLoop>,
105}
106
107const MQTT_CHANNEL_CAP: usize = 1000;
108
109impl NetworkedMqttClient {
110 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 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 self.inner
158 .publish_bytes(topic, qos, false, payload.into())
159 .await
160 .map_err(other_error_v3)?;
161
162 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#[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}