wse-server 2.0.2__tar.gz → 2.0.7__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.7}/PKG-INFO +1 -1
  2. {wse_server-2.0.2 → wse_server-2.0.7}/pyproject.toml +1 -1
  3. {wse_server-2.0.2 → wse_server-2.0.7}/rust/Cargo.lock +1 -1
  4. {wse_server-2.0.2 → wse_server-2.0.7}/rust/Cargo.toml +1 -1
  5. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/cluster.rs +33 -17
  6. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/presence.rs +52 -18
  7. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/recovery.rs +3 -2
  8. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/server.rs +44 -11
  9. {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/__init__.py +1 -1
  10. {wse_server-2.0.2 → wse_server-2.0.7}/LICENSE +0 -0
  11. {wse_server-2.0.2 → wse_server-2.0.7}/README.md +0 -0
  12. {wse_server-2.0.2 → wse_server-2.0.7}/rust/.cargo/config.toml +0 -0
  13. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/jwt.rs +0 -0
  14. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/lib.rs +0 -0
  15. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/compression.rs +0 -0
  16. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/filters.rs +0 -0
  17. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/mod.rs +0 -0
  18. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/queue.rs +0 -0
  19. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/rate_limiter.rs +0 -0
  20. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/reliability.rs +0 -0
  21. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/security.rs +0 -0
  22. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/sequencer.rs +0 -0
  23. {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/transformer.rs +0 -0
  24. {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/_wse_accel.pyi +0 -0
  25. {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/core/__init__.py +0 -0
  26. {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/core/filters.py +0 -0
  27. {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/core/types.py +0 -0
  28. {wse_server-2.0.2 → wse_server-2.0.7}/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.7
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.7"
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.7"
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.7"
4
4
  edition = "2024"
5
5
 
6
6
  [lib]
@@ -1693,6 +1693,8 @@ pub(crate) async fn cluster_manager(
1693
1693
  // Dynamic peer tracking
1694
1694
  let known_peers: Arc<DashSet<String>> = Arc::new(DashSet::new());
1695
1695
  let connected_instances: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
1696
+ // Static peers have their own reconnect loop; never remove them from known_peers
1697
+ let static_peers: std::collections::HashSet<String> = peers.iter().cloned().collect();
1696
1698
 
1697
1699
  // Channel for peer_reader to notify about newly discovered peers
1698
1700
  let (new_peer_tx, mut new_peer_rx) = mpsc::unbounded_channel::<String>();
@@ -1946,14 +1948,15 @@ pub(crate) async fn cluster_manager(
1946
1948
  eprintln!("[WSE-Cluster] Ignoring invalid gossip address from cmd_rx: {addr}");
1947
1949
  continue;
1948
1950
  }
1949
- // Forward PEER_ANNOUNCE to all peers except the source
1950
- // Note: don't call write_framed() here -- peer_writer adds framing
1951
- let mut frame_data = BytesMut::new();
1952
- encode_peer_announce(&mut frame_data, &addr);
1953
- let frame_bytes = frame_data.freeze();
1954
- for (peer_addr, tx, _) in &peer_txs {
1955
- if *peer_addr != exclude_peer {
1956
- let _ = tx.try_send(frame_bytes.clone());
1951
+ // Forward only for newly discovered peers to prevent amplification
1952
+ if !known_peers.contains(&addr) {
1953
+ let mut frame_data = BytesMut::new();
1954
+ encode_peer_announce(&mut frame_data, &addr);
1955
+ let frame_bytes = frame_data.freeze();
1956
+ for (peer_addr, tx, _) in &peer_txs {
1957
+ if *peer_addr != exclude_peer {
1958
+ let _ = tx.try_send(frame_bytes.clone());
1959
+ }
1957
1960
  }
1958
1961
  }
1959
1962
  }
@@ -2036,15 +2039,17 @@ pub(crate) async fn cluster_manager(
2036
2039
  if is_self {
2037
2040
  continue;
2038
2041
  }
2039
- // Always forward PEER_ANNOUNCE to all peers except the source,
2040
- // even for peers we already know about. Other peers may not know yet.
2041
- // Note: don't call write_framed() here -- peer_writer adds framing
2042
- let mut frame_data = BytesMut::new();
2043
- encode_peer_announce(&mut frame_data, &addr);
2044
- let frame_bytes = frame_data.freeze();
2045
- for (peer_addr, tx, _) in &peer_txs {
2046
- if *peer_addr != exclude_peer {
2047
- let _ = tx.try_send(frame_bytes.clone());
2042
+ // Forward PEER_ANNOUNCE only for newly discovered peers
2043
+ // to prevent gossip amplification in dense clusters
2044
+ let is_new = !known_peers.contains(&addr);
2045
+ if is_new {
2046
+ let mut frame_data = BytesMut::new();
2047
+ encode_peer_announce(&mut frame_data, &addr);
2048
+ let frame_bytes = frame_data.freeze();
2049
+ for (peer_addr, tx, _) in &peer_txs {
2050
+ if *peer_addr != exclude_peer {
2051
+ let _ = tx.try_send(frame_bytes.clone());
2052
+ }
2048
2053
  }
2049
2054
  }
2050
2055
  // Only spawn connection to this peer if we don't already know it
@@ -2082,6 +2087,10 @@ pub(crate) async fn cluster_manager(
2082
2087
  }
2083
2088
  Some(InterestUpdate::PeerDisconnected { peer_addr }) => {
2084
2089
  remote_interest.remove(&peer_addr);
2090
+ // Allow gossip-discovered peers to be re-discovered
2091
+ if !static_peers.contains(&peer_addr) {
2092
+ known_peers.remove(&peer_addr);
2093
+ }
2085
2094
  }
2086
2095
  None => {
2087
2096
  // Interest channel closed, continue running
@@ -2101,6 +2110,10 @@ pub(crate) async fn cluster_manager(
2101
2110
  PeerRegistration::Deregister { peer_addr } => {
2102
2111
  // Remove disconnected inbound peer
2103
2112
  peer_txs.retain(|(a, _, _)| a != &peer_addr);
2113
+ // Allow gossip-discovered peers to be re-discovered
2114
+ if !static_peers.contains(&peer_addr) {
2115
+ known_peers.remove(&peer_addr);
2116
+ }
2104
2117
  }
2105
2118
  }
2106
2119
  }
@@ -2119,6 +2132,7 @@ pub(crate) async fn cluster_manager(
2119
2132
  let _ = tx.send(super::server::ServerCommand::BroadcastLocal {
2120
2133
  topic,
2121
2134
  data: msg,
2135
+ skip_recovery: true,
2122
2136
  });
2123
2137
  }
2124
2138
  }
@@ -2130,6 +2144,7 @@ pub(crate) async fn cluster_manager(
2130
2144
  let _ = tx.send(super::server::ServerCommand::BroadcastLocal {
2131
2145
  topic,
2132
2146
  data: msg,
2147
+ skip_recovery: true,
2133
2148
  });
2134
2149
  }
2135
2150
  }
@@ -2141,6 +2156,7 @@ pub(crate) async fn cluster_manager(
2141
2156
  let _ = tx.send(super::server::ServerCommand::BroadcastLocal {
2142
2157
  topic,
2143
2158
  data: msg,
2159
+ skip_recovery: true,
2144
2160
  });
2145
2161
  }
2146
2162
  }
@@ -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.7"
30
30
 
31
31
  __all__ = [
32
32
  "RustWSEServer",
File without changes
File without changes
File without changes
File without changes