Skip to content

Commit 3ac0c70

Browse files
Merge pull request #731 from swimos/mqtt
Adds MQTT connector.
2 parents 3d18c95 + ae059cc commit 3ac0c70

File tree

78 files changed

+5665
-1240
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+5665
-1240
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ members = [
4545
"example_apps/stocks_simulated",
4646
"example_apps/kafka_ingress_connector",
4747
"example_apps/kafka_egress_connector",
48-
"example_apps/fluvio_ingress_connector"
48+
"example_apps/fluvio_ingress_connector",
49+
"example_apps/mqtt_ingress_connector",
50+
"example_apps/mqtt_egress_connector"
4951
]
5052

5153
[workspace.package]
@@ -86,6 +88,7 @@ swimos_introspection = { path = "server/swimos_introspection", version = "0.1.1"
8688
swimos_server_app = { path = "server/swimos_server_app", version = "0.1.1" }
8789
swimos_connector = { path = "server/swimos_connector", version = "0.1.1" }
8890
swimos_connector_kafka = { path = "server/swimos_connector_kafka", version = "0.1.1" }
91+
swimos_connector_mqtt = { path = "server/swimos_connector_mqtt", version = "0.1.1" }
8992
swimos_connector_fluvio = { path = "server/swimos_connector_fluvio", version = "0.1.1" }
9093
swimos = { path = "swimos", version = "0.1.1" }
9194
swimos_client = { path = "swimos_client", version = "0.1.1" }
@@ -186,4 +189,5 @@ hyper-util = "0.1.5"
186189
rdkafka = "0.36"
187190
apache-avro = "0.17.0"
188191
time = "0.3.36"
192+
rumqttc = "0.24.0"
189193
fluvio = "0.23.2"

api/swimos_api/src/address.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
1717
use std::fmt::{Debug, Display};
1818

19+
use swimos_form::Form;
1920
use swimos_utilities::encoding::BytesStr;
2021

2122
use swimos_model::Text;
@@ -30,7 +31,7 @@ pub struct RelativeAddress<T> {
3031
}
3132

3233
/// A fully qualified address of a Swim lane.
33-
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
34+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Form)]
3435
pub struct Address<T> {
3536
/// The host at which the lane can be found. If absent this will be inferred from the routing mesh.
3637
pub host: Option<T>,

api/swimos_form/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ num-bigint = { workspace = true }
1919

2020
[dev-dependencies]
2121
trybuild = { workspace = true }
22+
23+
[lints.rust]
24+
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)'] }

api/swimos_form/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use trybuild::TestCases;
1616

1717
#[test]
18+
#[cfg_attr(tarpaulin, ignore)]
1819
fn test_derive() {
1920
let t = TestCases::new();
2021

docs/connectors.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Connectors for External Data Sources and Sinks
2+
==============================================
3+
4+
Connectors make it easier to create an interface between your Swim application and external data sources and repositories. They come in two varieties: ingress connectors and egress connectors. Ingress connectors allow you to update the the state of your application using a stream of events produced by an external system. Conversely, an egress connector observes the state of one or more Swim lanes and writes to changes into an external data sink.
5+
6+
For example, an ingress connector could consume a stream of messages from a queue (such as Apache Kafka) or poll a database for changes to the rows of a table. A corresponding egress connector could publish messages to the queue or write new records into the database.
7+
8+
SwimOS provides a number of standard connector implementations and also exposes an API for writing your own connectors. Connectors run within an SwimOS server applications as entirely normal agents. In fact, there is not reason that you could not implement your own connectors using the standard agent API. The connector API exists only as a convenience to simplify the process of writing connectors by providing a core that is applicable to many kinds of data source or sink.
9+
10+
Contents
11+
--------
12+
13+
1. Provided connector implementations.
14+
* [Fluvio Connector](fluvio.md) - An ingress connector for [Fluvio](https://www.fluvio.io/).
15+
* [Kafka Connectors](kafka.md) - Ingress and egress connectors for [Apache Kafka](https://kafka.apache.org/).
16+
* [MQTT Connectors](mqtt.md) - Ingress and egress connectors for [MQTT](https://mqtt.org/) brokers.
17+
2. [Field selectors](selectors.md)
18+
3. The connector API.
19+
* TODO

docs/fluvio.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
TODO
2+
====

docs/kafka.md

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
Connector for Apache Kafka
2+
==========================
3+
4+
The `swimos_connector_kafka` crate provides both an ingress and an egress connector for Apache Kafka. Internally, it uses the [`rdkafka`](https://crates.io/crates/rdkafka) crate to communicate with the Kafka brokers. To use the connectors it is also necessary to add a dependency on the `swimos_connector` crate.
5+
6+
The create has feature flags `json` and `avro` to enable support for JSON (via the [`serde_json`](https://crates.io/crates/serde_json) crate) and [Apache Avro](https://avro.apache.org/) (via the [`apache-avro`](https://crates.io/crates/apache-avro) crate) as serialization formats.
7+
8+
Ingress connector
9+
-----------------
10+
11+
### Registering the connector
12+
13+
To register a Kafka ingress connector with your server application, add the following:
14+
15+
```rust
16+
use swimos_connector::IngressConnectorModel;
17+
use swimos_connector_kafka::{KafkaIngressConfiguration, KafkaIngressConnector};
18+
19+
let connector_config: KafkaIngressConfiguration = ...;
20+
21+
let connector_agent = IngressConnectorModel::for_fn(move || {
22+
KafkaIngressConnector::for_config(connector_config.clone())
23+
});
24+
```
25+
26+
The connector can then be registered as an agent route with the `add_route` method on the server builder. For example:
27+
28+
```
29+
server
30+
...
31+
.add_route(RoutePattern::parse_str("/kafka")?)
32+
...
33+
.build()
34+
```
35+
36+
The `KafkaIngressConfiguration` can be instantiated in code or deserialized from a recon file (as it implements the `swimos_form::Form` trait):
37+
38+
```rust
39+
let recon = tokio::fs::read_to_string("kafka-config.recon").await?;
40+
KafkaIngressConfiguration::from_str(&recon)?
41+
```
42+
43+
### Configuring the connector
44+
45+
An example configuration file for the Kafka ingress connector would be:
46+
47+
```recon
48+
@kafka {
49+
properties: {
50+
"group.id": consumer-test,
51+
"message.timeout.ms": "5000",
52+
"bootstrap.servers": "localhost:9092",
53+
"auto.offset.reset": smallest
54+
},
55+
log_level: @Debug,
56+
key_deserializer: @Int32(@BigEndian),
57+
payload_deserializer: @Json,
58+
topics: {
59+
"example"
60+
}
61+
value_lanes: {},
62+
map_lanes: {},
63+
relays: {},
64+
}
65+
```
66+
67+
The configuration parameters are:
68+
69+
* `properties` - This is the collection of properties (string key value pairs) that are passed to the underlying Kafka consumer. This must include the list of bootstrap servers that the client should connect to.
70+
* `log_level` - This is the log level to be passed to the underlying Kafka consumer. It does not affect the logging of the Swim server application.
71+
* `key_deserializer` - This specifies the deserializer to use for the keys of the incoming Kafka messages. This must be a variant of `swimos_connector::config::format::DataFormat`.
72+
* `payload_deserializer` - This specifies the deserializer to use for the payloads of the incoming Kafka messages. This must be a variant of `swimos_connector::config::format::DataFormat`.
73+
* `topics` - A list of Kafka topics for the connector to subscribe to.
74+
75+
The remaining fields `value_lanes`, `map_lanes` and `relays` specify how the connector should handle the incoming Kafka messages.
76+
77+
The specifications for each of these contain selector strings that will select components of the incoming Kafka messages. For the syntax for selectors, see [here](../selectors.md). The valid root selectors for the Kafka ingress connector are "$topic", "$key" and "$payload". The topic selector evaluates to the topic of the message (as a string) and the key and payload selectors evaluate to the values that were deserialized from these parts of the message.
78+
79+
#### Value lanes
80+
81+
Each entry in the `value_lanes` list will add a value lane to the connector agent. A value lane entry has the following format:
82+
83+
```recon
84+
@ValueLaneSpec {
85+
name: example_name,
86+
selector: "$payload",
87+
required: true
88+
}
89+
```
90+
91+
The fields of the specification are:
92+
93+
1. `name` - The name of the lane. This field is optional. If it is not defined the connector will attempt infer the name from the `selector` field (in this case it would be "payload").
94+
2. `selector` - Describes how to select a value for the value lane from each incoming Kafka message.
95+
3. `required` - Specifies if this value should be present in every message. If it is required and the selector cannot select a value from a message, the connector will fail with an error.
96+
97+
#### Map lanes
98+
99+
Each entry in the `map_lanes` list will add a map lane to the connector agent. A map lane entry has the following format:
100+
101+
```recon
102+
@MapLaneSpec {
103+
name: example_name,
104+
key_selector: "$payload.key",
105+
value_selector: "$payload.value",
106+
remove_when_no_value: false,
107+
required: true
108+
}
109+
```
110+
111+
For each message from the Kafka consumer, the connector will attempt to extract a pair of a key and value which it will use to update an entry in the map lane.
112+
113+
The fields of the specification are:
114+
115+
1. `name` - The name of the lane.
116+
2. `key_selector` - Describes how to select a key for the entry.
117+
3. `value_selector` - Describes hot to select a value for the entry.
118+
4. `remove_when_no_value` - If this is true and the key selector returns a value while the value selector does not, the key will be removed from the map lane.
119+
5. `required` - Specifies that an operation to be applied to the map must be selected for each Kafka message. If it is required and the selector cannot select a key an value from the message (or a key if `remote_when_no_value` is true), the connector will fail with an error.
120+
121+
#### Relays
122+
123+
For each entry in the `relays` list, each time a Kafka message is received a command will be sent to a lane on another agent. This can either be a single, fixed lane or derived from the contents of the message. Relays can point at either value-like (value lane, command lane etc) lanes or map lanes.
124+
125+
The format for a value relay is:
126+
127+
```recon
128+
@Value @ValueRelaySpec {
129+
node: "/node",
130+
lane: lane,
131+
payload: "$payload",
132+
required: false
133+
}
134+
```
135+
136+
The format for a map relay is:
137+
138+
```recon
139+
@Map @MapRelaySpec {
140+
node: "/node",
141+
lane: lane,
142+
key: "$key",
143+
value: "$payload",
144+
required: false,
145+
remove_when_no_value: true
146+
}
147+
```
148+
149+
The `node` and `lane` fields indicate which lane the command should be sent to. They can either be fixed or may contain selectors (for example `node: "/$payload.target` to choose the node based on the `target` field from the message payload).
150+
151+
The other fields have the same meanings as those for value lanes and map lanes above.
152+
153+
Egress connector
154+
----------------
155+
156+
### Registering the connector
157+
158+
To register a Kafka egress connector with your server application, add the following:
159+
160+
```rust
161+
use swimos_connector::EgressConnectorModel;
162+
use swimos_connector_kafka::{KafkaEgressConfiguration, KafkaEgressConnector};
163+
164+
let connector_config: KafkaEgressConfiguration = ...;
165+
166+
let connector_agent = EgressConnectorModel::for_fn(move || {
167+
KafkaEgressConnector::for_config(connector_config.clone())
168+
});
169+
```
170+
171+
The connector can then be registered as an agent route with the `add_route` method on the server builder. For example:
172+
173+
```
174+
server
175+
...
176+
.add_route(RoutePattern::parse_str("/kafka")?)
177+
...
178+
.build()
179+
```
180+
181+
The `KafkaEgressConfiguration` can be instantiated in code or deserialized from a recon file (as it implements the `swimos_form::Form` trait):
182+
183+
```rust
184+
let recon = tokio::fs::read_to_string("kafka-config.recon").await?;
185+
KafkaEgressConfiguration::from_str(&recon)?
186+
```
187+
188+
### Configuring the connector
189+
190+
An example configuration file for the Kafka egress connector would be:
191+
192+
```recon
193+
@kafka {
194+
properties: {
195+
"message.timeout.ms": "5000",
196+
"group.id": producer-test,
197+
"bootstrap.servers": "localhost:9092",
198+
},
199+
log_level: @Debug,
200+
key_serializer: @Int32(@BigEndian),
201+
payload_serializer: @Json,
202+
fixed_topic: example-topic,
203+
retry_timeout_ms: 5000,
204+
value_lanes: {},
205+
map_lanes: {},
206+
event_downlinks: {},
207+
map_event_downlinks: {},
208+
}
209+
```
210+
211+
The configuration parameters are:
212+
213+
* `properties` - This is the collection of properties (string key value pairs) that are passed to the underlying Kafka producer. This must include the list of bootstrap servers that the client should connect to.
214+
* `log_level` - This is the log level to be passed to the underlying Kafka producer. It does not affect the logging of the Swim server application.
215+
* `key_serializer` - This specifies the serializer to use for the keys of the outgoing Kafka messages. This must be a variant of `swimos_connector::config::format::DataFormat`.
216+
* `payload_serializer` - This specifies the serializer to use for the payloads of the outgoing Kafka messages. This must be a variant of `swimos_connector::config::format::DataFormat`.
217+
* `fixed_topic` - A fixed topic to send outgoing messages to. This can be overridden on a per-message basis. It is optional and if it is not defined all outgoing messages must have an explicit topic or the connector agent will fail with an error.
218+
* `retry_timeout_ms` - If the producer is busy when the connector attempts to send a message, it will try again after this timeout period (in milliseconds).
219+
220+
The remaining fields `value_lanes`, `map_lanes`, `event_downlinks` and `map_event_downlinks` specify how the connector should produce outgoing Kafka messages.
221+
222+
The specifications for each of these contain selector strings that will select components of the events that are generated by each of the lanes and downlinks. For the syntax for selectors, see [here](../selectors.md). The valid root selectors for the Kafka egress connector are "$key", "$value".
223+
224+
The key selector evaluates to the key of an event on a map lane or map downlink and will always fail to select anything for the value equivalents. The value selector will select the value associated with any event.
225+
226+
Each outgoing Kafka message must be sent to a specific topic. Each of the types of item listed about require a topic selector. The possible topic selectors are:
227+
228+
* `@Fixed` - Uses the topic give in the `fixed_topic` configuration parameter.
229+
* `@Specified("target")` - An explicitly named topic (in this case "target").
230+
* `@Selector("$value.topic")` - Attempts to extract the topic from the contents of the events using a selector.
231+
232+
#### Value and map lanes
233+
234+
Each entry in the `value_lanes` list will add a value lane to the connector agent. Similarly,
235+
each entry on the `map_lanes` list will add a map lane to the agent. Both have the following format:
236+
237+
```recon
238+
@LaneSpec {
239+
name: event,
240+
extractor: @ExtractionSpec {
241+
topic_specifier: @Fixed,
242+
key_selector: "$value.key",
243+
payload_selector: "$value.payload"
244+
}
245+
}
246+
```
247+
248+
The fields of the specification are:
249+
250+
1. `name` - The name of the lane.
251+
2. `topic_specifier` - Describes how to select a topic from the lane events.
252+
3. `key_selector` - Describes how to select the Kafka key from the lane events.
253+
4. `payload_selector` - Describes how to select the Kafka payload from the lane events.
254+
255+
For each value set to a value lane or each key/value pair generated by an update to a map lane pair of Recon values will be extracted using the key and payload selectors. Additionally, a string value will be extracted with the topic specifier to indicate a Kafka topic. These will be combined to create a Kafka message which will be published, via the configured serializers.
256+
257+
The Kafka producer runs in a separate thread and, if it is too busy to accept a message, the connector will keep the most recent message for each topic and periodically retry.
258+
259+
#### Event and map-event downlinks
260+
261+
For each entry in the `event_downlinks` and `map_event_downlinks`, the connector agent will open a downlink, of the appropriate type to the specified lane. Both have the following format:
262+
263+
```recon
264+
@DownlinkSpec {
265+
address: @Address {
266+
host: "localhost:9000",
267+
node: "/node",
268+
lane: "lane",
269+
},
270+
extractor: @ExtractionSpec {
271+
topic_specifier: @Selector("$value.topic),
272+
key_selector: "$value.key",
273+
payload_selector: "$value.payload"
274+
}
275+
}
276+
```
277+
278+
The `host` field indicates the SwimOS server instance where the lane is located. This is optional and if it is absent, the local instance hosting the connector will be assumed. The `node` and `lane` fields specify the coordinates of the lane.
279+
280+
The extractor specification works in exactly the same way as for value an map lanes.
281+
282+
283+

0 commit comments

Comments
 (0)