wse-server 2.0.2__tar.gz → 2.0.6__tar.gz

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 (28) hide show
  1. {wse_server-2.0.2 → wse_server-2.0.6}/PKG-INFO +1 -1
  2. {wse_server-2.0.2 → wse_server-2.0.6}/pyproject.toml +1 -1
  3. {wse_server-2.0.2 → wse_server-2.0.6}/rust/Cargo.lock +1 -1
  4. {wse_server-2.0.2 → wse_server-2.0.6}/rust/Cargo.toml +1 -1
  5. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/cluster.rs +3 -0
  6. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/presence.rs +52 -18
  7. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/recovery.rs +3 -2
  8. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/server.rs +44 -11
  9. {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/__init__.py +1 -1
  10. {wse_server-2.0.2 → wse_server-2.0.6}/LICENSE +0 -0
  11. {wse_server-2.0.2 → wse_server-2.0.6}/README.md +0 -0
  12. {wse_server-2.0.2 → wse_server-2.0.6}/rust/.cargo/config.toml +0 -0
  13. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/jwt.rs +0 -0
  14. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/lib.rs +0 -0
  15. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/compression.rs +0 -0
  16. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/filters.rs +0 -0
  17. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/mod.rs +0 -0
  18. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/queue.rs +0 -0
  19. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/rate_limiter.rs +0 -0
  20. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/reliability.rs +0 -0
  21. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/security.rs +0 -0
  22. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/sequencer.rs +0 -0
  23. {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/transformer.rs +0 -0
  24. {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/_wse_accel.pyi +0 -0
  25. {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/core/__init__.py +0 -0
  26. {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/core/filters.py +0 -0
  27. {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/core/types.py +0 -0
  28. {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wse-server
3
- Version: 2.0.2
3
+ Version: 2.0.6
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "wse-server"
7
- version = "2.0.2"
7
+ version = "2.0.6"
8
8
  description = "Rust-powered WebSocket engine for Python. Up to 5M fan-out del/s. Zero-GIL JWT."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1912,7 +1912,7 @@ dependencies = [
1912
1912
 
1913
1913
  [[package]]
1914
1914
  name = "wse-accel"
1915
- version = "2.0.2"
1915
+ version = "2.0.6"
1916
1916
  dependencies = [
1917
1917
  "aes-gcm",
1918
1918
  "ahash",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "wse-accel"
3
- version = "2.0.2"
3
+ version = "2.0.6"
4
4
  edition = "2024"
5
5
 
6
6
  [lib]
@@ -2119,6 +2119,7 @@ pub(crate) async fn cluster_manager(
2119
2119
  let _ = tx.send(super::server::ServerCommand::BroadcastLocal {
2120
2120
  topic,
2121
2121
  data: msg,
2122
+ skip_recovery: true,
2122
2123
  });
2123
2124
  }
2124
2125
  }
@@ -2130,6 +2131,7 @@ pub(crate) async fn cluster_manager(
2130
2131
  let _ = tx.send(super::server::ServerCommand::BroadcastLocal {
2131
2132
  topic,
2132
2133
  data: msg,
2134
+ skip_recovery: true,
2133
2135
  });
2134
2136
  }
2135
2137
  }
@@ -2141,6 +2143,7 @@ pub(crate) async fn cluster_manager(
2141
2143
  let _ = tx.send(super::server::ServerCommand::BroadcastLocal {
2142
2144
  topic,
2143
2145
  data: msg,
2146
+ skip_recovery: true,
2144
2147
  });
2145
2148
  }
2146
2149
  }
@@ -142,7 +142,7 @@ impl PresenceManager {
142
142
  return TrackResult::AtCapacity;
143
143
  }
144
144
 
145
- let (is_first, actually_inserted) = {
145
+ let (is_first, actually_inserted, has_remote_sentinel) = {
146
146
  let mut entry = topic_map
147
147
  .entry(user_id.clone())
148
148
  .or_insert_with(|| PresenceEntry {
@@ -151,7 +151,12 @@ impl PresenceManager {
151
151
  connections: Vec::new(),
152
152
  updated_at: now,
153
153
  });
154
- // Check for first LOCAL connection (ignore remote sentinels from cluster sync)
154
+ // Check for remote sentinel (from cluster sync) while holding the shard lock
155
+ let has_remote = entry
156
+ .connections
157
+ .iter()
158
+ .any(|c| c.conn_id.starts_with("__remote__"));
159
+ // Check for first LOCAL connection (ignore remote sentinels)
155
160
  let was_empty = !entry
156
161
  .connections
157
162
  .iter()
@@ -168,7 +173,7 @@ impl PresenceManager {
168
173
  };
169
174
  entry.data = data.clone();
170
175
  entry.updated_at = now;
171
- (was_empty, inserted)
176
+ (was_empty, inserted, has_remote)
172
177
  };
173
178
 
174
179
  // Track which topics this conn has presence in
@@ -187,7 +192,11 @@ impl PresenceManager {
187
192
  }
188
193
 
189
194
  if is_first {
190
- stats.num_users.fetch_add(1, Ordering::Relaxed);
195
+ // Only increment num_users if no remote sentinel exists
196
+ // (merge_remote_join already counted this user)
197
+ if !has_remote_sentinel {
198
+ stats.num_users.fetch_add(1, Ordering::Relaxed);
199
+ }
191
200
  TrackResult::FirstJoin {
192
201
  user_id,
193
202
  data: data.clone(),
@@ -381,8 +390,12 @@ impl PresenceManager {
381
390
  }
382
391
 
383
392
  for (user_id, data, _dead_conn_count) in dead_users {
384
- // Collect dead conn_ids before removing the user entry
385
- let actual_conn_count = if let Some((_, entry)) = topic_map.remove(&user_id) {
393
+ // Use remove_if to guard against concurrent track() adding a new conn
394
+ let actual_conn_count = if let Some((_, entry)) =
395
+ topic_map.remove_if(&user_id, |_, e| {
396
+ // Only remove if all connections are still dead
397
+ e.connections.iter().all(|c| !is_conn_alive(&c.conn_id))
398
+ }) {
386
399
  let count = entry.connections.len();
387
400
  for conn in &entry.connections {
388
401
  self.conn_user_id.remove(&conn.conn_id);
@@ -390,21 +403,42 @@ impl PresenceManager {
390
403
  }
391
404
  count
392
405
  } else {
406
+ // Entry was either already removed or a live conn was added - skip
407
+ // But still clean up any dead local connections from the entry
408
+ if let Some(mut entry) = topic_map.get_mut(&user_id) {
409
+ let before = entry.connections.len();
410
+ entry.connections.retain(|c| {
411
+ c.conn_id.starts_with("__remote__") || is_conn_alive(&c.conn_id)
412
+ });
413
+ let removed_count = before - entry.connections.len();
414
+ if removed_count > 0
415
+ && let Some(stats) = self.topic_presence_stats.get(&topic)
416
+ {
417
+ let _ = stats.num_connections.fetch_update(
418
+ Ordering::Relaxed,
419
+ Ordering::Relaxed,
420
+ |current| Some(current.saturating_sub(removed_count)),
421
+ );
422
+ }
423
+ }
393
424
  0
394
425
  };
395
- if let Some(stats) = self.topic_presence_stats.get(&topic) {
396
- let _ = stats.num_users.fetch_update(
397
- Ordering::Relaxed,
398
- Ordering::Relaxed,
399
- |current| Some(current.saturating_sub(1)),
400
- );
401
- let _ = stats.num_connections.fetch_update(
402
- Ordering::Relaxed,
403
- Ordering::Relaxed,
404
- |current| Some(current.saturating_sub(actual_conn_count)),
405
- );
426
+ // Only decrement stats and emit leave if the entry was actually removed
427
+ if actual_conn_count > 0 {
428
+ if let Some(stats) = self.topic_presence_stats.get(&topic) {
429
+ let _ = stats.num_users.fetch_update(
430
+ Ordering::Relaxed,
431
+ Ordering::Relaxed,
432
+ |current| Some(current.saturating_sub(1)),
433
+ );
434
+ let _ = stats.num_connections.fetch_update(
435
+ Ordering::Relaxed,
436
+ Ordering::Relaxed,
437
+ |current| Some(current.saturating_sub(actual_conn_count)),
438
+ );
439
+ }
440
+ removed.push((topic.clone(), user_id, data));
406
441
  }
407
- removed.push((topic.clone(), user_id, data));
408
442
  }
409
443
  }
410
444
 
@@ -164,9 +164,10 @@ impl TopicRecoveryBuffer {
164
164
  {
165
165
  result.push(entry.data.clone());
166
166
  } else {
167
- // Unreachable: the tail guard ensures every slot in [start, head_offset)
168
- // was written after tail advanced past any previous occupant.
167
+ // Slot mismatch should be unreachable, but if it occurs in release,
168
+ // signal failure so the client re-subscribes rather than missing messages.
169
169
  debug_assert!(false, "slot {} mismatch: expected offset {}", idx, offset);
170
+ return None;
170
171
  }
171
172
  }
172
173
 
@@ -170,14 +170,35 @@ const RATE_CAPACITY: f64 = 100_000.0;
170
170
  const RATE_REFILL: f64 = 10_000.0; // tokens per second
171
171
 
172
172
  pub(crate) enum ServerCommand {
173
- SendText { conn_id: String, data: String },
174
- SendBytes { conn_id: String, data: Vec<u8> },
175
- SendPrebuilt { conn_id: String, message: Message },
176
- BroadcastText { data: String },
177
- BroadcastBytes { data: Vec<u8> },
178
- BroadcastLocal { topic: String, data: String },
179
- Disconnect { conn_id: String },
180
- GetConnections { reply: oneshot::Sender<Vec<String>> },
173
+ SendText {
174
+ conn_id: String,
175
+ data: String,
176
+ },
177
+ SendBytes {
178
+ conn_id: String,
179
+ data: Vec<u8>,
180
+ },
181
+ SendPrebuilt {
182
+ conn_id: String,
183
+ message: Message,
184
+ },
185
+ BroadcastText {
186
+ data: String,
187
+ },
188
+ BroadcastBytes {
189
+ data: Vec<u8>,
190
+ },
191
+ BroadcastLocal {
192
+ topic: String,
193
+ data: String,
194
+ skip_recovery: bool,
195
+ },
196
+ Disconnect {
197
+ conn_id: String,
198
+ },
199
+ GetConnections {
200
+ reply: oneshot::Sender<Vec<String>>,
201
+ },
181
202
  Shutdown,
182
203
  }
183
204
 
@@ -1321,6 +1342,7 @@ async fn handle_connection(stream: TcpStream, addr: SocketAddr, state: Arc<Share
1321
1342
  let _ = tx.send(ServerCommand::BroadcastLocal {
1322
1343
  topic: topic.clone(),
1323
1344
  data: msg,
1345
+ skip_recovery: true,
1324
1346
  });
1325
1347
  }
1326
1348
  // Cluster sync: notify peers of presence leave
@@ -1605,10 +1627,14 @@ async fn process_commands(
1605
1627
  }
1606
1628
  }
1607
1629
  }
1608
- ServerCommand::BroadcastLocal { topic, data } => {
1630
+ ServerCommand::BroadcastLocal {
1631
+ topic,
1632
+ data,
1633
+ skip_recovery,
1634
+ } => {
1609
1635
  let preframed = pre_frame_text(&data);
1610
- // Store in recovery buffer (zero-copy: Bytes is Arc-refcounted)
1611
- if let Some(ref recovery) = state.recovery {
1636
+ // Store in recovery buffer (skip for presence events)
1637
+ if !skip_recovery && let Some(ref recovery) = state.recovery {
1612
1638
  recovery.push(&topic, preframed.clone());
1613
1639
  }
1614
1640
  if state.conn_encryption.is_empty() {
@@ -1921,6 +1947,7 @@ impl RustWSEServer {
1921
1947
  ServerCommand::BroadcastLocal {
1922
1948
  topic: topic.clone(),
1923
1949
  data: msg,
1950
+ skip_recovery: true,
1924
1951
  },
1925
1952
  );
1926
1953
  }
@@ -2029,6 +2056,7 @@ impl RustWSEServer {
2029
2056
  let _ = tx.send(ServerCommand::BroadcastLocal {
2030
2057
  topic: topic.clone(),
2031
2058
  data: msg,
2059
+ skip_recovery: true,
2032
2060
  });
2033
2061
  }
2034
2062
  if let Some(ref ctx) = cluster_tx {
@@ -2934,6 +2962,7 @@ impl RustWSEServer {
2934
2962
  let _ = tx.send(ServerCommand::BroadcastLocal {
2935
2963
  topic: topic.clone(),
2936
2964
  data: msg,
2965
+ skip_recovery: true,
2937
2966
  });
2938
2967
  }
2939
2968
  // Cluster sync: notify peers of presence join
@@ -3161,6 +3190,7 @@ impl RustWSEServer {
3161
3190
  let _ = tx.send(ServerCommand::BroadcastLocal {
3162
3191
  topic: topic.clone(),
3163
3192
  data: msg,
3193
+ skip_recovery: true,
3164
3194
  });
3165
3195
  }
3166
3196
  // Cluster sync: notify peers of presence leave
@@ -3206,6 +3236,7 @@ impl RustWSEServer {
3206
3236
  tx.send(ServerCommand::BroadcastLocal {
3207
3237
  topic: topic.to_owned(),
3208
3238
  data: data.to_owned(),
3239
+ skip_recovery: false,
3209
3240
  })
3210
3241
  .map_err(|_| PyRuntimeError::new_err("Channel closed"))?;
3211
3242
  Ok(())
@@ -3218,6 +3249,7 @@ impl RustWSEServer {
3218
3249
  let _ = tx.send(ServerCommand::BroadcastLocal {
3219
3250
  topic: topic.to_owned(),
3220
3251
  data: data.to_owned(),
3252
+ skip_recovery: false,
3221
3253
  });
3222
3254
  }
3223
3255
 
@@ -3393,6 +3425,7 @@ impl RustWSEServer {
3393
3425
  let _ = tx.send(ServerCommand::BroadcastLocal {
3394
3426
  topic: topic.clone(),
3395
3427
  data: msg.clone(),
3428
+ skip_recovery: true,
3396
3429
  });
3397
3430
  }
3398
3431
  }
@@ -26,7 +26,7 @@ from wse_server._wse_accel import (
26
26
  from wse_server.core.filters import EventFilter
27
27
  from wse_server.core.types import DeliveryGuarantee, EventPriority
28
28
 
29
- __version__ = "2.0.2"
29
+ __version__ = "2.0.6"
30
30
 
31
31
  __all__ = [
32
32
  "RustWSEServer",
File without changes
File without changes
File without changes
File without changes