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.
- {wse_server-2.0.2 → wse_server-2.0.6}/PKG-INFO +1 -1
- {wse_server-2.0.2 → wse_server-2.0.6}/pyproject.toml +1 -1
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/Cargo.lock +1 -1
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/Cargo.toml +1 -1
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/cluster.rs +3 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/presence.rs +52 -18
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/recovery.rs +3 -2
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/server.rs +44 -11
- {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/__init__.py +1 -1
- {wse_server-2.0.2 → wse_server-2.0.6}/LICENSE +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/README.md +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/.cargo/config.toml +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/jwt.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/lib.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/compression.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/filters.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/mod.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/queue.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/rate_limiter.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/reliability.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/security.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/sequencer.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/rust/src/wse/transformer.rs +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/_wse_accel.pyi +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/core/__init__.py +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/core/filters.py +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/core/types.py +0 -0
- {wse_server-2.0.2 → wse_server-2.0.6}/wse_server/py.typed +0 -0
|
@@ -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
|
|
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
|