prosody 0.4.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/AGENTS.md +395 -0
  5. data/ARCHITECTURE.md +2 -2
  6. data/CHANGELOG.md +15 -0
  7. data/CLAUDE.md +1 -0
  8. data/CONFIGURATION.md +167 -0
  9. data/Cargo.lock +660 -326
  10. data/Cargo.toml +2 -1
  11. data/README.md +290 -191
  12. data/examples/keyed_state.rb +15 -3
  13. data/examples/keyed_state_windowing.rb +9 -1
  14. data/ext/prosody/Cargo.toml +2 -1
  15. data/ext/prosody/src/admin.rs +1 -5
  16. data/ext/prosody/src/bridge/mod.rs +17 -32
  17. data/ext/prosody/src/client/config.rs +194 -89
  18. data/ext/prosody/src/client/mod.rs +167 -74
  19. data/ext/prosody/src/client/request.rs +132 -0
  20. data/ext/prosody/src/client/support.rs +122 -0
  21. data/ext/prosody/src/handler/context.rs +24 -20
  22. data/ext/prosody/src/handler/message.rs +50 -0
  23. data/ext/prosody/src/handler/mod.rs +112 -84
  24. data/ext/prosody/src/handler/state/mod.rs +488 -0
  25. data/ext/prosody/src/handler/state/registration.rs +104 -0
  26. data/ext/prosody/src/handler/state/scan.rs +218 -0
  27. data/ext/prosody/src/lib.rs +15 -3
  28. data/ext/prosody/src/published.rs +273 -0
  29. data/ext/prosody/src/scheduler/mod.rs +2 -2
  30. data/ext/prosody/src/scheduler/processor.rs +2 -2
  31. data/ext/prosody/src/scheduler/result.rs +7 -4
  32. data/ext/prosody/src/util.rs +86 -5
  33. data/lib/prosody/configuration.rb +49 -15
  34. data/lib/prosody/handler.rb +63 -10
  35. data/lib/prosody/native_stubs.rb +197 -31
  36. data/lib/prosody/request.rb +45 -0
  37. data/lib/prosody/state.rb +164 -41
  38. data/lib/prosody/version.rb +1 -1
  39. data/lib/prosody.rb +1 -0
  40. data/sig/configuration.rbs +51 -15
  41. data/sig/handler.rbs +12 -4
  42. data/sig/prosody.rbs +43 -2
  43. data/sig/request.rbs +66 -0
  44. data/sig/state.rbs +165 -47
  45. data/steep_expectations.yml +10 -0
  46. data/typecheck/payload_types.rb +14 -3
  47. data/typecheck/payload_types.rbs +4 -2
  48. data/typecheck_negative/payload_types.rb +4 -0
  49. data/typecheck_negative/payload_types.rbs +1 -0
  50. metadata +12 -2
  51. data/ext/prosody/src/handler/state.rs +0 -1035
@@ -7,7 +7,8 @@
7
7
  //! builders.
8
8
 
9
9
  use magnus::{Error, Ruby, Value};
10
- use prosody::JsonCodec;
10
+ use prosody::PeerConfiguration;
11
+ use prosody::PeerEndpoint;
11
12
  use prosody::cassandra::config::CassandraConfigurationBuilder;
12
13
  use prosody::consumer::ConsumerConfigurationBuilder;
13
14
  use prosody::consumer::KeyedStateConfiguration;
@@ -29,13 +30,15 @@ use prosody::state::descriptor::{
29
30
  DequeDescriptor, MapDescriptor, StateDescriptor, deque_state, map_state, value_state,
30
31
  };
31
32
  use prosody::state::order_codec::Utf8KeyCodec;
33
+ use prosody::subsystem::SubsystemName;
32
34
  use prosody::telemetry::emitter::TelemetryEmitterConfiguration;
33
35
  use prosody::timers::duration::CompactDuration;
36
+ use prosody::{ByteSize, JsonCodec};
34
37
  use serde::{Deserialize, Deserializer};
35
38
  use serde_magnus::deserialize;
36
39
  use serde_untagged::UntaggedEnumVisitor;
37
- use std::collections::HashSet;
38
- use std::num::{NonZeroU64, NonZeroUsize};
40
+ use std::net::SocketAddr;
41
+ use std::num::NonZeroUsize;
39
42
  use std::path::PathBuf;
40
43
  use std::time::Duration;
41
44
 
@@ -117,7 +120,7 @@ pub struct NativeConfiguration {
117
120
  /// List of Cassandra contact nodes (hostnames or IPs)
118
121
  cassandra_nodes: Option<Vec<String>>,
119
122
 
120
- /// Keyspace to use for storing timer data in Cassandra
123
+ /// Keyspace used for persistent Prosody data in Cassandra.
121
124
  cassandra_keyspace: Option<String>,
122
125
 
123
126
  /// Preferred datacenter for Cassandra query routing
@@ -132,8 +135,8 @@ pub struct NativeConfiguration {
132
135
  /// Password for authenticating with Cassandra
133
136
  cassandra_password: Option<String>,
134
137
 
135
- /// Retention period for failed/unprocessed timer data in Cassandra (in
136
- /// seconds)
138
+ /// Retention period for persistent timer and deferral data in Cassandra,
139
+ /// in seconds.
137
140
  cassandra_retention: Option<f32>,
138
141
 
139
142
  /// Timer slab partitioning duration in seconds.
@@ -179,24 +182,24 @@ pub struct NativeConfiguration {
179
182
  /// Maximum delay between deferred retries (in seconds).
180
183
  defer_max_delay: Option<f32>,
181
184
 
182
- /// Failure rate threshold for enabling deferral (0.0 to 1.0).
185
+ /// Failure rate threshold for disabling deferral (0.0 to 1.0).
183
186
  defer_failure_threshold: Option<f64>,
184
187
 
185
188
  /// Sliding window duration (in seconds) for failure rate tracking.
186
189
  defer_failure_window: Option<f32>,
187
190
 
188
- /// Cache size for defer middleware.
189
- defer_cache_size: Option<u32>,
191
+ /// Maximum messages retained by the shared Kafka loader.
192
+ loader_cache_size: Option<u32>,
190
193
 
191
194
  /// Maximum number of deferred store entries kept in the write-through cache
192
195
  /// per Cassandra defer store.
193
196
  defer_store_cache_size: Option<u32>,
194
197
 
195
- /// Timeout for Kafka seek operations (in seconds).
196
- defer_seek_timeout: Option<f32>,
198
+ /// Timeout for Kafka loader seek operations (in seconds).
199
+ loader_seek_timeout: Option<f32>,
197
200
 
198
201
  /// Messages to read sequentially before seeking.
199
- defer_discard_threshold: Option<i64>,
202
+ loader_discard_threshold: Option<i64>,
200
203
 
201
204
  // Timeout configuration
202
205
  /// Fixed timeout duration for handler execution (in seconds).
@@ -216,15 +219,39 @@ pub struct NativeConfiguration {
216
219
  /// Span linking for timer execution spans (`child` or `follows_from`).
217
220
  timer_spans: Option<String>,
218
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
+
219
237
  // Keyed-state configuration
220
238
  /// Keyed-state collections to register before subscribe.
221
239
  state_collections: Option<Vec<StateCollectionConfig>>,
222
240
 
241
+ /// Subsystem under which published collections are advertised.
242
+ subsystem: Option<String>,
243
+
223
244
  /// Root directory for the local keyed-state cache. Must not be empty.
224
245
  state_cache_dir: Option<String>,
225
246
 
226
- /// Capacity of the in-memory keyed-state cache, in bytes.
227
- state_cache_size_bytes: Option<u64>,
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>,
228
255
 
229
256
  /// Delay in whole seconds before the keyed-state recovery sweep.
230
257
  ///
@@ -236,7 +263,7 @@ pub struct NativeConfiguration {
236
263
  /// Declares one keyed-state collection to register before subscribe.
237
264
  #[derive(Clone, Debug, Deserialize)]
238
265
  struct StateCollectionConfig {
239
- /// The collection name (non-empty, unique within the client).
266
+ /// The collection name. Prosody requires it to be non-empty and unique.
240
267
  name: String,
241
268
 
242
269
  /// The collection kind: `"value"`, `"map"`, or `"deque"`.
@@ -252,8 +279,11 @@ struct StateCollectionConfig {
252
279
  /// Optional opt-out of transactional staging.
253
280
  read_uncommitted: Option<bool>,
254
281
 
255
- /// Optional map-only keyset bound (`0..=4096`). Crosses as `f64` so
256
- /// fractional/negative/non-finite values reach the whole-number guard.
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.
257
287
  keyset_limit: Option<f64>,
258
288
 
259
289
  /// Optional deque-only window capacity (`>= 1`). Runtime-only and not
@@ -262,6 +292,13 @@ struct StateCollectionConfig {
262
292
  capacity: Option<f64>,
263
293
  }
264
294
 
295
+ #[derive(Clone, Debug, Deserialize)]
296
+ #[serde(untagged)]
297
+ enum ReadCacheConfig {
298
+ Disabled(bool),
299
+ Ttl(f64),
300
+ }
301
+
265
302
  /// Configuration for the health probe port.
266
303
  ///
267
304
  /// This enum represents the three possible states for the probe port
@@ -906,11 +943,42 @@ fn whole_number_field(value: f64, field: &str, min: u32, max: u32) -> Result<u32
906
943
  }
907
944
  }
908
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
+
909
976
  /// Applies the shared descriptor options (TTL, commit mode) fluently.
910
977
  fn with_def<D: StateDescriptor>(
911
978
  descriptor: D,
912
979
  ttl_seconds: Option<u32>,
913
980
  read_uncommitted: Option<bool>,
981
+ published: Option<bool>,
914
982
  ) -> D {
915
983
  let mut descriptor = descriptor;
916
984
  if let Some(ttl) = ttl_seconds {
@@ -919,6 +987,9 @@ fn with_def<D: StateDescriptor>(
919
987
  if read_uncommitted == Some(true) {
920
988
  descriptor = descriptor.read_uncommitted();
921
989
  }
990
+ if let Some(published) = published {
991
+ descriptor = descriptor.published(published);
992
+ }
922
993
  descriptor
923
994
  }
924
995
 
@@ -944,24 +1015,18 @@ fn with_capacity<T>(
944
1015
  }
945
1016
  }
946
1017
 
947
- /// Validates one collection and registers its descriptor over the closed 3×2
948
- /// (kind × payload) matrix.
1018
+ /// Maps one collection into its descriptor over the closed 3×2 (kind ×
1019
+ /// payload) matrix.
949
1020
  ///
950
1021
  /// # Errors
951
1022
  ///
952
- /// Returns a permanent-category error naming the offending field if a field is
953
- /// invalid.
1023
+ /// Returns a permanent-category error when a host value cannot be mapped into
1024
+ /// a Prosody type.
954
1025
  fn register_state_collection(
955
1026
  keyed: &mut KeyedStateConfiguration,
956
1027
  index: usize,
957
1028
  collection: &StateCollectionConfig,
958
1029
  ) -> Result<(), String> {
959
- if collection.name.is_empty() {
960
- return Err(format!(
961
- "state_collections[{index}].name: must not be empty"
962
- ));
963
- }
964
-
965
1030
  let kind = parse_kind(index, &collection.kind)?;
966
1031
  let payload = parse_payload(index, &collection.payload)?;
967
1032
 
@@ -969,46 +1034,14 @@ fn register_state_collection(
969
1034
  Some(value) => Some(whole_number_field(
970
1035
  value,
971
1036
  &format!("state_collections[{index}].ttl_seconds"),
972
- 1,
1037
+ 0,
973
1038
  u32::MAX,
974
1039
  )?),
975
1040
  None => None,
976
1041
  };
977
1042
 
978
- let keyset_limit = match collection.keyset_limit {
979
- Some(value) => {
980
- if !matches!(kind, CollectionKind::Map) {
981
- return Err(format!(
982
- "state_collections[{index}].keyset_limit: only valid for map collections"
983
- ));
984
- }
985
- Some(whole_number_field(
986
- value,
987
- &format!("state_collections[{index}].keyset_limit"),
988
- 0,
989
- 4096,
990
- )?)
991
- }
992
- None => None,
993
- };
994
-
995
- let capacity = match collection.capacity {
996
- Some(value) => {
997
- if !matches!(kind, CollectionKind::Deque) {
998
- return Err(format!(
999
- "state_collections[{index}].capacity: only valid for deque collections"
1000
- ));
1001
- }
1002
- let n = whole_number_field(
1003
- value,
1004
- &format!("state_collections[{index}].capacity"),
1005
- 1,
1006
- u32::MAX,
1007
- )?;
1008
- NonZeroUsize::new(n as usize)
1009
- }
1010
- None => None,
1011
- };
1043
+ let keyset_limit = keyset_limit(collection.keyset_limit, &kind, index)?;
1044
+ let capacity = capacity(collection.capacity, &kind, index)?;
1012
1045
 
1013
1046
  let read_uncommitted = collection.read_uncommitted;
1014
1047
  let name = collection.name.as_str();
@@ -1018,6 +1051,7 @@ fn register_state_collection(
1018
1051
  value_state::<JsonCodec>(name),
1019
1052
  ttl_seconds,
1020
1053
  read_uncommitted,
1054
+ collection.published,
1021
1055
  ));
1022
1056
  }
1023
1057
  (CollectionKind::Map, CollectionPayload::Json) => {
@@ -1025,6 +1059,7 @@ fn register_state_collection(
1025
1059
  map_state::<Utf8KeyCodec, JsonCodec>(name),
1026
1060
  ttl_seconds,
1027
1061
  read_uncommitted,
1062
+ collection.published,
1028
1063
  );
1029
1064
  let _ = keyed.register(with_keyset(descriptor, keyset_limit));
1030
1065
  }
@@ -1033,6 +1068,7 @@ fn register_state_collection(
1033
1068
  deque_state::<JsonCodec>(name),
1034
1069
  ttl_seconds,
1035
1070
  read_uncommitted,
1071
+ collection.published,
1036
1072
  );
1037
1073
  let _ = keyed.register(with_capacity(descriptor, capacity));
1038
1074
  }
@@ -1041,6 +1077,7 @@ fn register_state_collection(
1041
1077
  message_state::<KafkaLoader<JsonCodec>>(name),
1042
1078
  ttl_seconds,
1043
1079
  read_uncommitted,
1080
+ collection.published,
1044
1081
  ));
1045
1082
  }
1046
1083
  (CollectionKind::Map, CollectionPayload::Message) => {
@@ -1048,6 +1085,7 @@ fn register_state_collection(
1048
1085
  message_map_state::<Utf8KeyCodec, KafkaLoader<JsonCodec>>(name),
1049
1086
  ttl_seconds,
1050
1087
  read_uncommitted,
1088
+ collection.published,
1051
1089
  );
1052
1090
  let _ = keyed.register(with_keyset(descriptor, keyset_limit));
1053
1091
  }
@@ -1056,6 +1094,7 @@ fn register_state_collection(
1056
1094
  message_deque_state::<KafkaLoader<JsonCodec>>(name),
1057
1095
  ttl_seconds,
1058
1096
  read_uncommitted,
1097
+ collection.published,
1059
1098
  );
1060
1099
  let _ = keyed.register(with_capacity(descriptor, capacity));
1061
1100
  }
@@ -1064,46 +1103,113 @@ fn register_state_collection(
1064
1103
  Ok(())
1065
1104
  }
1066
1105
 
1067
- /// Builds the `KeyedStateConfiguration`, registering each declared collection
1068
- /// synchronously (before subscribe) and rejecting duplicate names.
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.
1069
1152
  ///
1070
1153
  /// # Errors
1071
1154
  ///
1072
- /// Returns an error if a keyed-state field is invalid or a name is duplicated.
1155
+ /// Returns an error if a host value cannot be mapped.
1073
1156
  fn build_keyed_state_config(
1074
1157
  config: &NativeConfiguration,
1075
1158
  ) -> Result<KeyedStateConfiguration, String> {
1076
1159
  let mut builder = KeyedStateConfiguration::builder();
1077
1160
 
1078
1161
  if let Some(dir) = &config.state_cache_dir {
1079
- if dir.is_empty() {
1080
- return Err("state_cache_dir: must not be an empty string".to_owned());
1081
- }
1082
1162
  builder.cache_dir(PathBuf::from(dir));
1083
1163
  }
1084
1164
 
1085
1165
  if let Some(seconds) = config.state_recovery_delay {
1086
- let seconds = whole_number_field(seconds, "state_recovery_delay", 1, u32::MAX)?;
1166
+ let seconds = whole_number_field(seconds, "state_recovery_delay", 0, u32::MAX)?;
1087
1167
  builder.recovery_delay(CompactDuration::new(seconds));
1088
1168
  }
1089
1169
 
1090
- if let Some(bytes) = config.state_cache_size_bytes {
1091
- let bytes = NonZeroU64::new(bytes)
1092
- .ok_or_else(|| "state_cache_size_bytes: must be greater than 0".to_owned())?;
1093
- builder.cache_size_bytes(Some(bytes));
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
+ ));
1094
1207
  }
1095
1208
 
1096
1209
  let mut keyed = builder.build().map_err(|error| error.to_string())?;
1097
1210
 
1098
1211
  if let Some(collections) = &config.state_collections {
1099
- let mut seen = HashSet::with_capacity(collections.len());
1100
1212
  for (index, collection) in collections.iter().enumerate() {
1101
- if !seen.insert(collection.name.as_str()) {
1102
- return Err(format!(
1103
- "state_collections[{index}].name: duplicate collection name {:?}",
1104
- collection.name
1105
- ));
1106
- }
1107
1213
  register_state_collection(&mut keyed, index, collection)?;
1108
1214
  }
1109
1215
  }
@@ -1155,24 +1261,22 @@ impl<'a> TryFrom<&'a NativeConfiguration> for ConsumerBuilders {
1155
1261
  consumer.timer_spans(relation);
1156
1262
  }
1157
1263
 
1158
- // The Kafka message loader that the defer middleware uses to reload
1159
- // failed messages is now consumer-wide configuration. Route the
1160
- // defer-loader tuning knobs onto the consumer builder's loader.
1161
- if config.defer_cache_size.is_some()
1162
- || config.defer_seek_timeout.is_some()
1163
- || config.defer_discard_threshold.is_some()
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()
1164
1268
  {
1165
1269
  let mut loader = KafkaLoaderConfiguration::builder();
1166
1270
 
1167
- if let Some(cache_size) = &config.defer_cache_size {
1271
+ if let Some(cache_size) = &config.loader_cache_size {
1168
1272
  loader.cache_size(*cache_size as usize);
1169
1273
  }
1170
1274
 
1171
- if let Some(seek_timeout) = &config.defer_seek_timeout {
1275
+ if let Some(seek_timeout) = &config.loader_seek_timeout {
1172
1276
  loader.seek_timeout(Duration::from_secs_f32(*seek_timeout));
1173
1277
  }
1174
1278
 
1175
- if let Some(discard_threshold) = &config.defer_discard_threshold {
1279
+ if let Some(discard_threshold) = &config.loader_discard_threshold {
1176
1280
  loader.discard_threshold(*discard_threshold);
1177
1281
  }
1178
1282
 
@@ -1190,6 +1294,7 @@ impl<'a> TryFrom<&'a NativeConfiguration> for ConsumerBuilders {
1190
1294
  dedup: config.try_into()?,
1191
1295
  emitter: config.try_into()?,
1192
1296
  keyed_state: build_keyed_state_config(config)?,
1297
+ peer: build_peer_config(config)?,
1193
1298
  })
1194
1299
  }
1195
1300
  }