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.
- {wse_server-2.0.2 → wse_server-2.0.7}/PKG-INFO +1 -1
- {wse_server-2.0.2 → wse_server-2.0.7}/pyproject.toml +1 -1
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/Cargo.lock +1 -1
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/Cargo.toml +1 -1
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/cluster.rs +33 -17
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/presence.rs +52 -18
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/recovery.rs +3 -2
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/server.rs +44 -11
- {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/__init__.py +1 -1
- {wse_server-2.0.2 → wse_server-2.0.7}/LICENSE +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/README.md +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/.cargo/config.toml +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/jwt.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/lib.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/compression.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/filters.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/mod.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/queue.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/rate_limiter.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/reliability.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/security.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/sequencer.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/rust/src/wse/transformer.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/_wse_accel.pyi +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/core/__init__.py +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/core/filters.py +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/core/types.py +0 -0
- {wse_server-2.0.2 → wse_server-2.0.7}/wse_server/py.typed +0 -0
|
@@ -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
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
-
//
|
|
2040
|
-
//
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
385
|
-
let actual_conn_count = if let Some((_, entry)) =
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
//
|
|
168
|
-
//
|
|
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 {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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 {
|
|
1630
|
+
ServerCommand::BroadcastLocal {
|
|
1631
|
+
topic,
|
|
1632
|
+
data,
|
|
1633
|
+
skip_recovery,
|
|
1634
|
+
} => {
|
|
1609
1635
|
let preframed = pre_frame_text(&data);
|
|
1610
|
-
// Store in recovery buffer (
|
|
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
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|