prosody 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cargo/config.toml +3 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +395 -0
- data/ARCHITECTURE.md +14 -4
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +1 -0
- data/CONFIGURATION.md +167 -0
- data/Cargo.lock +1115 -645
- data/Cargo.toml +7 -6
- data/README.md +436 -146
- data/Rakefile +11 -1
- data/examples/keyed_state.rb +70 -0
- data/examples/keyed_state.rbs +18 -0
- data/examples/keyed_state_windowing.rb +55 -0
- data/examples/keyed_state_windowing.rbs +16 -0
- data/ext/prosody/Cargo.toml +1 -0
- data/ext/prosody/src/admin.rs +1 -5
- data/ext/prosody/src/bridge/mod.rs +17 -32
- data/ext/prosody/src/client/config.rs +501 -28
- data/ext/prosody/src/client/mod.rs +167 -74
- data/ext/prosody/src/client/request.rs +132 -0
- data/ext/prosody/src/client/support.rs +122 -0
- data/ext/prosody/src/handler/context.rs +150 -5
- data/ext/prosody/src/handler/message.rs +67 -0
- data/ext/prosody/src/handler/mod.rs +115 -85
- data/ext/prosody/src/handler/state/mod.rs +488 -0
- data/ext/prosody/src/handler/state/registration.rs +104 -0
- data/ext/prosody/src/handler/state/scan.rs +218 -0
- data/ext/prosody/src/lib.rs +15 -3
- data/ext/prosody/src/published.rs +273 -0
- data/ext/prosody/src/scheduler/mod.rs +2 -2
- data/ext/prosody/src/scheduler/processor.rs +2 -2
- data/ext/prosody/src/scheduler/result.rs +7 -4
- data/ext/prosody/src/util.rs +86 -5
- data/lib/prosody/configuration.rb +71 -11
- data/lib/prosody/handler.rb +65 -8
- data/lib/prosody/native_stubs.rb +550 -9
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +816 -0
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +6 -0
- data/release-please-config.json +4 -0
- data/sig/configuration.rbs +70 -11
- data/sig/handler.rbs +17 -5
- data/sig/processor.rbs +28 -12
- data/sig/prosody.rbs +53 -7
- data/sig/request.rbs +66 -0
- data/sig/sentry.rbs +6 -0
- data/sig/state.rbs +390 -0
- data/steep_expectations.yml +57 -0
- data/typecheck/payload_types.rb +54 -0
- data/typecheck/payload_types.rbs +22 -0
- data/typecheck_negative/payload_types.rb +20 -0
- data/typecheck_negative/payload_types.rbs +9 -0
- metadata +32 -9
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
//! builders.
|
|
8
8
|
|
|
9
9
|
use magnus::{Error, Ruby, Value};
|
|
10
|
+
use prosody::PeerConfiguration;
|
|
11
|
+
use prosody::PeerEndpoint;
|
|
10
12
|
use prosody::cassandra::config::CassandraConfigurationBuilder;
|
|
11
13
|
use prosody::consumer::ConsumerConfigurationBuilder;
|
|
14
|
+
use prosody::consumer::KeyedStateConfiguration;
|
|
12
15
|
use prosody::consumer::SpanRelation;
|
|
16
|
+
use prosody::consumer::kafka_state::{message_deque_state, message_map_state, message_state};
|
|
13
17
|
use prosody::consumer::middleware::deduplication::DeduplicationConfigurationBuilder;
|
|
14
18
|
use prosody::consumer::middleware::defer::DeferConfigurationBuilder;
|
|
15
19
|
use prosody::consumer::middleware::monopolization::MonopolizationConfigurationBuilder;
|
|
@@ -19,11 +23,23 @@ use prosody::consumer::middleware::timeout::TimeoutConfigurationBuilder;
|
|
|
19
23
|
use prosody::consumer::middleware::topic::FailureTopicConfigurationBuilder;
|
|
20
24
|
use prosody::high_level::ConsumerBuilders;
|
|
21
25
|
use prosody::high_level::mode::Mode;
|
|
26
|
+
use prosody::loader::KafkaLoader;
|
|
27
|
+
use prosody::loader::KafkaLoaderConfiguration;
|
|
22
28
|
use prosody::producer::ProducerConfigurationBuilder;
|
|
29
|
+
use prosody::state::descriptor::{
|
|
30
|
+
DequeDescriptor, MapDescriptor, StateDescriptor, deque_state, map_state, value_state,
|
|
31
|
+
};
|
|
32
|
+
use prosody::state::order_codec::Utf8KeyCodec;
|
|
33
|
+
use prosody::subsystem::SubsystemName;
|
|
23
34
|
use prosody::telemetry::emitter::TelemetryEmitterConfiguration;
|
|
35
|
+
use prosody::timers::duration::CompactDuration;
|
|
36
|
+
use prosody::{ByteSize, JsonCodec};
|
|
24
37
|
use serde::{Deserialize, Deserializer};
|
|
25
38
|
use serde_magnus::deserialize;
|
|
26
39
|
use serde_untagged::UntaggedEnumVisitor;
|
|
40
|
+
use std::net::SocketAddr;
|
|
41
|
+
use std::num::NonZeroUsize;
|
|
42
|
+
use std::path::PathBuf;
|
|
27
43
|
use std::time::Duration;
|
|
28
44
|
|
|
29
45
|
/// Configuration structure for the Prosody client that maps Ruby configuration
|
|
@@ -104,7 +120,7 @@ pub struct NativeConfiguration {
|
|
|
104
120
|
/// List of Cassandra contact nodes (hostnames or IPs)
|
|
105
121
|
cassandra_nodes: Option<Vec<String>>,
|
|
106
122
|
|
|
107
|
-
/// Keyspace
|
|
123
|
+
/// Keyspace used for persistent Prosody data in Cassandra.
|
|
108
124
|
cassandra_keyspace: Option<String>,
|
|
109
125
|
|
|
110
126
|
/// Preferred datacenter for Cassandra query routing
|
|
@@ -119,8 +135,8 @@ pub struct NativeConfiguration {
|
|
|
119
135
|
/// Password for authenticating with Cassandra
|
|
120
136
|
cassandra_password: Option<String>,
|
|
121
137
|
|
|
122
|
-
/// Retention period for
|
|
123
|
-
/// seconds
|
|
138
|
+
/// Retention period for persistent timer and deferral data in Cassandra,
|
|
139
|
+
/// in seconds.
|
|
124
140
|
cassandra_retention: Option<f32>,
|
|
125
141
|
|
|
126
142
|
/// Timer slab partitioning duration in seconds.
|
|
@@ -166,24 +182,24 @@ pub struct NativeConfiguration {
|
|
|
166
182
|
/// Maximum delay between deferred retries (in seconds).
|
|
167
183
|
defer_max_delay: Option<f32>,
|
|
168
184
|
|
|
169
|
-
/// Failure rate threshold for
|
|
185
|
+
/// Failure rate threshold for disabling deferral (0.0 to 1.0).
|
|
170
186
|
defer_failure_threshold: Option<f64>,
|
|
171
187
|
|
|
172
188
|
/// Sliding window duration (in seconds) for failure rate tracking.
|
|
173
189
|
defer_failure_window: Option<f32>,
|
|
174
190
|
|
|
175
|
-
///
|
|
176
|
-
|
|
191
|
+
/// Maximum messages retained by the shared Kafka loader.
|
|
192
|
+
loader_cache_size: Option<u32>,
|
|
177
193
|
|
|
178
194
|
/// Maximum number of deferred store entries kept in the write-through cache
|
|
179
195
|
/// per Cassandra defer store.
|
|
180
196
|
defer_store_cache_size: Option<u32>,
|
|
181
197
|
|
|
182
|
-
/// Timeout for Kafka seek operations (in seconds).
|
|
183
|
-
|
|
198
|
+
/// Timeout for Kafka loader seek operations (in seconds).
|
|
199
|
+
loader_seek_timeout: Option<f32>,
|
|
184
200
|
|
|
185
201
|
/// Messages to read sequentially before seeking.
|
|
186
|
-
|
|
202
|
+
loader_discard_threshold: Option<i64>,
|
|
187
203
|
|
|
188
204
|
// Timeout configuration
|
|
189
205
|
/// Fixed timeout duration for handler execution (in seconds).
|
|
@@ -202,6 +218,85 @@ pub struct NativeConfiguration {
|
|
|
202
218
|
|
|
203
219
|
/// Span linking for timer execution spans (`child` or `follows_from`).
|
|
204
220
|
timer_spans: Option<String>,
|
|
221
|
+
|
|
222
|
+
/// Address for the peer listener.
|
|
223
|
+
peer_bind_address: Option<String>,
|
|
224
|
+
|
|
225
|
+
/// gRPC connect URI that peers use for this client.
|
|
226
|
+
peer_advertised_connect: Option<String>,
|
|
227
|
+
|
|
228
|
+
/// Network name used to identify direct routes.
|
|
229
|
+
peer_network_name: Option<String>,
|
|
230
|
+
|
|
231
|
+
/// Maximum number of peer channels and registrations held in each cache.
|
|
232
|
+
peer_cache_capacity: Option<usize>,
|
|
233
|
+
|
|
234
|
+
/// Duration of each peer registration lease, in seconds.
|
|
235
|
+
peer_registration_ttl: Option<f64>,
|
|
236
|
+
|
|
237
|
+
// Keyed-state configuration
|
|
238
|
+
/// Keyed-state collections to register before subscribe.
|
|
239
|
+
state_collections: Option<Vec<StateCollectionConfig>>,
|
|
240
|
+
|
|
241
|
+
/// Subsystem under which published collections are advertised.
|
|
242
|
+
subsystem: Option<String>,
|
|
243
|
+
|
|
244
|
+
/// Root directory for the local keyed-state cache. Must not be empty.
|
|
245
|
+
state_cache_dir: Option<String>,
|
|
246
|
+
|
|
247
|
+
/// Capacity of the owning keyed-state cache.
|
|
248
|
+
state_owned_cache_size: Option<String>,
|
|
249
|
+
|
|
250
|
+
/// Capacity of the published-state read-through cache.
|
|
251
|
+
state_read_cache_size: Option<String>,
|
|
252
|
+
|
|
253
|
+
/// Default cache policy for published-state reads.
|
|
254
|
+
state_read_cache: Option<ReadCacheConfig>,
|
|
255
|
+
|
|
256
|
+
/// Delay in whole seconds before the keyed-state recovery sweep.
|
|
257
|
+
///
|
|
258
|
+
/// Crosses as an `f64` so fractional/negative/non-finite values reach the
|
|
259
|
+
/// whole-number guard rather than being silently truncated.
|
|
260
|
+
state_recovery_delay: Option<f64>,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Declares one keyed-state collection to register before subscribe.
|
|
264
|
+
#[derive(Clone, Debug, Deserialize)]
|
|
265
|
+
struct StateCollectionConfig {
|
|
266
|
+
/// The collection name. Prosody requires it to be non-empty and unique.
|
|
267
|
+
name: String,
|
|
268
|
+
|
|
269
|
+
/// The collection kind: `"value"`, `"map"`, or `"deque"`.
|
|
270
|
+
kind: String,
|
|
271
|
+
|
|
272
|
+
/// The item payload: `"json"` or `"message"`.
|
|
273
|
+
payload: String,
|
|
274
|
+
|
|
275
|
+
/// Optional per-write TTL in whole seconds. Crosses as `f64` so
|
|
276
|
+
/// fractional/negative/non-finite values reach the whole-number guard.
|
|
277
|
+
ttl_seconds: Option<f64>,
|
|
278
|
+
|
|
279
|
+
/// Optional opt-out of transactional staging.
|
|
280
|
+
read_uncommitted: Option<bool>,
|
|
281
|
+
|
|
282
|
+
/// Whether other consumer groups may read this JSON collection.
|
|
283
|
+
published: Option<bool>,
|
|
284
|
+
|
|
285
|
+
/// Optional map-only keyset bound (`0..=4096`). The binding rejects values
|
|
286
|
+
/// that cannot map to an unsigned integer. Prosody enforces the ceiling.
|
|
287
|
+
keyset_limit: Option<f64>,
|
|
288
|
+
|
|
289
|
+
/// Optional deque-only window capacity (`>= 1`). Runtime-only and not
|
|
290
|
+
/// persisted. Crosses as `f64` so fractional/negative/non-finite values
|
|
291
|
+
/// reach the whole-number guard.
|
|
292
|
+
capacity: Option<f64>,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[derive(Clone, Debug, Deserialize)]
|
|
296
|
+
#[serde(untagged)]
|
|
297
|
+
enum ReadCacheConfig {
|
|
298
|
+
Disabled(bool),
|
|
299
|
+
Ttl(f64),
|
|
205
300
|
}
|
|
206
301
|
|
|
207
302
|
/// Configuration for the health probe port.
|
|
@@ -651,22 +746,10 @@ impl<'a> From<&'a NativeConfiguration> for DeferConfigurationBuilder {
|
|
|
651
746
|
builder.failure_window(Duration::from_secs_f32(*failure_window));
|
|
652
747
|
}
|
|
653
748
|
|
|
654
|
-
if let Some(cache_size) = &config.defer_cache_size {
|
|
655
|
-
builder.cache_size(*cache_size as usize);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
749
|
if let Some(store_cache_size) = &config.defer_store_cache_size {
|
|
659
750
|
builder.store_cache_size(*store_cache_size as usize);
|
|
660
751
|
}
|
|
661
752
|
|
|
662
|
-
if let Some(seek_timeout) = &config.defer_seek_timeout {
|
|
663
|
-
builder.seek_timeout(Duration::from_secs_f32(*seek_timeout));
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if let Some(discard_threshold) = &config.defer_discard_threshold {
|
|
667
|
-
builder.discard_threshold(*discard_threshold);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
753
|
builder
|
|
671
754
|
}
|
|
672
755
|
}
|
|
@@ -696,25 +779,41 @@ impl<'a> From<&'a NativeConfiguration> for TimeoutConfigurationBuilder {
|
|
|
696
779
|
}
|
|
697
780
|
}
|
|
698
781
|
|
|
699
|
-
impl<'a>
|
|
700
|
-
|
|
782
|
+
impl<'a> TryFrom<&'a NativeConfiguration> for DeduplicationConfigurationBuilder {
|
|
783
|
+
type Error = String;
|
|
784
|
+
|
|
785
|
+
/// Attempts to convert a `NativeConfiguration` reference into a
|
|
701
786
|
/// `DeduplicationConfigurationBuilder`.
|
|
702
787
|
///
|
|
703
788
|
/// This takes the relevant deduplication settings from the configuration
|
|
704
789
|
/// and sets them on a new `DeduplicationConfigurationBuilder` instance.
|
|
705
790
|
///
|
|
791
|
+
/// Consumer deduplication is mandatory in the core (it is the keyed-state
|
|
792
|
+
/// commit oracle), so `cache_capacity` is `NonZeroUsize` and a zero
|
|
793
|
+
/// capacity is unrepresentable rather than a silent "disable". An explicit
|
|
794
|
+
/// `idempotence_cache_size` of `0` is therefore rejected here rather than
|
|
795
|
+
/// silently defaulting; this mirrors the sibling `prosody-js` binding and
|
|
796
|
+
/// the core's own rejection of `PROSODY_IDEMPOTENCE_CACHE_SIZE=0`.
|
|
797
|
+
///
|
|
706
798
|
/// # Arguments
|
|
707
799
|
///
|
|
708
800
|
/// * `config` - The configuration to convert
|
|
709
801
|
///
|
|
710
802
|
/// # Returns
|
|
711
803
|
///
|
|
712
|
-
/// A configured `DeduplicationConfigurationBuilder`
|
|
713
|
-
|
|
804
|
+
/// A configured `DeduplicationConfigurationBuilder` if successful
|
|
805
|
+
///
|
|
806
|
+
/// # Errors
|
|
807
|
+
///
|
|
808
|
+
/// Returns a `String` error if `idempotence_cache_size` is explicitly set
|
|
809
|
+
/// to `0`.
|
|
810
|
+
fn try_from(config: &'a NativeConfiguration) -> Result<Self, Self::Error> {
|
|
714
811
|
let mut builder = Self::default();
|
|
715
812
|
|
|
716
813
|
if let Some(cache_capacity) = &config.idempotence_cache_size {
|
|
717
|
-
|
|
814
|
+
let cache_capacity = NonZeroUsize::new(*cache_capacity as usize)
|
|
815
|
+
.ok_or_else(|| "idempotence_cache_size must be greater than 0".to_owned())?;
|
|
816
|
+
builder.cache_capacity(cache_capacity);
|
|
718
817
|
}
|
|
719
818
|
|
|
720
819
|
if let Some(version) = &config.idempotence_version {
|
|
@@ -728,7 +827,7 @@ impl<'a> From<&'a NativeConfiguration> for DeduplicationConfigurationBuilder {
|
|
|
728
827
|
builder.ttl(Duration::from_secs_f64(*ttl));
|
|
729
828
|
}
|
|
730
829
|
|
|
731
|
-
builder
|
|
830
|
+
Ok(builder)
|
|
732
831
|
}
|
|
733
832
|
}
|
|
734
833
|
|
|
@@ -770,6 +869,354 @@ impl<'a> TryFrom<&'a NativeConfiguration> for TelemetryEmitterConfiguration {
|
|
|
770
869
|
}
|
|
771
870
|
}
|
|
772
871
|
|
|
872
|
+
/// The kind of a keyed-state collection.
|
|
873
|
+
enum CollectionKind {
|
|
874
|
+
/// A single-value collection.
|
|
875
|
+
Value,
|
|
876
|
+
/// A `String`-keyed ordered map.
|
|
877
|
+
Map,
|
|
878
|
+
/// A deque.
|
|
879
|
+
Deque,
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/// The item payload of a keyed-state collection.
|
|
883
|
+
enum CollectionPayload {
|
|
884
|
+
/// JSON values.
|
|
885
|
+
Json,
|
|
886
|
+
/// The full Kafka message the handler received.
|
|
887
|
+
Message,
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/// Parses a collection-kind token.
|
|
891
|
+
///
|
|
892
|
+
/// # Errors
|
|
893
|
+
///
|
|
894
|
+
/// Returns a permanent-category error naming the field if the token is not
|
|
895
|
+
/// `"value"`, `"map"`, or `"deque"`.
|
|
896
|
+
fn parse_kind(index: usize, kind: &str) -> Result<CollectionKind, String> {
|
|
897
|
+
match kind {
|
|
898
|
+
"value" => Ok(CollectionKind::Value),
|
|
899
|
+
"map" => Ok(CollectionKind::Map),
|
|
900
|
+
"deque" => Ok(CollectionKind::Deque),
|
|
901
|
+
other => Err(format!(
|
|
902
|
+
"state_collections[{index}].kind: expected \"value\", \"map\", or \"deque\", got \
|
|
903
|
+
{other:?}"
|
|
904
|
+
)),
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/// Parses a collection-payload token.
|
|
909
|
+
///
|
|
910
|
+
/// # Errors
|
|
911
|
+
///
|
|
912
|
+
/// Returns a permanent-category error naming the field if the token is not
|
|
913
|
+
/// `"json"` or `"message"`.
|
|
914
|
+
fn parse_payload(index: usize, payload: &str) -> Result<CollectionPayload, String> {
|
|
915
|
+
match payload {
|
|
916
|
+
"json" => Ok(CollectionPayload::Json),
|
|
917
|
+
"message" => Ok(CollectionPayload::Message),
|
|
918
|
+
other => Err(format!(
|
|
919
|
+
"state_collections[{index}].payload: expected \"json\" or \"message\", got {other:?}"
|
|
920
|
+
)),
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/// Validates a numeric field as a whole number within `min..=max`.
|
|
925
|
+
///
|
|
926
|
+
/// The field arrives as an `f64` (the raw Ruby number, un-coerced) so that
|
|
927
|
+
/// fractional, negative, and non-finite values reach this guard instead of
|
|
928
|
+
/// being silently truncated or wrapped by an earlier integer conversion.
|
|
929
|
+
///
|
|
930
|
+
/// # Errors
|
|
931
|
+
///
|
|
932
|
+
/// Returns a permanent-category error naming the field if the value is not a
|
|
933
|
+
/// whole number in the inclusive range.
|
|
934
|
+
fn whole_number_field(value: f64, field: &str, min: u32, max: u32) -> Result<u32, String> {
|
|
935
|
+
if value.is_finite()
|
|
936
|
+
&& value.fract() == 0.0
|
|
937
|
+
&& value >= f64::from(min)
|
|
938
|
+
&& value <= f64::from(max)
|
|
939
|
+
{
|
|
940
|
+
Ok(value as u32)
|
|
941
|
+
} else {
|
|
942
|
+
Err(format!("{field}: must be a whole number in {min}..={max}"))
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
fn build_peer_config(config: &NativeConfiguration) -> Result<PeerConfiguration, String> {
|
|
947
|
+
let mut builder = PeerConfiguration::builder();
|
|
948
|
+
if let Some(value) = &config.peer_bind_address {
|
|
949
|
+
builder.bind_address(
|
|
950
|
+
value
|
|
951
|
+
.parse::<SocketAddr>()
|
|
952
|
+
.map_err(|error| format!("peer_bind_address: {error}"))?,
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
if let Some(value) = &config.peer_advertised_connect {
|
|
956
|
+
builder.advertised_connect(
|
|
957
|
+
PeerEndpoint::try_from(value.clone())
|
|
958
|
+
.map_err(|error| format!("peer_advertised_connect: {error}"))?,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
if let Some(value) = &config.peer_network_name {
|
|
962
|
+
builder.network_name(value.clone());
|
|
963
|
+
}
|
|
964
|
+
if let Some(value) = config.peer_cache_capacity {
|
|
965
|
+
builder.peer_cache_capacity(value);
|
|
966
|
+
}
|
|
967
|
+
if let Some(value) = config.peer_registration_ttl {
|
|
968
|
+
builder.registration_ttl(
|
|
969
|
+
Duration::try_from_secs_f64(value)
|
|
970
|
+
.map_err(|_| "peer_registration_ttl: must be a valid duration".to_owned())?,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
builder.build().map_err(|error| error.to_string())
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/// Applies the shared descriptor options (TTL, commit mode) fluently.
|
|
977
|
+
fn with_def<D: StateDescriptor>(
|
|
978
|
+
descriptor: D,
|
|
979
|
+
ttl_seconds: Option<u32>,
|
|
980
|
+
read_uncommitted: Option<bool>,
|
|
981
|
+
published: Option<bool>,
|
|
982
|
+
) -> D {
|
|
983
|
+
let mut descriptor = descriptor;
|
|
984
|
+
if let Some(ttl) = ttl_seconds {
|
|
985
|
+
descriptor = descriptor.ttl(CompactDuration::new(ttl));
|
|
986
|
+
}
|
|
987
|
+
if read_uncommitted == Some(true) {
|
|
988
|
+
descriptor = descriptor.read_uncommitted();
|
|
989
|
+
}
|
|
990
|
+
if let Some(published) = published {
|
|
991
|
+
descriptor = descriptor.published(published);
|
|
992
|
+
}
|
|
993
|
+
descriptor
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/// Applies the map-only keyset bound when configured.
|
|
997
|
+
fn with_keyset<KC, V>(
|
|
998
|
+
descriptor: MapDescriptor<KC, V>,
|
|
999
|
+
keyset_limit: Option<u32>,
|
|
1000
|
+
) -> MapDescriptor<KC, V> {
|
|
1001
|
+
match keyset_limit {
|
|
1002
|
+
Some(limit) => descriptor.keyset_limit(limit as usize),
|
|
1003
|
+
None => descriptor,
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/// Applies the deque-only window capacity when configured.
|
|
1008
|
+
fn with_capacity<T>(
|
|
1009
|
+
descriptor: DequeDescriptor<T>,
|
|
1010
|
+
capacity: Option<NonZeroUsize>,
|
|
1011
|
+
) -> DequeDescriptor<T> {
|
|
1012
|
+
match capacity {
|
|
1013
|
+
Some(cap) => descriptor.capacity(cap),
|
|
1014
|
+
None => descriptor,
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/// Maps one collection into its descriptor over the closed 3×2 (kind ×
|
|
1019
|
+
/// payload) matrix.
|
|
1020
|
+
///
|
|
1021
|
+
/// # Errors
|
|
1022
|
+
///
|
|
1023
|
+
/// Returns a permanent-category error when a host value cannot be mapped into
|
|
1024
|
+
/// a Prosody type.
|
|
1025
|
+
fn register_state_collection(
|
|
1026
|
+
keyed: &mut KeyedStateConfiguration,
|
|
1027
|
+
index: usize,
|
|
1028
|
+
collection: &StateCollectionConfig,
|
|
1029
|
+
) -> Result<(), String> {
|
|
1030
|
+
let kind = parse_kind(index, &collection.kind)?;
|
|
1031
|
+
let payload = parse_payload(index, &collection.payload)?;
|
|
1032
|
+
|
|
1033
|
+
let ttl_seconds = match collection.ttl_seconds {
|
|
1034
|
+
Some(value) => Some(whole_number_field(
|
|
1035
|
+
value,
|
|
1036
|
+
&format!("state_collections[{index}].ttl_seconds"),
|
|
1037
|
+
0,
|
|
1038
|
+
u32::MAX,
|
|
1039
|
+
)?),
|
|
1040
|
+
None => None,
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
let keyset_limit = keyset_limit(collection.keyset_limit, &kind, index)?;
|
|
1044
|
+
let capacity = capacity(collection.capacity, &kind, index)?;
|
|
1045
|
+
|
|
1046
|
+
let read_uncommitted = collection.read_uncommitted;
|
|
1047
|
+
let name = collection.name.as_str();
|
|
1048
|
+
match (kind, payload) {
|
|
1049
|
+
(CollectionKind::Value, CollectionPayload::Json) => {
|
|
1050
|
+
let _ = keyed.register(with_def(
|
|
1051
|
+
value_state::<JsonCodec>(name),
|
|
1052
|
+
ttl_seconds,
|
|
1053
|
+
read_uncommitted,
|
|
1054
|
+
collection.published,
|
|
1055
|
+
));
|
|
1056
|
+
}
|
|
1057
|
+
(CollectionKind::Map, CollectionPayload::Json) => {
|
|
1058
|
+
let descriptor = with_def(
|
|
1059
|
+
map_state::<Utf8KeyCodec, JsonCodec>(name),
|
|
1060
|
+
ttl_seconds,
|
|
1061
|
+
read_uncommitted,
|
|
1062
|
+
collection.published,
|
|
1063
|
+
);
|
|
1064
|
+
let _ = keyed.register(with_keyset(descriptor, keyset_limit));
|
|
1065
|
+
}
|
|
1066
|
+
(CollectionKind::Deque, CollectionPayload::Json) => {
|
|
1067
|
+
let descriptor = with_def(
|
|
1068
|
+
deque_state::<JsonCodec>(name),
|
|
1069
|
+
ttl_seconds,
|
|
1070
|
+
read_uncommitted,
|
|
1071
|
+
collection.published,
|
|
1072
|
+
);
|
|
1073
|
+
let _ = keyed.register(with_capacity(descriptor, capacity));
|
|
1074
|
+
}
|
|
1075
|
+
(CollectionKind::Value, CollectionPayload::Message) => {
|
|
1076
|
+
let _ = keyed.register(with_def(
|
|
1077
|
+
message_state::<KafkaLoader<JsonCodec>>(name),
|
|
1078
|
+
ttl_seconds,
|
|
1079
|
+
read_uncommitted,
|
|
1080
|
+
collection.published,
|
|
1081
|
+
));
|
|
1082
|
+
}
|
|
1083
|
+
(CollectionKind::Map, CollectionPayload::Message) => {
|
|
1084
|
+
let descriptor = with_def(
|
|
1085
|
+
message_map_state::<Utf8KeyCodec, KafkaLoader<JsonCodec>>(name),
|
|
1086
|
+
ttl_seconds,
|
|
1087
|
+
read_uncommitted,
|
|
1088
|
+
collection.published,
|
|
1089
|
+
);
|
|
1090
|
+
let _ = keyed.register(with_keyset(descriptor, keyset_limit));
|
|
1091
|
+
}
|
|
1092
|
+
(CollectionKind::Deque, CollectionPayload::Message) => {
|
|
1093
|
+
let descriptor = with_def(
|
|
1094
|
+
message_deque_state::<KafkaLoader<JsonCodec>>(name),
|
|
1095
|
+
ttl_seconds,
|
|
1096
|
+
read_uncommitted,
|
|
1097
|
+
collection.published,
|
|
1098
|
+
);
|
|
1099
|
+
let _ = keyed.register(with_capacity(descriptor, capacity));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
Ok(())
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
fn keyset_limit(
|
|
1107
|
+
value: Option<f64>,
|
|
1108
|
+
kind: &CollectionKind,
|
|
1109
|
+
index: usize,
|
|
1110
|
+
) -> Result<Option<u32>, String> {
|
|
1111
|
+
let Some(value) = value else {
|
|
1112
|
+
return Ok(None);
|
|
1113
|
+
};
|
|
1114
|
+
if !matches!(kind, CollectionKind::Map) {
|
|
1115
|
+
return Err(format!(
|
|
1116
|
+
"state_collections[{index}].keyset_limit: only valid for map collections"
|
|
1117
|
+
));
|
|
1118
|
+
}
|
|
1119
|
+
whole_number_field(
|
|
1120
|
+
value,
|
|
1121
|
+
&format!("state_collections[{index}].keyset_limit"),
|
|
1122
|
+
0,
|
|
1123
|
+
u32::MAX,
|
|
1124
|
+
)
|
|
1125
|
+
.map(Some)
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
fn capacity(
|
|
1129
|
+
value: Option<f64>,
|
|
1130
|
+
kind: &CollectionKind,
|
|
1131
|
+
index: usize,
|
|
1132
|
+
) -> Result<Option<NonZeroUsize>, String> {
|
|
1133
|
+
let Some(value) = value else {
|
|
1134
|
+
return Ok(None);
|
|
1135
|
+
};
|
|
1136
|
+
if !matches!(kind, CollectionKind::Deque) {
|
|
1137
|
+
return Err(format!(
|
|
1138
|
+
"state_collections[{index}].capacity: only valid for deque collections"
|
|
1139
|
+
));
|
|
1140
|
+
}
|
|
1141
|
+
let value = whole_number_field(
|
|
1142
|
+
value,
|
|
1143
|
+
&format!("state_collections[{index}].capacity"),
|
|
1144
|
+
1,
|
|
1145
|
+
u32::MAX,
|
|
1146
|
+
)?;
|
|
1147
|
+
Ok(NonZeroUsize::new(value as usize))
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/// Builds the `KeyedStateConfiguration` by mapping each declared collection.
|
|
1151
|
+
/// The normal Prosody construction path validates the result.
|
|
1152
|
+
///
|
|
1153
|
+
/// # Errors
|
|
1154
|
+
///
|
|
1155
|
+
/// Returns an error if a host value cannot be mapped.
|
|
1156
|
+
fn build_keyed_state_config(
|
|
1157
|
+
config: &NativeConfiguration,
|
|
1158
|
+
) -> Result<KeyedStateConfiguration, String> {
|
|
1159
|
+
let mut builder = KeyedStateConfiguration::builder();
|
|
1160
|
+
|
|
1161
|
+
if let Some(dir) = &config.state_cache_dir {
|
|
1162
|
+
builder.cache_dir(PathBuf::from(dir));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if let Some(seconds) = config.state_recovery_delay {
|
|
1166
|
+
let seconds = whole_number_field(seconds, "state_recovery_delay", 0, u32::MAX)?;
|
|
1167
|
+
builder.recovery_delay(CompactDuration::new(seconds));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if let Some(size) = &config.state_owned_cache_size {
|
|
1171
|
+
let size = size
|
|
1172
|
+
.parse::<ByteSize>()
|
|
1173
|
+
.map_err(|error| format!("state_owned_cache_size: {error}"))?;
|
|
1174
|
+
builder.owned_cache_size(Some(size));
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if let Some(size) = &config.state_read_cache_size {
|
|
1178
|
+
let size = size
|
|
1179
|
+
.parse::<ByteSize>()
|
|
1180
|
+
.map_err(|error| format!("state_read_cache_size: {error}"))?;
|
|
1181
|
+
builder.read_cache_size(Some(size));
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if let Some(cache) = &config.state_read_cache {
|
|
1185
|
+
match cache {
|
|
1186
|
+
ReadCacheConfig::Disabled(false) => {
|
|
1187
|
+
builder.read_cache_ttl(None);
|
|
1188
|
+
}
|
|
1189
|
+
ReadCacheConfig::Disabled(true) => {
|
|
1190
|
+
return Err(
|
|
1191
|
+
"state_read_cache: true is ambiguous; use a duration or false".to_owned(),
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
ReadCacheConfig::Ttl(seconds) => {
|
|
1195
|
+
let ttl = Duration::try_from_secs_f64(*seconds).map_err(|_| {
|
|
1196
|
+
"state_read_cache: duration must be finite and non-negative".to_owned()
|
|
1197
|
+
})?;
|
|
1198
|
+
builder.read_cache_ttl(Some(ttl));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if let Some(subsystem) = &config.subsystem {
|
|
1204
|
+
builder.subsystem(Some(
|
|
1205
|
+
SubsystemName::try_new(subsystem).map_err(|error| error.to_string())?,
|
|
1206
|
+
));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
let mut keyed = builder.build().map_err(|error| error.to_string())?;
|
|
1210
|
+
|
|
1211
|
+
if let Some(collections) = &config.state_collections {
|
|
1212
|
+
for (index, collection) in collections.iter().enumerate() {
|
|
1213
|
+
register_state_collection(&mut keyed, index, collection)?;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
Ok(keyed)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
773
1220
|
impl<'a> TryFrom<&'a NativeConfiguration> for ConsumerBuilders {
|
|
774
1221
|
type Error = String;
|
|
775
1222
|
|
|
@@ -795,6 +1242,8 @@ impl<'a> TryFrom<&'a NativeConfiguration> for ConsumerBuilders {
|
|
|
795
1242
|
/// environment variable contains an unparseable value).
|
|
796
1243
|
/// - `message_spans` or `timer_spans` contains an unrecognized value
|
|
797
1244
|
/// (expected `"child"` or `"follows_from"`).
|
|
1245
|
+
/// - The Kafka loader configuration cannot be built (e.g. a tuning value
|
|
1246
|
+
/// fails validation).
|
|
798
1247
|
fn try_from(config: &'a NativeConfiguration) -> Result<Self, Self::Error> {
|
|
799
1248
|
let mut consumer: ConsumerConfigurationBuilder = config.into();
|
|
800
1249
|
|
|
@@ -812,6 +1261,28 @@ impl<'a> TryFrom<&'a NativeConfiguration> for ConsumerBuilders {
|
|
|
812
1261
|
consumer.timer_spans(relation);
|
|
813
1262
|
}
|
|
814
1263
|
|
|
1264
|
+
// Route the shared Kafka message loader settings onto the consumer.
|
|
1265
|
+
if config.loader_cache_size.is_some()
|
|
1266
|
+
|| config.loader_seek_timeout.is_some()
|
|
1267
|
+
|| config.loader_discard_threshold.is_some()
|
|
1268
|
+
{
|
|
1269
|
+
let mut loader = KafkaLoaderConfiguration::builder();
|
|
1270
|
+
|
|
1271
|
+
if let Some(cache_size) = &config.loader_cache_size {
|
|
1272
|
+
loader.cache_size(*cache_size as usize);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if let Some(seek_timeout) = &config.loader_seek_timeout {
|
|
1276
|
+
loader.seek_timeout(Duration::from_secs_f32(*seek_timeout));
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if let Some(discard_threshold) = &config.loader_discard_threshold {
|
|
1280
|
+
loader.discard_threshold(*discard_threshold);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
consumer.loader(loader.build().map_err(|e| e.to_string())?);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
815
1286
|
Ok(Self {
|
|
816
1287
|
consumer,
|
|
817
1288
|
retry: config.into(),
|
|
@@ -820,8 +1291,10 @@ impl<'a> TryFrom<&'a NativeConfiguration> for ConsumerBuilders {
|
|
|
820
1291
|
monopolization: config.into(),
|
|
821
1292
|
defer: config.into(),
|
|
822
1293
|
timeout: config.into(),
|
|
823
|
-
dedup: config.
|
|
1294
|
+
dedup: config.try_into()?,
|
|
824
1295
|
emitter: config.try_into()?,
|
|
1296
|
+
keyed_state: build_keyed_state_config(config)?,
|
|
1297
|
+
peer: build_peer_config(config)?,
|
|
825
1298
|
})
|
|
826
1299
|
}
|
|
827
1300
|
}
|