feat(event): add the actor, socket and internal rpc foundation
- extract the Redis actor runtime into `lib/actor` (directory leases, node inbox streams, push backlog) and stop it from depending on event/command types; Redis connections now go through `lib/cache` instead of a local pool - extract the Engine.IO v4 / Socket.IO subset server into `lib/socket` and invert its dependencies: auth, subscription and delivery are provided by a `SocketBackend` trait so the protocol layer stays free of business rules - add `lib/internal`: `x-rpc-key` gate, encrypted identity envelope (RSA-OAEP + AES-GCM with an HMAC over expiry/nonce/audience), nonce replay store, shared router with gRPC health/reflection, and an internal client - add `internal.v1` protos under `lib/proto/proto/internal/v1` plus the standard `grpc.health.v1` / `grpc.reflection.v1` definitions used by the shared router - add the `event` configuration section (socket/actor/redis/consumer/rpc/ platform) and `SecretMaterial` for file/env/inline key material; the internal section grows server and event-client settings - keep local dev key files out of git: the default config carries empty key placeholders, the gitignored `config/local` profile points at local PEMs
ZhenYi committed
Added config/default/event.json config/default/event.json +63 −0 View config/default/event.json
@@ -0,0 +1,63 @@
1{
2 "socket": {
3 "listen_address": "0.0.0.0:7031",
4 "heartbeat_interval_secs": 30,
5 "heartbeat_timeout_secs": 30,
6 "auth_timeout_secs": 5,
7 "max_payload": 1000000,
8 "session_cookie": "session"
9 },
10 "actor": {
11 "node_id": null,
12 "key_prefix": "event:actor",
13 "call_timeout_secs": 5,
14 "idle_timeout_secs": 60,
15 "lease_ttl_secs": 15,
16 "mailbox_capacity": 1024,
17 "inbox_max_len": 10000,
18 "read_count": 64,
19 "read_block_ms": 1000
20 },
21 "redis": {
22 "url": "redis://127.0.0.1:6379",
23 "mode": "standalone",
24 "database": 0,
25 "pool_size": 8,
26 "connection_timeout_secs": 2,
27 "username": null,
28 "password": null,
29 "key_prefix": "event",
30 "default_ttl_secs": 300
31 },
32 "consumer": {
33 "name_server": "127.0.0.1:9876",
34 "consumer_group": "gitdata-event",
35 "topic_prefixes": [
36 "gitdata",
37 "kenshell"
38 ],
39 "consume_timeout_ms": 900000,
40 "max_reconsume_times": 16,
41 "pull_batch_size": 32,
42 "pull_batch_size_in_bytes": 262144,
43 "pull_interval_ms": 0
44 },
45 "rpc": {
46 "listen_address": "127.0.0.1:7032",
47 "rpc_key": "dev-event-internal-rpc-key-change-in-production",
48 "identity_key_id": "event-dev-1",
49 "identity_private_key": "",
50 "platform_mac_key_id": "event-mac-dev-1",
51 "platform_mac_key": "",
52 "nonce_ttl_secs": 120,
53 "clock_skew_secs": 30,
54 "call_timeout_secs": 5
55 },
56 "platform": {
57 "rpc_endpoint": "http://127.0.0.1:7052",
58 "rpc_key": "dev-event-internal-rpc-key-change-in-production",
59 "timeout_secs": 5,
60 "resource_api_endpoint": "http://127.0.0.1:7051/api/v1",
61 "resource_api_service_key": ""
62 }
63}
Modified config/default/internal.json config/default/internal.json +16 −1 View config/default/internal.json
@@ -1,3 +1,18 @@
11{
2 "service_key": ""
2 "service_key": "",
3 "rpc": {
4 "listen_address": "127.0.0.1:7052",
5 "rpc_key": "dev-event-internal-rpc-key-change-in-production"
6 },
7 "event_client": {
8 "endpoint": "http://127.0.0.1:7032",
9 "rpc_key": "dev-event-internal-rpc-key-change-in-production",
10 "timeout_secs": 5,
11 "key_id": "event-dev-1",
12 "public_key": "",
13 "mac_key_id": "event-mac-dev-1",
14 "mac_key": "",
15 "clock_skew_secs": 30,
16 "envelope_ttl_secs": 60
17 }
318}
Added lib/actor/src/actor/host/activation.rs lib/actor/src/actor/host/activation.rs +202 −0 View lib/actor/src/actor/host/activation.rs
@@ -0,0 +1,202 @@
1use std::time::Duration;
2
3use tokio::sync::mpsc;
4use tokio::time::{MissedTickBehavior, interval};
5
6use super::super::actor::{ActorContext, ErasedActor};
7use super::super::error::ActorError;
8use super::super::id::ActorId;
9use super::super::transport::WireMessage;
10use super::core::{ActorHost, LocalRequest, ReplyTarget};
11
12impl ActorHost {
13 pub(super) async fn run_activation(
14 self,
15 id: ActorId,
16 mut actor: Box<dyn ErasedActor>,
17 sender: mpsc::Sender<LocalRequest>,
18 mut receiver: mpsc::Receiver<LocalRequest>,
19 ) {
20 let ctx = ActorContext::new(id, self.clone());
21 let mut shutdown = self.inner.shutdown.subscribe();
22
23 if *shutdown.borrow() {
24 self.finish_activation(id, &sender, &mut receiver, true)
25 .await;
26 return;
27 }
28
29 let mut active_entry = None;
30 let mut activated = false;
31 let lease_period = (self.inner.options.lease_ttl / 3).max(Duration::from_millis(1));
32 let renew = async {
33 let mut tick = interval(lease_period);
34 tick.set_missed_tick_behavior(MissedTickBehavior::Delay);
35 loop {
36 tick.tick().await;
37 match tokio::time::timeout(
38 lease_period,
39 self.inner.directory.renew(id, self.node_id()),
40 )
41 .await
42 {
43 Ok(Ok(true)) => {}
44 _ => return ActorError::DirectoryUnavailable,
45 }
46 }
47 };
48 let work = async {
49 actor.activate(&ctx).await?;
50 activated = true;
51 loop {
52 let request = match self.inner.options.idle_timeout {
53 Some(idle) => match tokio::time::timeout(idle, receiver.recv()).await {
54 Ok(request) => request,
55 Err(_) => break,
56 },
57 None => receiver.recv().await,
58 };
59 let Some(request) = request else { break };
60 active_entry = match &request {
61 LocalRequest::Message { entry, .. } => entry.clone(),
62 LocalRequest::Heartbeat => None,
63 };
64 self.handle_request(&mut *actor, &ctx, request).await;
65 active_entry = None;
66 }
67 Ok::<(), ActorError>(())
68 };
69 let result = tokio::select! {
70 biased;
71 _ = shutdown.changed() => Err(ActorError::ActivationClosed),
72 error = renew => Err(error),
73 result = work => result,
74 };
75 if let Some(entry) = active_entry {
76 self.inner.transport.inflight.lock().await.remove(&entry);
77 }
78 let _ = tokio::time::timeout(lease_period, actor.deactivate(&ctx)).await;
79 let shutting_down = *shutdown.borrow();
80 match result {
81 Err(error) if !activated => {
82 self.fail_activation(id, &sender, &mut receiver, error.to_string())
83 .await
84 }
85 _ => {
86 self.finish_activation(id, &sender, &mut receiver, shutting_down)
87 .await
88 }
89 }
90 }
91
92 async fn handle_request(
93 &self,
94 actor: &mut dyn ErasedActor,
95 ctx: &ActorContext,
96 request: LocalRequest,
97 ) {
98 let LocalRequest::Message {
99 payload,
100 reply,
101 entry,
102 } = request
103 else {
104 return;
105 };
106
107 let result = actor.receive(ctx, payload).await;
108
109 if let Some(entry) = entry.as_deref() {
110 let _ = self.inner.transport.ack(entry).await;
111 }
112
113 match reply {
114 Some(ReplyTarget::Local(reply)) => {
115 let _ = reply.send(result);
116 }
117 Some(ReplyTarget::Remote { stream, id }) => {
118 let wire = WireMessage::Response {
119 id,
120 result: result.map_err(|error| error.to_string()),
121 };
122 let _ = self.inner.transport.send(&stream, &wire).await;
123 }
124 None => {}
125 }
126 }
127
128 async fn finish_activation(
129 &self,
130 id: ActorId,
131 sender: &mpsc::Sender<LocalRequest>,
132 receiver: &mut mpsc::Receiver<LocalRequest>,
133 shutting_down: bool,
134 ) {
135 receiver.close();
136 let mut activations = self.inner.activations.lock().await;
137 if activations
138 .get(&id)
139 .is_some_and(|current| current.same_channel(sender))
140 {
141 let _ = self.inner.directory.release(id, self.node_id()).await;
142 activations.remove(&id);
143 }
144 drop(activations);
145
146 while let Ok(request) = receiver.try_recv() {
147 if shutting_down {
148 self.reply_error(request, ActorError::ActivationClosed.to_string())
149 .await;
150 } else {
151 let _ = self.dispatch_request(id, request).await;
152 }
153 }
154 }
155
156 async fn fail_activation(
157 &self,
158 id: ActorId,
159 sender: &mpsc::Sender<LocalRequest>,
160 receiver: &mut mpsc::Receiver<LocalRequest>,
161 error: String,
162 ) {
163 receiver.close();
164 let mut activations = self.inner.activations.lock().await;
165 if activations
166 .get(&id)
167 .is_some_and(|current| current.same_channel(sender))
168 {
169 let _ = self.inner.directory.release(id, self.node_id()).await;
170 activations.remove(&id);
171 }
172 drop(activations);
173
174 while let Ok(request) = receiver.try_recv() {
175 self.reply_error(request, error.clone()).await;
176 }
177 }
178
179 async fn reply_error(&self, request: LocalRequest, message: String) {
180 let LocalRequest::Message { reply, entry, .. } = request else {
181 return;
182 };
183
184 if let Some(entry) = entry.as_deref() {
185 let _ = self.inner.transport.ack(entry).await;
186 }
187
188 match reply {
189 Some(ReplyTarget::Local(reply)) => {
190 let _ = reply.send(Err(ActorError::Remote(message)));
191 }
192 Some(ReplyTarget::Remote { stream, id }) => {
193 let wire = WireMessage::Response {
194 id,
195 result: Err(message),
196 };
197 let _ = self.inner.transport.send(&stream, &wire).await;
198 }
199 None => {}
200 }
201 }
202}
Added lib/actor/src/actor/host/builder.rs lib/actor/src/actor/host/builder.rs +94 −0 View lib/actor/src/actor/host/builder.rs
@@ -0,0 +1,94 @@
1use std::collections::HashMap;
2use std::sync::Arc;
3
4use tokio::sync::{Mutex, mpsc, watch};
5
6use config::redis::RedisConfig;
7
8use super::super::actor::{ActorAdapter, ErasedActor, VirtualActor};
9use super::super::directory::RedisDirectory;
10use super::super::error::ActorError;
11use super::super::id::ActorId;
12use super::super::redis_conn;
13use super::super::transport::RedisTransport;
14use super::core::{ActorFactory, ActorHost, Inner};
15use super::node;
16use super::options::ActorHostOptions;
17
18pub struct ActorHostBuilder {
19 pub(super) options: ActorHostOptions,
20 pub(super) redis: RedisConfig,
21 pub(super) factory: Option<ActorFactory>,
22}
23
24impl ActorHostBuilder {
25 pub(super) fn new(options: ActorHostOptions, redis: RedisConfig) -> Self {
26 Self {
27 options,
28 redis,
29 factory: None,
30 }
31 }
32
33 pub fn register<A>(mut self, factory: impl Fn(ActorId) -> A + Send + Sync + 'static) -> Self
34 where
35 A: VirtualActor,
36 {
37 self.factory = Some(Arc::new(move |id| {
38 Box::new(ActorAdapter::new(factory(id))) as Box<dyn ErasedActor>
39 }));
40 self
41 }
42
43 pub async fn start(self) -> Result<ActorHost, ActorError> {
44 let mut options = self.options;
45 if options.node_id.is_empty() {
46 options.node_id = node::load_or_create().map_err(ActorError::Io)?;
47 }
48 options.validate().map_err(ActorError::InvalidOptions)?;
49 self.redis.validate().map_err(ActorError::InvalidOptions)?;
50 let factory = self
51 .factory
52 .ok_or_else(|| ActorError::InvalidOptions("no actor type registered".to_string()))?;
53
54 let (commands, reads) = redis_conn::connect(&self.redis).await?;
55 let directory = RedisDirectory::new(
56 commands.clone(),
57 options.key_prefix.clone(),
58 options.lease_ttl,
59 );
60 let transport = RedisTransport::new(
61 commands,
62 reads,
63 options.key_prefix.clone(),
64 options.node_id.clone(),
65 options.inbox_max_len,
66 options.read_count,
67 options.read_block_ms,
68 );
69 transport.ensure_group().await?;
70
71 let (shutdown, _) = watch::channel(false);
72 let (events, event_receiver) = mpsc::channel(options.mailbox_capacity);
73 let inner = Arc::new(Inner {
74 options,
75 directory,
76 transport,
77 factory,
78 activations: Mutex::new(HashMap::new()),
79 shutdown,
80 reader: Mutex::new(None),
81 workers: Mutex::new(Vec::new()),
82 events,
83 event_receiver: Mutex::new(Some(event_receiver)),
84 });
85 let host = ActorHost { inner };
86 let reader = host.clone();
87 let handle = tokio::spawn(async move {
88 reader.run_reader().await;
89 });
90 *host.inner.reader.lock().await = Some(handle);
91
92 Ok(host)
93 }
94}
Added lib/actor/src/actor/host/core.rs lib/actor/src/actor/host/core.rs +193 −0 View lib/actor/src/actor/host/core.rs
@@ -0,0 +1,193 @@
1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use serde_json::Value;
6use tokio::sync::{Mutex, mpsc, oneshot, watch};
7use tokio::task::JoinHandle;
8
9use config::event::redis::RedisConfig;
10
11use super::super::actor::{ActorRef, ErasedActor, VirtualActor};
12use super::super::directory::RedisDirectory;
13use super::super::error::ActorError;
14use super::super::id::ActorId;
15use super::super::transport::RedisTransport;
16use super::builder::ActorHostBuilder;
17use super::options::ActorHostOptions;
18
19pub(super) type ActorFactory = Arc<dyn Fn(ActorId) -> Box<dyn ErasedActor> + Send + Sync>;
20
21pub(super) struct Inner {
22 pub(super) options: ActorHostOptions,
23 pub(super) directory: RedisDirectory,
24 pub(super) transport: RedisTransport,
25 pub(super) factory: ActorFactory,
26 pub(super) activations: Mutex<HashMap<ActorId, mpsc::Sender<LocalRequest>>>,
27 pub(super) shutdown: watch::Sender<bool>,
28 pub(super) reader: Mutex<Option<JoinHandle<()>>>,
29 pub(super) workers: Mutex<Vec<JoinHandle<()>>>,
30 pub(super) events: mpsc::Sender<ActorEvent>,
31 pub(super) event_receiver: Mutex<Option<mpsc::Receiver<ActorEvent>>>,
32}
33
34pub struct ActorEvent {
35 pub user_id: i64,
36 pub payload: Value,
37 pub(super) entry: String,
38}
39
40impl ActorEvent {
41 pub fn entry_id(&self) -> &str {
42 &self.entry
43 }
44}
45
46pub(super) enum LocalRequest {
47 Message {
48 payload: Value,
49 reply: Option<ReplyTarget>,
50 entry: Option<String>,
51 },
52 Heartbeat,
53}
54
55pub(super) enum ReplyTarget {
56 Local(oneshot::Sender<Result<Value, ActorError>>),
57 Remote { stream: String, id: String },
58}
59
60#[derive(Clone)]
61pub struct ActorHost {
62 pub(super) inner: Arc<Inner>,
63}
64
65impl ActorHost {
66 pub fn builder(options: ActorHostOptions, redis: RedisConfig) -> ActorHostBuilder {
67 ActorHostBuilder::new(options, redis)
68 }
69
70 pub fn node_id(&self) -> &str {
71 &self.inner.options.node_id
72 }
73
74 pub fn idle_timeout(&self) -> Option<Duration> {
75 self.inner.options.idle_timeout
76 }
77
78 pub fn actor<A: VirtualActor>(&self, id: impl Into<ActorId>) -> ActorRef<A> {
79 ActorRef::new(self.clone(), id.into())
80 }
81
82 pub async fn heartbeat(&self, id: impl Into<ActorId>) -> Result<(), ActorError> {
83 self.dispatch_request(id.into(), LocalRequest::Heartbeat)
84 .await
85 }
86
87 pub async fn shutdown(&self) {
88 self.inner.shutdown.send_replace(true);
89 let handle = self.inner.reader.lock().await.take();
90 if let Some(handle) = handle {
91 let _ = handle.await;
92 }
93 let activations = self.inner.activations.lock().await;
94 let workers = std::mem::take(&mut *self.inner.workers.lock().await);
95 drop(activations);
96 for worker in workers {
97 let _ = worker.await;
98 }
99 }
100
101 pub async fn take_events(&self) -> Option<mpsc::Receiver<ActorEvent>> {
102 self.inner.event_receiver.lock().await.take()
103 }
104
105 pub async fn complete_event(&self, event: &ActorEvent) -> Result<(), ActorError> {
106 self.inner.transport.ack(&event.entry).await
107 }
108
109 pub async fn retry_event(&self, event: &ActorEvent) {
110 self.inner
111 .transport
112 .inflight
113 .lock()
114 .await
115 .remove(&event.entry);
116 }
117
118 pub async fn socket_heartbeat(&self, user_id: i64) -> Result<(), ActorError> {
119 let ttl = self
120 .inner
121 .options
122 .idle_timeout
123 .unwrap_or(self.inner.options.lease_ttl)
124 .max(self.inner.options.lease_ttl)
125 .as_millis();
126 let mut command = redis::cmd("EVAL");
127 command.arg("local t=redis.call('TIME'); local now=t[1]*1000+math.floor(t[2]/1000); redis.call('ZREMRANGEBYSCORE',KEYS[1],'-inf',now); redis.call('ZADD',KEYS[1],now+ARGV[2],ARGV[1]); redis.call('PEXPIRE',KEYS[1],ARGV[2]*2); return 1")
128 .arg(1).arg(format!("{}:sockets:{user_id}", self.inner.options.key_prefix))
129 .arg(self.node_id()).arg(u64::try_from(ttl).unwrap_or(u64::MAX));
130 let _: i64 = self.inner.transport.commands.query(&command).await?;
131 self.heartbeat(user_id).await
132 }
133
134 pub async fn publish_event(&self, user_id: i64, payload: Value) -> Result<(), ActorError> {
135 self.push_store()
136 .append(user_id, payload)
137 .await
138 .map_err(ActorError::Remote)?;
139 let wake = async {
140 let mut command = redis::cmd("EVAL");
141 command.arg("local t=redis.call('TIME'); local now=t[1]*1000+math.floor(t[2]/1000); return redis.call('ZRANGEBYSCORE',KEYS[1],now+1,'+inf')")
142 .arg(1).arg(format!("{}:sockets:{user_id}", self.inner.options.key_prefix));
143 let nodes: Vec<String> = self
144 .inner
145 .transport
146 .commands
147 .query(&command)
148 .await
149 .unwrap_or_default();
150 for node in nodes {
151 let wire = super::super::transport::WireMessage::Event {
152 user_id,
153 payload: serde_json::json!({"wake": true}),
154 };
155 self.inner
156 .transport
157 .send(&self.inner.transport.inbox(&node), &wire)
158 .await
159 .ok();
160 }
161 };
162 let _ = tokio::time::timeout(Duration::from_secs(1), wake).await;
163 Ok(())
164 }
165
166 pub fn push_store(&self) -> crate::push_store::PushStore {
167 crate::push_store::PushStore::new(
168 self.inner.transport.commands.clone(),
169 self.inner.options.key_prefix.clone(),
170 )
171 }
172
173 pub(crate) async fn call_value(
174 &self,
175 id: ActorId,
176 payload: Value,
177 ) -> Result<Value, ActorError> {
178 let operation = async {
179 let (reply_tx, reply_rx) = oneshot::channel();
180 self.dispatch(id, payload, Some(ReplyTarget::Local(reply_tx)), None)
181 .await?;
182 reply_rx.await.map_err(|_| ActorError::ReplyLost)?
183 };
184 match tokio::time::timeout(self.inner.options.call_timeout, operation).await {
185 Ok(result) => result,
186 Err(_) => Err(ActorError::Timeout),
187 }
188 }
189
190 pub(crate) async fn tell_value(&self, id: ActorId, payload: Value) -> Result<(), ActorError> {
191 self.dispatch(id, payload, None, None).await
192 }
193}
Added lib/actor/src/actor/host/dispatch.rs lib/actor/src/actor/host/dispatch.rs +177 −0 View lib/actor/src/actor/host/dispatch.rs
@@ -0,0 +1,177 @@
1use serde_json::Value;
2use tokio::sync::mpsc;
3
4use super::super::error::ActorError;
5use super::super::id::ActorId;
6use super::super::transport::WireMessage;
7use super::core::{ActorHost, LocalRequest, ReplyTarget};
8
9impl ActorHost {
10 pub(super) async fn dispatch(
11 &self,
12 id: ActorId,
13 payload: Value,
14 reply: Option<ReplyTarget>,
15 entry: Option<String>,
16 ) -> Result<(), ActorError> {
17 self.dispatch_request(
18 id,
19 LocalRequest::Message {
20 payload,
21 reply,
22 entry,
23 },
24 )
25 .await
26 }
27
28 pub(super) async fn dispatch_request(
29 &self,
30 id: ActorId,
31 mut request: LocalRequest,
32 ) -> Result<(), ActorError> {
33 if *self.inner.shutdown.borrow() {
34 return Err(ActorError::ActivationClosed);
35 }
36 if let Some(sender) = self.existing_sender(id).await {
37 match sender.send(request).await {
38 Ok(()) => return Ok(()),
39 Err(error) => {
40 request = error.0;
41 self.remove_activation(id, &sender).await;
42 }
43 }
44 }
45
46 let owner = self.resolve_owner(id).await?;
47 if owner != self.inner.options.node_id {
48 self.forward(&owner, id, request).await?;
49 return Ok(());
50 }
51
52 let sender = self.sender_for(id).await?;
53 sender
54 .send(request)
55 .await
56 .map_err(|_| ActorError::ActivationClosed)
57 }
58
59 async fn forward(
60 &self,
61 owner: &str,
62 id: ActorId,
63 request: LocalRequest,
64 ) -> Result<(), ActorError> {
65 let stream = self.inner.transport.inbox(owner);
66
67 match request {
68 LocalRequest::Heartbeat => {
69 let wire = WireMessage::Heartbeat { target: id };
70 self.inner.transport.send(&stream, &wire).await?;
71 }
72 LocalRequest::Message {
73 payload,
74 reply,
75 entry,
76 } => {
77 match reply {
78 None => {
79 self.inner.transport.tell(&stream, id, payload).await?;
80 }
81 Some(ReplyTarget::Remote {
82 stream: reply_stream,
83 id: message_id,
84 }) => {
85 let wire = WireMessage::Request {
86 id: message_id,
87 target: id,
88 payload,
89 reply: Some(reply_stream),
90 };
91 self.inner.transport.send(&stream, &wire).await?;
92 }
93 Some(ReplyTarget::Local(reply)) => {
94 let result = self
95 .inner
96 .transport
97 .call(&stream, id, payload, self.inner.options.call_timeout)
98 .await;
99 let _ = reply.send(result);
100 }
101 }
102
103 if let Some(entry) = entry {
104 self.inner.transport.ack(&entry).await?;
105 }
106 }
107 }
108
109 Ok(())
110 }
111
112 async fn resolve_owner(&self, id: ActorId) -> Result<String, ActorError> {
113 let node_id = &self.inner.options.node_id;
114
115 for _ in 0..2 {
116 match self.inner.directory.owner(id).await? {
117 Some(owner) if owner != *node_id => return Ok(owner),
118 Some(_) => {
119 if self.inner.directory.renew(id, node_id).await? {
120 return Ok(node_id.clone());
121 }
122 }
123 None => {
124 if self.inner.directory.claim(id, node_id).await? {
125 return Ok(node_id.clone());
126 }
127 }
128 }
129 }
130
131 self.inner
132 .directory
133 .owner(id)
134 .await?
135 .ok_or(ActorError::DirectoryUnavailable)
136 }
137
138 async fn existing_sender(&self, id: ActorId) -> Option<mpsc::Sender<LocalRequest>> {
139 self.inner.activations.lock().await.get(&id).cloned()
140 }
141
142 async fn sender_for(&self, id: ActorId) -> Result<mpsc::Sender<LocalRequest>, ActorError> {
143 let mut activations = self.inner.activations.lock().await;
144 if *self.inner.shutdown.borrow() {
145 return Err(ActorError::ActivationClosed);
146 }
147 if let Some(sender) = activations.get(&id) {
148 return Ok(sender.clone());
149 }
150
151 let actor = (self.inner.factory)(id);
152 let (sender, receiver) = mpsc::channel(self.inner.options.mailbox_capacity);
153 activations.insert(id, sender.clone());
154
155 let host = self.clone();
156 let worker_sender = sender.clone();
157 let handle = tokio::spawn(async move {
158 host.run_activation(id, actor, worker_sender, receiver)
159 .await;
160 });
161 let mut workers = self.inner.workers.lock().await;
162 workers.retain(|worker| !worker.is_finished());
163 workers.push(handle);
164
165 Ok(sender)
166 }
167
168 pub(super) async fn remove_activation(&self, id: ActorId, sender: &mpsc::Sender<LocalRequest>) {
169 let mut activations = self.inner.activations.lock().await;
170 if activations
171 .get(&id)
172 .is_some_and(|current| current.same_channel(sender))
173 {
174 activations.remove(&id);
175 }
176 }
177}
Added lib/actor/src/actor/host/mod.rs lib/actor/src/actor/host/mod.rs +12 −0 View lib/actor/src/actor/host/mod.rs
@@ -0,0 +1,12 @@
1mod activation;
2mod builder;
3mod core;
4mod dispatch;
5mod node;
6mod options;
7mod reader;
8
9pub use builder::ActorHostBuilder;
10pub use core::{ActorEvent, ActorHost};
11pub use node::{NODE_FILE, load_or_create as load_node_id};
12pub use options::ActorHostOptions;
Added lib/actor/src/actor/host/node.rs lib/actor/src/actor/host/node.rs +33 −0 View lib/actor/src/actor/host/node.rs
@@ -0,0 +1,33 @@
1use std::path::Path;
2use std::{fs, io};
3
4use uuid::Uuid;
5
6pub const NODE_FILE: &str = "actor.node";
7
8pub fn load_or_create() -> io::Result<String> {
9 let path = Path::new(NODE_FILE);
10 match fs::read_to_string(path) {
11 Ok(value) => {
12 let value = value.trim();
13 if is_node_id(value) {
14 return Ok(value.to_string());
15 }
16 }
17 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
18 Err(error) => return Err(error),
19 }
20
21 let node_id = generate();
22 fs::write(path, &node_id)?;
23 Ok(node_id)
24}
25
26fn generate() -> String {
27 let (high, low) = Uuid::new_v4().as_u64_pair();
28 format!("{:016x}", high ^ low)
29}
30
31fn is_node_id(value: &str) -> bool {
32 value.len() == 16 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
33}
Added lib/actor/src/actor/host/options.rs lib/actor/src/actor/host/options.rs +114 −0 View lib/actor/src/actor/host/options.rs
@@ -0,0 +1,114 @@
1use std::time::Duration;
2
3use config::event::actor::ActorConfig;
4
5const DEFAULT_KEY_PREFIX: &str = "kenevent:actor";
6const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(5);
7const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
8const DEFAULT_LEASE_TTL: Duration = Duration::from_secs(15);
9const DEFAULT_MAILBOX_CAPACITY: usize = 1024;
10const DEFAULT_INBOX_MAX_LEN: usize = 10_000;
11const DEFAULT_READ_COUNT: usize = 64;
12const DEFAULT_READ_BLOCK_MS: u64 = 1_000;
13const MAX_NODE_ID_LEN: usize = 128;
14
15#[derive(Debug, Clone)]
16pub struct ActorHostOptions {
17 pub node_id: String,
18 pub key_prefix: String,
19 pub call_timeout: Duration,
20 pub idle_timeout: Option<Duration>,
21 pub lease_ttl: Duration,
22 pub mailbox_capacity: usize,
23 pub inbox_max_len: usize,
24 pub read_count: usize,
25 pub read_block_ms: u64,
26}
27
28impl ActorHostOptions {
29 pub fn new(node_id: impl Into<String>) -> Self {
30 Self {
31 node_id: node_id.into(),
32 ..Self::default()
33 }
34 }
35
36 pub fn validate(&self) -> Result<(), String> {
37 if !is_valid_node_id(&self.node_id) {
38 return Err(
39 "actor.node_id must use letters, digits, `_` or `-` and be at most 128 characters"
40 .to_string(),
41 );
42 }
43 self.validate_settings()
44 }
45
46 pub(crate) fn validate_settings(&self) -> Result<(), String> {
47 if self.key_prefix.trim().is_empty() {
48 return Err("actor.key_prefix must not be blank".to_string());
49 }
50 if self.call_timeout.is_zero() {
51 return Err("actor.call_timeout must be greater than zero".to_string());
52 }
53 if self.lease_ttl.is_zero() {
54 return Err("actor.lease_ttl must be greater than zero".to_string());
55 }
56 if self.idle_timeout.is_some_and(|idle| idle.is_zero()) {
57 return Err("actor.idle_timeout must be greater than zero".to_string());
58 }
59 if self.mailbox_capacity == 0 {
60 return Err("actor.mailbox_capacity must be greater than zero".to_string());
61 }
62 if self.inbox_max_len == 0 {
63 return Err("actor.inbox_max_len must be greater than zero".to_string());
64 }
65 if self.read_count == 0 {
66 return Err("actor.read_count must be greater than zero".to_string());
67 }
68 if self.read_block_ms == 0 {
69 return Err("actor.read_block_ms must be greater than zero".to_string());
70 }
71
72 Ok(())
73 }
74}
75
76impl Default for ActorHostOptions {
77 fn default() -> Self {
78 Self {
79 node_id: String::new(),
80 key_prefix: DEFAULT_KEY_PREFIX.to_string(),
81 call_timeout: DEFAULT_CALL_TIMEOUT,
82 idle_timeout: Some(DEFAULT_IDLE_TIMEOUT),
83 lease_ttl: DEFAULT_LEASE_TTL,
84 mailbox_capacity: DEFAULT_MAILBOX_CAPACITY,
85 inbox_max_len: DEFAULT_INBOX_MAX_LEN,
86 read_count: DEFAULT_READ_COUNT,
87 read_block_ms: DEFAULT_READ_BLOCK_MS,
88 }
89 }
90}
91
92impl From<&ActorConfig> for ActorHostOptions {
93 fn from(config: &ActorConfig) -> Self {
94 Self {
95 node_id: String::new(),
96 key_prefix: config.key_prefix.clone(),
97 call_timeout: Duration::from_secs(config.call_timeout_secs),
98 idle_timeout: config.idle_timeout_secs.map(Duration::from_secs),
99 lease_ttl: Duration::from_secs(config.lease_ttl_secs),
100 mailbox_capacity: config.mailbox_capacity,
101 inbox_max_len: config.inbox_max_len,
102 read_count: config.read_count,
103 read_block_ms: config.read_block_ms,
104 }
105 }
106}
107
108fn is_valid_node_id(value: &str) -> bool {
109 !value.is_empty()
110 && value.len() <= MAX_NODE_ID_LEN
111 && value
112 .chars()
113 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'))
114}
Added lib/actor/src/actor/host/reader.rs lib/actor/src/actor/host/reader.rs +117 −0 View lib/actor/src/actor/host/reader.rs
@@ -0,0 +1,117 @@
1use std::time::Duration;
2
3use super::super::error::ActorError;
4use super::super::transport::WireMessage;
5use super::core::{ActorEvent, ActorHost, LocalRequest, ReplyTarget};
6
7const ERROR_BACKOFF: Duration = Duration::from_millis(250);
8
9impl ActorHost {
10 pub(super) async fn run_reader(self) {
11 let mut shutdown = self.inner.shutdown.subscribe();
12 let reader = async {
13 loop {
14 self.recover_pending().await;
15 match self.inner.transport.receive().await {
16 Ok(entries) => {
17 for (entry, wire) in entries {
18 self.handle_entry(entry, wire).await;
19 }
20 }
21 Err(_) => tokio::time::sleep(ERROR_BACKOFF).await,
22 }
23 }
24 };
25
26 if *shutdown.borrow() {
27 return;
28 }
29 tokio::select! {
30 _ = shutdown.changed() => {}
31 _ = reader => {}
32 }
33 }
34
35 async fn recover_pending(&self) {
36 let mut after = "0-0".to_string();
37
38 loop {
39 let entries = match self.inner.transport.receive_pending(&after).await {
40 Ok(entries) => entries,
41 Err(_) => return,
42 };
43 let Some(last) = entries.last().map(|(entry, _)| entry.clone()) else {
44 return;
45 };
46 after = last;
47
48 for (entry, wire) in entries {
49 self.handle_entry(entry, wire).await;
50 }
51 }
52 }
53
54 async fn handle_entry(&self, entry: String, wire: Option<WireMessage>) {
55 if !self
56 .inner
57 .transport
58 .inflight
59 .lock()
60 .await
61 .insert(entry.clone())
62 {
63 return;
64 }
65 match wire {
66 Some(WireMessage::Event { user_id, payload }) => {
67 let event = ActorEvent {
68 user_id,
69 payload,
70 entry: entry.clone(),
71 };
72 if self.inner.events.try_send(event).is_err() {
73 self.inner.transport.inflight.lock().await.remove(&entry);
74 }
75 }
76 Some(WireMessage::Response { id, result }) => {
77 self.inner
78 .transport
79 .complete(&id, result.map_err(ActorError::Remote))
80 .await;
81 let _ = self.inner.transport.ack(&entry).await;
82 }
83 Some(WireMessage::Request {
84 id,
85 target,
86 payload,
87 reply,
88 }) => {
89 let reply = reply.map(|stream| ReplyTarget::Remote { stream, id });
90 if !matches!(
91 tokio::time::timeout(
92 self.inner.options.call_timeout,
93 self.dispatch(target, payload, reply, Some(entry.clone()))
94 )
95 .await,
96 Ok(Ok(()))
97 ) {
98 self.inner.transport.inflight.lock().await.remove(&entry);
99 }
100 }
101 Some(WireMessage::Heartbeat { target }) => {
102 if self
103 .dispatch_request(target, LocalRequest::Heartbeat)
104 .await
105 .is_ok()
106 {
107 let _ = self.inner.transport.ack(&entry).await;
108 } else {
109 self.inner.transport.inflight.lock().await.remove(&entry);
110 }
111 }
112 None => {
113 let _ = self.inner.transport.ack(&entry).await;
114 }
115 }
116 }
117}
Added lib/actor/src/actor/transport/client.rs lib/actor/src/actor/transport/client.rs +136 −0 View lib/actor/src/actor/transport/client.rs
@@ -0,0 +1,136 @@
1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Duration;
5
6use serde_json::Value;
7use tokio::sync::{Mutex, oneshot};
8
9use super::super::error::ActorError;
10use super::super::id::ActorId;
11use super::super::redis_conn::RedisConnection;
12use super::message::WireMessage;
13
14#[derive(Clone)]
15pub(crate) struct RedisTransport {
16 pub(crate) commands: RedisConnection,
17 pub(super) reads: RedisConnection,
18 pub(super) prefix: String,
19 pub(super) stream: String,
20 pub(super) group: String,
21 pub(super) consumer: String,
22 pub(super) pending: Arc<Mutex<HashMap<String, oneshot::Sender<Result<Value, ActorError>>>>>,
23 pub(crate) inflight: Arc<Mutex<HashSet<String>>>,
24 pub(crate) max_len: usize,
25 pub(super) read_count: usize,
26 pub(super) read_block_ms: u64,
27 pub(crate) counter: Arc<AtomicU64>,
28}
29
30impl RedisTransport {
31 pub(crate) fn new(
32 commands: RedisConnection,
33 reads: RedisConnection,
34 prefix: String,
35 node_id: String,
36 max_len: usize,
37 read_count: usize,
38 read_block_ms: u64,
39 ) -> Self {
40 let stream = format!("{prefix}:inbox:{node_id}");
41 Self {
42 commands,
43 reads,
44 prefix,
45 stream,
46 group: node_id.clone(),
47 consumer: node_id,
48 pending: Arc::new(Mutex::new(HashMap::new())),
49 inflight: Arc::new(Mutex::new(HashSet::new())),
50 max_len,
51 read_count,
52 read_block_ms,
53 counter: Arc::new(AtomicU64::new(0)),
54 }
55 }
56
57 pub(crate) fn inbox(&self, node_id: &str) -> String {
58 format!("{}:inbox:{}", self.prefix, node_id)
59 }
60
61 pub(crate) async fn send(&self, stream: &str, wire: &WireMessage) -> Result<(), ActorError> {
62 let payload = serde_json::to_string(wire).map_err(ActorError::Encode)?;
63 let mut cmd = redis::cmd("EVAL");
64 cmd.arg("if redis.call('XLEN',KEYS[1]) >= tonumber(ARGV[1]) then return false end; return redis.call('XADD',KEYS[1],'*','m',ARGV[2])")
65 .arg(1)
66 .arg(stream)
67 .arg(self.max_len)
68 .arg(payload);
69 let entry: Option<String> = self.commands.query(&cmd).await?;
70 if entry.is_none() {
71 return Err(ActorError::Remote("actor inbox is full".to_string()));
72 }
73 Ok(())
74 }
75
76 pub(crate) async fn tell(
77 &self,
78 stream: &str,
79 target: ActorId,
80 payload: Value,
81 ) -> Result<(), ActorError> {
82 let wire = WireMessage::Request {
83 id: self.next_id(),
84 target,
85 payload,
86 reply: None,
87 };
88 self.send(stream, &wire).await
89 }
90
91 pub(crate) async fn call(
92 &self,
93 stream: &str,
94 target: ActorId,
95 payload: Value,
96 timeout: Duration,
97 ) -> Result<Value, ActorError> {
98 let id = self.next_id();
99 let (reply_tx, reply_rx) = oneshot::channel();
100 self.pending
101 .lock()
102 .await
103 .retain(|_, reply| !reply.is_closed());
104 self.pending.lock().await.insert(id.clone(), reply_tx);
105
106 let wire = WireMessage::Request {
107 id: id.clone(),
108 target,
109 payload,
110 reply: Some(self.stream.clone()),
111 };
112 let result = tokio::time::timeout(timeout, async {
113 self.send(stream, &wire).await?;
114 reply_rx.await.map_err(|_| ActorError::ReplyLost)?
115 })
116 .await
117 .unwrap_or(Err(ActorError::Timeout));
118 self.pending.lock().await.remove(&id);
119 result
120 }
121
122 pub(crate) async fn complete(&self, id: &str, result: Result<Value, ActorError>) {
123 if let Some(reply) = self.pending.lock().await.remove(id) {
124 let _ = reply.send(result);
125 }
126 }
127
128 fn next_id(&self) -> String {
129 let now = std::time::SystemTime::now()
130 .duration_since(std::time::UNIX_EPOCH)
131 .unwrap_or_default()
132 .as_nanos();
133 let counter = self.counter.fetch_add(1, Ordering::Relaxed);
134 format!("{}-{now}-{counter}", self.consumer)
135 }
136}
Added lib/actor/src/actor/transport/message.rs lib/actor/src/actor/transport/message.rs +26 −0 View lib/actor/src/actor/transport/message.rs
@@ -0,0 +1,26 @@
1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use super::super::id::ActorId;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(tag = "kind", rename_all = "snake_case")]
8pub(crate) enum WireMessage {
9 Event {
10 user_id: i64,
11 payload: Value,
12 },
13 Request {
14 id: String,
15 target: ActorId,
16 payload: Value,
17 reply: Option<String>,
18 },
19 Response {
20 id: String,
21 result: Result<Value, String>,
22 },
23 Heartbeat {
24 target: ActorId,
25 },
26}
Added lib/actor/src/actor/transport/mod.rs lib/actor/src/actor/transport/mod.rs +6 −0 View lib/actor/src/actor/transport/mod.rs
@@ -0,0 +1,6 @@
1mod client;
2mod message;
3mod stream;
4
5pub(crate) use client::RedisTransport;
6pub(crate) use message::WireMessage;
Added lib/actor/src/actor/transport/stream.rs lib/actor/src/actor/transport/stream.rs +120 −0 View lib/actor/src/actor/transport/stream.rs
@@ -0,0 +1,120 @@
1use redis::FromRedisValue;
2use redis::streams::StreamReadReply;
3
4use super::super::error::ActorError;
5use super::client::RedisTransport;
6use super::message::WireMessage;
7
8impl RedisTransport {
9 pub(crate) async fn ensure_group(&self) -> Result<(), ActorError> {
10 let mut cmd = redis::cmd("XGROUP");
11 cmd.arg("CREATE")
12 .arg(&self.stream)
13 .arg(&self.group)
14 .arg("0")
15 .arg("MKSTREAM");
16 match self.commands.query::<redis::Value>(&cmd).await {
17 Ok(_) => {}
18 Err(error) if error.to_string().contains("BUSYGROUP") => {}
19 Err(error) => return Err(error.into()),
20 }
21 let mut compact = redis::cmd("EVAL");
22 compact
23 .arg(
24 r#"
25local cutoff
26for _, group in ipairs(redis.call('XINFO', 'GROUPS', KEYS[1])) do
27 local name, delivered
28 for i = 1, #group, 2 do
29 if group[i] == 'name' then name = group[i + 1] end
30 if group[i] == 'last-delivered-id' then delivered = group[i + 1] end
31 end
32 if name == ARGV[1] then cutoff = delivered end
33end
34local removed = 0
35if cutoff then
36 for _, entry in ipairs(redis.call('XRANGE', KEYS[1], '-', cutoff)) do
37 if #redis.call('XPENDING', KEYS[1], ARGV[1], entry[1], entry[1], 1) == 0 then
38 removed = removed + redis.call('XDEL', KEYS[1], entry[1])
39 end
40 end
41end
42return removed
43"#,
44 )
45 .arg(1)
46 .arg(&self.stream)
47 .arg(&self.group);
48 let _: i64 = self.commands.query(&compact).await?;
49 Ok(())
50 }
51
52 pub(crate) async fn ack(&self, entry: &str) -> Result<(), ActorError> {
53 let mut cmd = redis::cmd("EVAL");
54 cmd.arg(
55 "redis.call('XACK',KEYS[1],ARGV[1],ARGV[2]); return redis.call('XDEL',KEYS[1],ARGV[2])",
56 )
57 .arg(1)
58 .arg(&self.stream)
59 .arg(&self.group)
60 .arg(entry);
61 let result = self.commands.query::<i64>(&cmd).await;
62 self.inflight.lock().await.remove(entry);
63 result.map(|_| ()).map_err(Into::into)
64 }
65
66 pub(crate) async fn receive(&self) -> Result<Vec<(String, Option<WireMessage>)>, ActorError> {
67 self.pending
68 .lock()
69 .await
70 .retain(|_, sender| !sender.is_closed());
71 let mut cmd = redis::cmd("XREADGROUP");
72 cmd.arg("GROUP")
73 .arg(&self.group)
74 .arg(&self.consumer)
75 .arg("COUNT")
76 .arg(self.read_count)
77 .arg("BLOCK")
78 .arg(self.read_block_ms)
79 .arg("STREAMS")
80 .arg(&self.stream)
81 .arg(">");
82 let reply: Option<StreamReadReply> = self.reads.query(&cmd).await?;
83 Ok(decode(reply))
84 }
85
86 pub(crate) async fn receive_pending(
87 &self,
88 after: &str,
89 ) -> Result<Vec<(String, Option<WireMessage>)>, ActorError> {
90 let mut cmd = redis::cmd("XREADGROUP");
91 cmd.arg("GROUP")
92 .arg(&self.group)
93 .arg(&self.consumer)
94 .arg("COUNT")
95 .arg(self.read_count)
96 .arg("STREAMS")
97 .arg(&self.stream)
98 .arg(after);
99 let reply: Option<StreamReadReply> = self.reads.query(&cmd).await?;
100 Ok(decode(reply))
101 }
102}
103
104fn decode(reply: Option<StreamReadReply>) -> Vec<(String, Option<WireMessage>)> {
105 let Some(reply) = reply else {
106 return Vec::new();
107 };
108 let mut entries = Vec::new();
109 for key in reply.keys {
110 for entry in key.ids {
111 let wire = entry
112 .map
113 .get("m")
114 .and_then(|value| String::from_redis_value(value.clone()).ok())
115 .and_then(|text| serde_json::from_str(&text).ok());
116 entries.push((entry.id, wire));
117 }
118 }
119 entries
120}
Added lib/actor/src/actor/actor.rs lib/actor/src/actor/actor.rs +147 −0 View lib/actor/src/actor/actor.rs
@@ -0,0 +1,147 @@
1use std::future::Future;
2use std::marker::PhantomData;
3use std::pin::Pin;
4
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7use serde_json::Value;
8
9use super::error::ActorError;
10use super::host::ActorHost;
11use super::id::ActorId;
12
13pub(crate) type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
14
15pub trait VirtualActor: Send + Sized + 'static {
16 type Message: Serialize + DeserializeOwned + Send + 'static;
17 type Response: Serialize + DeserializeOwned + Send + 'static;
18
19 fn receive<'a>(
20 &'a mut self,
21 ctx: &'a ActorContext,
22 message: Self::Message,
23 ) -> impl Future<Output = Result<Self::Response, ActorError>> + Send + 'a;
24
25 fn on_activate<'a>(
26 &'a mut self,
27 _ctx: &'a ActorContext,
28 ) -> impl Future<Output = Result<(), ActorError>> + Send + 'a {
29 async { Ok(()) }
30 }
31
32 fn on_deactivate<'a>(
33 &'a mut self,
34 _ctx: &'a ActorContext,
35 ) -> impl Future<Output = ()> + Send + 'a {
36 async {}
37 }
38}
39
40pub(crate) trait ErasedActor: Send {
41 fn receive<'a>(
42 &'a mut self,
43 ctx: &'a ActorContext,
44 payload: Value,
45 ) -> BoxFuture<'a, Result<Value, ActorError>>;
46
47 fn activate<'a>(&'a mut self, ctx: &'a ActorContext) -> BoxFuture<'a, Result<(), ActorError>>;
48
49 fn deactivate<'a>(&'a mut self, ctx: &'a ActorContext) -> BoxFuture<'a, ()>;
50}
51
52pub(crate) struct ActorAdapter<A: VirtualActor> {
53 actor: A,
54}
55
56impl<A: VirtualActor> ActorAdapter<A> {
57 pub(crate) fn new(actor: A) -> Self {
58 Self { actor }
59 }
60}
61
62impl<A: VirtualActor> ErasedActor for ActorAdapter<A> {
63 fn receive<'a>(
64 &'a mut self,
65 ctx: &'a ActorContext,
66 payload: Value,
67 ) -> BoxFuture<'a, Result<Value, ActorError>> {
68 Box::pin(async move {
69 let message = serde_json::from_value(payload).map_err(ActorError::Decode)?;
70 let response = self.actor.receive(ctx, message).await?;
71 serde_json::to_value(response).map_err(ActorError::Encode)
72 })
73 }
74
75 fn activate<'a>(&'a mut self, ctx: &'a ActorContext) -> BoxFuture<'a, Result<(), ActorError>> {
76 Box::pin(async move { self.actor.on_activate(ctx).await })
77 }
78
79 fn deactivate<'a>(&'a mut self, ctx: &'a ActorContext) -> BoxFuture<'a, ()> {
80 Box::pin(async move { self.actor.on_deactivate(ctx).await })
81 }
82}
83
84#[derive(Clone)]
85pub struct ActorContext {
86 id: ActorId,
87 host: ActorHost,
88}
89
90impl ActorContext {
91 pub(crate) fn new(id: ActorId, host: ActorHost) -> Self {
92 Self { id, host }
93 }
94
95 pub fn id(&self) -> ActorId {
96 self.id
97 }
98
99 pub fn host(&self) -> &ActorHost {
100 &self.host
101 }
102
103 pub fn actor<A: VirtualActor>(&self, id: impl Into<ActorId>) -> ActorRef<A> {
104 self.host.actor(id)
105 }
106}
107
108pub struct ActorRef<A: VirtualActor> {
109 host: ActorHost,
110 id: ActorId,
111 marker: PhantomData<fn() -> A>,
112}
113
114impl<A: VirtualActor> ActorRef<A> {
115 pub(crate) fn new(host: ActorHost, id: ActorId) -> Self {
116 Self {
117 host,
118 id,
119 marker: PhantomData,
120 }
121 }
122
123 pub fn id(&self) -> ActorId {
124 self.id
125 }
126
127 pub async fn call(&self, message: A::Message) -> Result<A::Response, ActorError> {
128 let payload = serde_json::to_value(message).map_err(ActorError::Encode)?;
129 let response = self.host.call_value(self.id, payload).await?;
130 serde_json::from_value(response).map_err(ActorError::Decode)
131 }
132
133 pub async fn tell(&self, message: A::Message) -> Result<(), ActorError> {
134 let payload = serde_json::to_value(message).map_err(ActorError::Encode)?;
135 self.host.tell_value(self.id, payload).await
136 }
137}
138
139impl<A: VirtualActor> Clone for ActorRef<A> {
140 fn clone(&self) -> Self {
141 Self {
142 host: self.host.clone(),
143 id: self.id,
144 marker: PhantomData,
145 }
146 }
147}
Added lib/actor/src/actor/directory.rs lib/actor/src/actor/directory.rs +71 −0 View lib/actor/src/actor/directory.rs
@@ -0,0 +1,71 @@
1use std::time::Duration;
2
3use super::error::ActorError;
4use super::id::ActorId;
5use super::redis_conn::RedisConnection;
6
7const RENEW_SCRIPT: &str = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end";
8const RELEASE_SCRIPT: &str = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end";
9
10pub(crate) struct RedisDirectory {
11 connection: RedisConnection,
12 prefix: String,
13 ttl: Duration,
14}
15
16impl RedisDirectory {
17 pub(crate) fn new(connection: RedisConnection, prefix: String, ttl: Duration) -> Self {
18 Self {
19 connection,
20 prefix,
21 ttl,
22 }
23 }
24
25 pub(crate) async fn owner(&self, id: ActorId) -> Result<Option<String>, ActorError> {
26 let mut cmd = redis::cmd("GET");
27 cmd.arg(self.key(id));
28 let owner: Option<String> = self.connection.query(&cmd).await?;
29 Ok(owner)
30 }
31
32 pub(crate) async fn claim(&self, id: ActorId, node_id: &str) -> Result<bool, ActorError> {
33 let mut cmd = redis::cmd("SET");
34 cmd.arg(self.key(id))
35 .arg(node_id)
36 .arg("NX")
37 .arg("PX")
38 .arg(ttl_millis(self.ttl));
39 let claimed: Option<String> = self.connection.query(&cmd).await?;
40 Ok(claimed.is_some())
41 }
42
43 pub(crate) async fn renew(&self, id: ActorId, node_id: &str) -> Result<bool, ActorError> {
44 let mut cmd = redis::cmd("EVAL");
45 cmd.arg(RENEW_SCRIPT)
46 .arg(1)
47 .arg(self.key(id))
48 .arg(node_id)
49 .arg(ttl_millis(self.ttl));
50 let renewed: i64 = self.connection.query(&cmd).await?;
51 Ok(renewed == 1)
52 }
53
54 pub(crate) async fn release(&self, id: ActorId, node_id: &str) -> Result<bool, ActorError> {
55 let mut cmd = redis::cmd("EVAL");
56 cmd.arg(RELEASE_SCRIPT)
57 .arg(1)
58 .arg(self.key(id))
59 .arg(node_id);
60 let released: i64 = self.connection.query(&cmd).await?;
61 Ok(released == 1)
62 }
63
64 fn key(&self, id: ActorId) -> String {
65 format!("{}:directory:{}", self.prefix, id.user_id())
66 }
67}
68
69fn ttl_millis(ttl: Duration) -> u64 {
70 u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX)
71}
Added lib/actor/src/actor/error.rs lib/actor/src/actor/error.rs +58 −0 View lib/actor/src/actor/error.rs
@@ -0,0 +1,58 @@
1use std::error::Error;
2use std::fmt;
3
4use redis::RedisError;
5
6#[derive(Debug)]
7pub enum ActorError {
8 InvalidOptions(String),
9 Io(std::io::Error),
10 Redis(RedisError),
11 Decode(serde_json::Error),
12 Encode(serde_json::Error),
13 Timeout,
14 ActivationClosed,
15 ReplyLost,
16 DirectoryUnavailable,
17 Remote(String),
18}
19
20impl fmt::Display for ActorError {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 Self::InvalidOptions(message) => write!(f, "invalid actor options: {message}"),
24 Self::Io(error) => write!(f, "actor node file error: {error}"),
25 Self::Redis(error) => write!(f, "redis error: {error}"),
26 Self::Decode(error) => write!(f, "failed to decode actor message: {error}"),
27 Self::Encode(error) => write!(f, "failed to encode actor message: {error}"),
28 Self::Timeout => f.write_str("actor call timed out"),
29 Self::ActivationClosed => f.write_str("actor activation is closed"),
30 Self::ReplyLost => f.write_str("actor reply channel was dropped"),
31 Self::DirectoryUnavailable => f.write_str("actor directory entry is unavailable"),
32 Self::Remote(message) => write!(f, "remote actor error: {message}"),
33 }
34 }
35}
36
37impl Error for ActorError {
38 fn source(&self) -> Option<&(dyn Error + 'static)> {
39 match self {
40 Self::Io(error) => Some(error),
41 Self::Redis(error) => Some(error),
42 Self::Decode(error) | Self::Encode(error) => Some(error),
43 _ => None,
44 }
45 }
46}
47
48impl From<RedisError> for ActorError {
49 fn from(error: RedisError) -> Self {
50 Self::Redis(error)
51 }
52}
53
54impl From<serde_json::Error> for ActorError {
55 fn from(error: serde_json::Error) -> Self {
56 Self::Decode(error)
57 }
58}
Added lib/actor/src/actor/id.rs lib/actor/src/actor/id.rs +35 −0 View lib/actor/src/actor/id.rs
@@ -0,0 +1,35 @@
1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
6#[serde(transparent)]
7pub struct ActorId(i64);
8
9impl ActorId {
10 pub const fn new(user_id: i64) -> Self {
11 Self(user_id)
12 }
13
14 pub const fn user_id(self) -> i64 {
15 self.0
16 }
17}
18
19impl fmt::Display for ActorId {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 write!(f, "{}", self.0)
22 }
23}
24
25impl From<i64> for ActorId {
26 fn from(user_id: i64) -> Self {
27 Self(user_id)
28 }
29}
30
31impl From<ActorId> for i64 {
32 fn from(actor_id: ActorId) -> Self {
33 actor_id.0
34 }
35}
Added lib/actor/src/actor/mod.rs lib/actor/src/actor/mod.rs +16 −0 View lib/actor/src/actor/mod.rs
@@ -0,0 +1,16 @@
1mod actor;
2mod directory;
3mod error;
4mod host;
5mod id;
6mod redis_conn;
7mod transport;
8
9pub use actor::{ActorContext, ActorRef, VirtualActor};
10pub use error::ActorError;
11pub use host::{
12 ActorEvent, ActorHost, ActorHostBuilder, ActorHostOptions, NODE_FILE, load_node_id,
13};
14pub use id::ActorId;
15
16pub use redis_conn::{RedisConnection, connect_single as connect_redis_single};
Added lib/actor/src/actor/redis_conn.rs lib/actor/src/actor/redis_conn.rs +48 −0 View lib/actor/src/actor/redis_conn.rs
@@ -0,0 +1,48 @@
1//! Redis 连接统一走 `lib/cache`:连接池、集群/单机选择、超时与错误类型都由 cache 维护。
2//! 本模块只保留 actor/订阅需要的 `query` 便捷方法。
3
4use cache::CacheConn;
5use redis::{Cmd, FromRedisValue, RedisError, RedisResult};
6
7use config::redis::RedisConfig;
8
9use super::error::ActorError;
10
11#[derive(Clone)]
12pub struct RedisConnection {
13 conn: CacheConn,
14}
15
16impl RedisConnection {
17 pub async fn connect(config: &RedisConfig) -> Result<Self, ActorError> {
18 let conn = CacheConn::init(config)
19 .await
20 .map_err(|error| ActorError::InvalidOptions(error.to_string()))?;
21 Ok(Self { conn })
22 }
23
24 pub async fn query<T: FromRedisValue>(&self, cmd: &Cmd) -> RedisResult<T> {
25 let mut conn = self.conn.clone();
26 cmd.query_async(&mut conn).await
27 }
28}
29
30pub async fn connect(
31 config: &RedisConfig,
32) -> Result<(RedisConnection, RedisConnection), ActorError> {
33 let connection = RedisConnection::connect(config).await?;
34 Ok((connection.clone(), connection))
35}
36
37pub async fn connect_single(config: &RedisConfig) -> Result<RedisConnection, ActorError> {
38 RedisConnection::connect(config).await
39}
40
41#[allow(dead_code)]
42fn cache_error(error: cache::CacheError) -> RedisError {
43 RedisError::from((
44 redis::ErrorKind::Io,
45 "redis cache pool error",
46 error.to_string(),
47 ))
48}
Added lib/actor/src/lib.rs lib/actor/src/lib.rs +8 −0 View lib/actor/src/lib.rs
@@ -0,0 +1,8 @@
1pub mod actor;
2pub mod push_store;
3
4pub use actor::{
5 ActorContext, ActorError, ActorEvent, ActorHost, ActorHostBuilder, ActorHostOptions, ActorId,
6 ActorRef, NODE_FILE, RedisConnection, VirtualActor, connect_redis_single, load_node_id,
7};
8pub use push_store::PushStore;
Added lib/actor/src/push_store.rs lib/actor/src/push_store.rs +277 −0 View lib/actor/src/push_store.rs
@@ -0,0 +1,277 @@
1use std::time::Duration;
2
3use base64::Engine as _;
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use redis::FromRedisValue;
6use redis::streams::StreamReadReply;
7use serde_json::Value;
8use uuid::Uuid;
9
10use crate::actor::RedisConnection;
11
12const MAX_PENDING: usize = 10_000;
13const DEDUP_SECONDS: u64 = 30 * 24 * 60 * 60;
14
15#[derive(Clone)]
16pub struct PushStore {
17 connection: RedisConnection,
18 prefix: String,
19}
20
21impl PushStore {
22 pub(crate) fn new(connection: RedisConnection, prefix: String) -> Self {
23 Self { connection, prefix }
24 }
25
26 async fn query<T: FromRedisValue>(&self, command: &redis::Cmd) -> Result<T, String> {
27 tokio::time::timeout(Duration::from_secs(5), self.connection.query(command))
28 .await
29 .map_err(|_| "push storage timed out".to_string())?
30 .map_err(|error| error.to_string())
31 }
32
33 fn key(&self, user_id: i64, suffix: &str) -> String {
34 format!("{}:push:{{{user_id}}}:{suffix}", self.prefix)
35 }
36
37 fn device(device_id: &str, namespace: &str) -> String {
38 URL_SAFE_NO_PAD.encode(serde_json::to_vec(&(device_id, namespace)).unwrap_or_default())
39 }
40
41 pub async fn append(&self, user_id: i64, payload: Value) -> Result<String, String> {
42 if user_id <= 0 {
43 return Err("push user must be positive".to_string());
44 }
45 let event_id = payload
46 .get("event_id")
47 .or_else(|| payload.pointer("/body/id"))
48 .and_then(Value::as_str)
49 .ok_or("push event_id is required")?;
50 let id = Uuid::parse_str(event_id).map_err(|_| "push event_id must be a UUID")?;
51 let event_id = id.to_string();
52 let source_order = if payload.get("source").and_then(Value::as_str) == Some("socket") {
53 String::new()
54 } else if id.get_version_num() == 7 {
55 id.simple().to_string()
56 } else {
57 "unknown".to_string()
58 };
59 let resource = ["room_id", "issue_id", "pull_id", "repo_id", "org_id"]
60 .into_iter()
61 .find_map(|field| {
62 payload
63 .get("properties")?
64 .get(field)?
65 .as_str()
66 .map(|id| format!("{field}:{id}"))
67 })
68 .unwrap_or_else(|| format!("user:{user_id}"));
69 if !payload.is_object() {
70 return Err("push payload must be an object".to_string());
71 }
72 let mut command = redis::cmd("EVAL");
73 command.arg(r#"
74local existing = redis.call('GET', KEYS[3])
75if existing then return existing end
76if redis.call('XLEN', KEYS[1]) >= tonumber(ARGV[3]) then return redis.error_reply('push backlog is full') end
77local prior = redis.call('HGET', KEYS[4], ARGV[5])
78local resync = ARGV[4] == 'unknown' or (ARGV[4] ~= '' and prior and ARGV[4] < prior) or false
79if ARGV[4] ~= '' and ARGV[4] ~= 'unknown' and (not prior or ARGV[4] > prior) then
80 redis.call('HSET', KEYS[4], ARGV[5], ARGV[4])
81end
82redis.call('INCR', KEYS[2])
83local sequence = redis.call('GET', KEYS[2])
84local delivery = sequence .. '-0'
85redis.call('XADD', KEYS[1], delivery, 'payload', ARGV[1], 'sequence', sequence, 'event_id', ARGV[2], 'resync_required', resync and '1' or '0')
86redis.call('SET', KEYS[3], delivery, 'EX', ARGV[6])
87return delivery
88"#).arg(4).arg(self.key(user_id, "events")).arg(self.key(user_id, "sequence"))
89 .arg(self.key(user_id, &format!("dedup:{event_id}"))).arg(self.key(user_id, "source-order"))
90 .arg(serde_json::to_string(&payload).map_err(|error| error.to_string())?)
91 .arg(event_id).arg(MAX_PENDING).arg(source_order).arg(resource).arg(DEDUP_SECONDS);
92 self.query(&command).await
93 }
94
95 pub async fn legacy_event_id(
96 &self,
97 user: i64,
98 node: &str,
99 entry: &str,
100 ) -> Result<String, String> {
101 let mut command = redis::cmd("EVAL");
102 command.arg("redis.call('SET',KEYS[1],ARGV[1],'NX','EX',ARGV[2]); return redis.call('GET',KEYS[1])")
103 .arg(1).arg(self.key(user,&format!("legacy:{node}:{entry}"))).arg(Uuid::new_v4().to_string()).arg(DEDUP_SECONDS);
104 self.query(&command).await
105 }
106
107 pub async fn claim(
108 &self,
109 user_id: i64,
110 device: &str,
111 namespace: &str,
112 owner: &str,
113 ) -> Result<bool, String> {
114 let field = Self::device(device, namespace);
115 let mut command = redis::cmd("EVAL");
116 command.arg("local current=redis.call('GET',KEYS[1]); if current and current~=ARGV[1] then return 0 end; redis.call('SET',KEYS[1],ARGV[1],'PX',30000); redis.call('HSETNX',KEYS[2],ARGV[2],'0-0'); return 1")
117 .arg(2).arg(self.key(user_id, &format!("lease:{field}"))).arg(self.key(user_id,"cursors"))
118 .arg(owner).arg(field);
119 self.query::<i64>(&command).await.map(|result| result == 1)
120 }
121
122 pub async fn register_device(
123 &self,
124 user_id: i64,
125 device: &str,
126 namespace: &str,
127 ) -> Result<(), String> {
128 let mut command = redis::cmd("EVAL");
129 command.arg("if redis.call('HEXISTS',KEYS[1],ARGV[1])==0 and redis.call('HLEN',KEYS[1])>=256 then return redis.error_reply('device namespace limit exceeded') end; return redis.call('HSETNX',KEYS[1],ARGV[1],'0-0')")
130 .arg(1).arg(self.key(user_id,"cursors")).arg(Self::device(device,namespace));
131 self.query::<i64>(&command).await.map(|_| ())
132 }
133
134 pub async fn forget_device(&self, user: i64, device: &str) -> Result<(), String> {
135 let mut command = redis::cmd("HKEYS");
136 command.arg(self.key(user, "cursors"));
137 let fields: Vec<String> = self.query(&command).await?;
138 let fields = fields
139 .into_iter()
140 .filter(|field| {
141 URL_SAFE_NO_PAD
142 .decode(field)
143 .ok()
144 .and_then(|value| serde_json::from_slice::<(String, String)>(&value).ok())
145 .is_some_and(|(known, _)| known == device)
146 })
147 .collect::<Vec<_>>();
148 if fields.is_empty() {
149 return Ok(());
150 }
151 let mut command = redis::cmd("EVAL");
152 command.arg("for i=2,#KEYS do if redis.call('EXISTS',KEYS[i])==1 then return redis.error_reply('device is still connected') end end; for _,field in ipairs(ARGV) do redis.call('HDEL',KEYS[1],field) end; return 1")
153 .arg(fields.len()+1).arg(self.key(user,"cursors"));
154 for field in &fields {
155 command.arg(self.key(user, &format!("lease:{field}")));
156 }
157 command.arg(fields);
158 self.query::<i64>(&command).await.map(|_| ())
159 }
160
161 pub async fn next(
162 &self,
163 user_id: i64,
164 device: &str,
165 namespace: &str,
166 ) -> Result<(String, Option<(String, Value)>), String> {
167 let mut cursor = redis::cmd("HGET");
168 cursor
169 .arg(self.key(user_id, "cursors"))
170 .arg(Self::device(device, namespace));
171 let cursor = self
172 .query::<Option<String>>(&cursor)
173 .await?
174 .unwrap_or_else(|| "0-0".to_string());
175 let mut read = redis::cmd("XREAD");
176 read.arg("COUNT")
177 .arg(1)
178 .arg("STREAMS")
179 .arg(self.key(user_id, "events"))
180 .arg(&cursor);
181 let result: Option<StreamReadReply> = self.query(&read).await?;
182 let entry = result
183 .into_iter()
184 .flat_map(|reply| reply.keys)
185 .flat_map(|key| key.ids)
186 .next();
187 let next = match entry {
188 Some(entry) => {
189 let value = entry.map.get("payload").ok_or("invalid push entry")?;
190 let text =
191 String::from_redis_value(value.clone()).map_err(|error| error.to_string())?;
192 let mut payload: Value =
193 serde_json::from_str(&text).map_err(|error| error.to_string())?;
194 if !payload.is_object() {
195 return Err("invalid push payload".to_string());
196 }
197 let metadata = |field: &str| -> Result<String, String> {
198 let value = entry.map.get(field).ok_or("missing push metadata")?;
199 String::from_redis_value(value.clone()).map_err(|error| error.to_string())
200 };
201 payload["delivery_id"] = Value::String(entry.id.clone());
202 payload["sequence"] = Value::String(metadata("sequence")?);
203 payload["event_id"] = Value::String(metadata("event_id")?);
204 payload["resync_required"] = Value::Bool(metadata("resync_required")? == "1");
205 Some((entry.id, payload))
206 }
207 None => None,
208 };
209 Ok((cursor, next))
210 }
211
212 pub async fn acknowledge(
213 &self,
214 user_id: i64,
215 device: &str,
216 namespace: &str,
217 owner: &str,
218 previous: &str,
219 delivery: &str,
220 ) -> Result<bool, String> {
221 let field = Self::device(device, namespace);
222 let mut command = redis::cmd("EVAL");
223 command
224 .arg(
225 r#"
226if redis.call('GET',KEYS[1]) ~= ARGV[1] then return 0 end
227if redis.call('HGET',KEYS[2],ARGV[2]) ~= ARGV[3] then return 0 end
228redis.call('HSET',KEYS[2],ARGV[2],ARGV[4])
229local function less(a,b)
230 local x=a:match('^(%d+)'); local y=b:match('^(%d+)')
231 return #x < #y or (#x == #y and x < y)
232end
233local minimum=ARGV[4]
234for _,cursor in ipairs(redis.call('HVALS',KEYS[2])) do
235 if less(cursor,minimum) then minimum=cursor end
236end
237for _,entry in ipairs(redis.call('XRANGE',KEYS[3],'-',minimum,'COUNT',256)) do
238 redis.call('XDEL',KEYS[3],entry[1])
239end
240return 1
241"#,
242 )
243 .arg(3)
244 .arg(self.key(user_id, &format!("lease:{field}")))
245 .arg(self.key(user_id, "cursors"))
246 .arg(self.key(user_id, "events"))
247 .arg(owner)
248 .arg(field)
249 .arg(previous)
250 .arg(delivery);
251 self.query::<i64>(&command).await.map(|result| result == 1)
252 }
253
254 pub async fn release(&self, user_id: i64, device: &str, namespace: &str, owner: &str) {
255 let mut command = redis::cmd("EVAL");
256 command.arg("if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end")
257 .arg(1).arg(self.key(user_id,&format!("lease:{}",Self::device(device,namespace)))).arg(owner);
258 let _ = self.query::<i64>(&command).await;
259 }
260
261 pub async fn remember_members(&self, organization: &str, users: &[i64]) -> Result<(), String> {
262 if users.is_empty() {
263 return Ok(());
264 }
265 let mut command = redis::cmd("SADD");
266 command
267 .arg(format!("{}:push:org:{organization}", self.prefix))
268 .arg(users);
269 self.query::<i64>(&command).await.map(|_| ())
270 }
271
272 pub async fn known_members(&self, organization: &str) -> Result<Vec<i64>, String> {
273 let mut command = redis::cmd("SMEMBERS");
274 command.arg(format!("{}:push:org:{organization}", self.prefix));
275 self.query(&command).await
276 }
277}
Added lib/actor/Cargo.toml lib/actor/Cargo.toml +16 −0 View lib/actor/Cargo.toml
@@ -0,0 +1,16 @@
1[package]
2name = "actor"
3version.workspace = true
4edition.workspace = true
5publish = false
6description = "Redis Streams 虚拟 actor:目录、租约、节点间 RPC 与事件积压"
7
8[dependencies]
9base64 = { workspace = true }
10cache = { workspace = true }
11config = { workspace = true }
12redis = { workspace = true }
13serde = { workspace = true, features = ["derive"] }
14serde_json = { workspace = true }
15tokio = { workspace = true, features = ["rt", "sync", "time", "macros"] }
16uuid = { workspace = true }
Added lib/config/src/event/actor.rs lib/config/src/event/actor.rs +80 −0 View lib/config/src/event/actor.rs
@@ -0,0 +1,80 @@
1use serde::{Deserialize, Serialize};
2
3pub const DEFAULT_KEY_PREFIX: &str = "event:actor";
4pub const DEFAULT_CALL_TIMEOUT_SECS: u64 = 5;
5pub const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 60;
6pub const DEFAULT_LEASE_TTL_SECS: u64 = 15;
7pub const DEFAULT_MAILBOX_CAPACITY: usize = 1024;
8pub const DEFAULT_INBOX_MAX_LEN: usize = 10_000;
9pub const DEFAULT_READ_COUNT: usize = 64;
10pub const DEFAULT_READ_BLOCK_MS: u64 = 1_000;
11
12/// event 进程的 actor host 参数。`node_id` 为空时回退到 `actor.node` 文件(单机兼容)。
13#[derive(Debug, Clone, Deserialize, Serialize)]
14#[serde(default)]
15pub struct ActorConfig {
16 pub node_id: Option<String>,
17 pub key_prefix: String,
18 pub call_timeout_secs: u64,
19 pub idle_timeout_secs: Option<u64>,
20 pub lease_ttl_secs: u64,
21 pub mailbox_capacity: usize,
22 pub inbox_max_len: usize,
23 pub read_count: usize,
24 pub read_block_ms: u64,
25}
26
27impl ActorConfig {
28 pub fn validate(&self) -> Result<(), String> {
29 if let Some(node_id) = self.node_id.as_deref().filter(|value| !value.is_empty()) {
30 if node_id.len() > 128
31 || !node_id
32 .chars()
33 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'))
34 {
35 return Err(
36 "event.actor.node_id must use letters, digits, `_` or `-` and be at most 128 characters"
37 .to_string(),
38 );
39 }
40 }
41 if self.key_prefix.trim().is_empty() {
42 return Err("event.actor.key_prefix must not be blank".to_string());
43 }
44 for (name, value) in [
45 ("call_timeout_secs", self.call_timeout_secs),
46 ("lease_ttl_secs", self.lease_ttl_secs),
47 ("read_block_ms", self.read_block_ms),
48 ] {
49 if value == 0 {
50 return Err(format!("event.actor.{name} must be greater than zero"));
51 }
52 }
53 if self.idle_timeout_secs == Some(0) {
54 return Err("event.actor.idle_timeout_secs must be greater than zero".to_string());
55 }
56 if self.mailbox_capacity == 0 || self.inbox_max_len == 0 || self.read_count == 0 {
57 return Err(
58 "event.actor.mailbox_capacity/inbox_max_len/read_count must be greater than zero"
59 .to_string(),
60 );
61 }
62 Ok(())
63 }
64}
65
66impl Default for ActorConfig {
67 fn default() -> Self {
68 Self {
69 node_id: None,
70 key_prefix: DEFAULT_KEY_PREFIX.to_string(),
71 call_timeout_secs: DEFAULT_CALL_TIMEOUT_SECS,
72 idle_timeout_secs: Some(DEFAULT_IDLE_TIMEOUT_SECS),
73 lease_ttl_secs: DEFAULT_LEASE_TTL_SECS,
74 mailbox_capacity: DEFAULT_MAILBOX_CAPACITY,
75 inbox_max_len: DEFAULT_INBOX_MAX_LEN,
76 read_count: DEFAULT_READ_COUNT,
77 read_block_ms: DEFAULT_READ_BLOCK_MS,
78 }
79 }
80}
Added lib/config/src/event/consumer.rs lib/config/src/event/consumer.rs +173 −0 View lib/config/src/event/consumer.rs
@@ -0,0 +1,173 @@
1use serde::{Deserialize, Serialize};
2
3pub const DEFAULT_NAME_SERVER: &str = "127.0.0.1:9876";
4pub const DEFAULT_CONSUMER_GROUP: &str = "kenevent";
5pub const DEFAULT_TOPIC_PREFIXES: [&str; 2] = ["gitdata", "kenshell"];
6pub const DEFAULT_CONSUME_TIMEOUT_MS: u64 = 15 * 60 * 1_000;
7pub const DEFAULT_MAX_RECONSUME_TIMES: u32 = 16;
8pub const DEFAULT_PULL_BATCH_SIZE: u32 = 32;
9pub const DEFAULT_PULL_BATCH_SIZE_IN_BYTES: u32 = 256 * 1024;
10pub const DEFAULT_PULL_INTERVAL_MS: u64 = 0;
11pub const MAX_TOPIC_LEN: usize = 127;
12pub const MAX_AREA_LEN: usize = 64;
13
14fn default_name_server() -> String {
15 DEFAULT_NAME_SERVER.to_string()
16}
17
18fn default_consumer_group() -> String {
19 DEFAULT_CONSUMER_GROUP.to_string()
20}
21
22fn default_topic_prefixes() -> Vec<String> {
23 DEFAULT_TOPIC_PREFIXES
24 .iter()
25 .map(|prefix| (*prefix).to_string())
26 .collect()
27}
28
29fn default_consume_timeout_ms() -> u64 {
30 DEFAULT_CONSUME_TIMEOUT_MS
31}
32
33fn default_max_reconsume_times() -> u32 {
34 DEFAULT_MAX_RECONSUME_TIMES
35}
36
37fn default_pull_batch_size() -> u32 {
38 DEFAULT_PULL_BATCH_SIZE
39}
40
41fn default_pull_batch_size_in_bytes() -> u32 {
42 DEFAULT_PULL_BATCH_SIZE_IN_BYTES
43}
44
45fn default_pull_interval_ms() -> u64 {
46 DEFAULT_PULL_INTERVAL_MS
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
50pub struct RocketMqConsumerConfig {
51 #[serde(default = "default_name_server")]
52 pub name_server: String,
53 #[serde(default = "default_consumer_group")]
54 pub consumer_group: String,
55 #[serde(default = "default_topic_prefixes")]
56 pub topic_prefixes: Vec<String>,
57 #[serde(default = "default_consume_timeout_ms")]
58 pub consume_timeout_ms: u64,
59 #[serde(default = "default_max_reconsume_times")]
60 pub max_reconsume_times: u32,
61 #[serde(default = "default_pull_batch_size")]
62 pub pull_batch_size: u32,
63 #[serde(default = "default_pull_batch_size_in_bytes")]
64 pub pull_batch_size_in_bytes: u32,
65 #[serde(default = "default_pull_interval_ms")]
66 pub pull_interval_ms: u64,
67}
68
69impl Default for RocketMqConsumerConfig {
70 fn default() -> Self {
71 Self {
72 name_server: default_name_server(),
73 consumer_group: default_consumer_group(),
74 topic_prefixes: default_topic_prefixes(),
75 consume_timeout_ms: default_consume_timeout_ms(),
76 max_reconsume_times: default_max_reconsume_times(),
77 pull_batch_size: default_pull_batch_size(),
78 pull_batch_size_in_bytes: default_pull_batch_size_in_bytes(),
79 pull_interval_ms: default_pull_interval_ms(),
80 }
81 }
82}
83
84impl RocketMqConsumerConfig {
85 pub fn name_servers(&self) -> Vec<&str> {
86 self.name_server
87 .split([';', ','])
88 .map(str::trim)
89 .filter(|server| !server.is_empty())
90 .collect()
91 }
92
93 pub fn consume_timeout_minutes(&self) -> u64 {
94 self.consume_timeout_ms.div_ceil(60_000).max(1)
95 }
96
97 pub fn validate(&self) -> Result<(), String> {
98 let servers = self.name_servers();
99 if servers.is_empty() {
100 return Err("rocketmq.name_server must not be blank".to_string());
101 }
102 for server in servers {
103 let (host, port) = server.rsplit_once(':').ok_or_else(|| {
104 format!("rocketmq.name_server `{server}` must use the `host:port` form")
105 })?;
106 if host.trim().is_empty() {
107 return Err(format!("rocketmq.name_server `{server}` has an empty host"));
108 }
109 let port = port
110 .parse::<u16>()
111 .map_err(|_| format!("rocketmq.name_server `{server}` has an invalid port"))?;
112 if port == 0 {
113 return Err(format!("rocketmq.name_server `{server}` has a zero port"));
114 }
115 }
116
117 if !is_valid_identifier(&self.consumer_group) {
118 return Err("rocketmq.consumer_group must use letters, digits, `_` or `-`".to_string());
119 }
120
121 if self.topic_prefixes.is_empty() {
122 return Err("rocketmq.topic_prefixes must not be empty".to_string());
123 }
124 for (index, prefix) in self.topic_prefixes.iter().enumerate() {
125 if !DEFAULT_TOPIC_PREFIXES.contains(&prefix.as_str()) {
126 return Err(format!(
127 "unsupported rocketmq.topic_prefixes[{index}]: {prefix}"
128 ));
129 }
130 if !is_valid_identifier(prefix) {
131 return Err(format!(
132 "rocketmq.topic_prefixes[{index}] must use letters, digits, `_` or `-`"
133 ));
134 }
135 if prefix.len() + 1 + MAX_AREA_LEN > MAX_TOPIC_LEN {
136 return Err(format!(
137 "rocketmq.topic_prefixes[{index}] leaves no room for a topic under {MAX_TOPIC_LEN} characters"
138 ));
139 }
140 if self.topic_prefixes[..index].contains(prefix) {
141 return Err(format!(
142 "rocketmq.topic_prefixes[{index}] `{prefix}` is duplicated"
143 ));
144 }
145 }
146
147 if self.consume_timeout_ms == 0 {
148 return Err("rocketmq.consume_timeout_ms must be greater than zero".to_string());
149 }
150 if self.max_reconsume_times == 0 {
151 return Err("rocketmq.max_reconsume_times must be greater than zero".to_string());
152 }
153 if self.max_reconsume_times > i32::MAX as u32 {
154 return Err("rocketmq.max_reconsume_times exceeds i32::MAX".to_string());
155 }
156 if self.pull_batch_size == 0 {
157 return Err("rocketmq.pull_batch_size must be greater than zero".to_string());
158 }
159 if self.pull_batch_size_in_bytes == 0 {
160 return Err("rocketmq.pull_batch_size_in_bytes must be greater than zero".to_string());
161 }
162
163 Ok(())
164 }
165}
166
167fn is_valid_identifier(value: &str) -> bool {
168 !value.is_empty()
169 && value.len() <= MAX_TOPIC_LEN
170 && value
171 .chars()
172 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'))
173}
Added lib/config/src/event/mod.rs lib/config/src/event/mod.rs +56 −0 View lib/config/src/event/mod.rs
@@ -0,0 +1,56 @@
1pub mod actor;
2
3/// event 与平台共用同一个 Redis 连接配置类型(`cache` 连接池的入参)。
4pub mod redis {
5 pub use crate::redis::{RedisConfig, RedisMode};
6}
7
8pub mod consumer;
9pub mod platform;
10pub mod rpc;
11pub mod socket;
12
13use serde::{Deserialize, Serialize};
14
15use crate::ConfigError;
16use crate::redis::RedisConfig;
17use actor::ActorConfig;
18use consumer::RocketMqConsumerConfig;
19use platform::PlatformClientConfig;
20
21use rpc::RpcConfig;
22use socket::SocketConfig;
23
24/// event 子命令的完整配置。
25///
26/// 只包含实时通道自身需要的五类参数:socket 通道、actor/心跳、订阅用 Redis、
27/// RocketMQ 消费、内部 RPC;不读取数据库/S3/业务域 section。
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29#[serde(default)]
30pub struct EventConfig {
31 pub socket: SocketConfig,
32 pub actor: ActorConfig,
33 pub redis: RedisConfig,
34 pub consumer: RocketMqConsumerConfig,
35 pub rpc: RpcConfig,
36 pub platform: PlatformClientConfig,
37}
38
39impl EventConfig {
40 pub fn load() -> Result<Self, ConfigError> {
41 let figment = crate::loader::figment()?;
42 let config: Self = figment.extract_inner("event").map_err(ConfigError::from)?;
43 config.validate().map_err(ConfigError::Validation)?;
44 Ok(config)
45 }
46
47 pub fn validate(&self) -> Result<(), String> {
48 self.socket.validate()?;
49 self.actor.validate()?;
50 self.redis.validate()?;
51 self.consumer.validate()?;
52 self.rpc.validate()?;
53 self.platform.validate()?;
54 Ok(())
55 }
56}
Added lib/config/src/event/platform.rs lib/config/src/event/platform.rs +48 −0 View lib/config/src/event/platform.rs
@@ -0,0 +1,48 @@
1use serde::{Deserialize, Serialize};
2
3/// event 进程访问基础核心平台(serve)的内部接口配置。
4#[derive(Debug, Clone, Deserialize, Serialize)]
5#[serde(default)]
6pub struct PlatformClientConfig {
7 /// 平台内部 RPC 端点,例如 `http://127.0.0.1:7052`。
8 pub rpc_endpoint: String,
9 pub rpc_key: String,
10 pub timeout_secs: u64,
11 /// 平台内部 HTTP 路由前缀,例如 `http://127.0.0.1:7051/api/v1`。
12 pub resource_api_endpoint: String,
13 pub resource_api_service_key: String,
14}
15
16impl PlatformClientConfig {
17 pub fn validate(&self) -> Result<(), String> {
18 if self.rpc_endpoint.trim().is_empty() {
19 return Err("event.platform.rpc_endpoint must not be blank".to_string());
20 }
21 if !(self.rpc_endpoint.starts_with("http://") || self.rpc_endpoint.starts_with("https://"))
22 {
23 return Err("event.platform.rpc_endpoint must be an http(s) URL".to_string());
24 }
25 if self.timeout_secs == 0 {
26 return Err("event.platform.timeout_secs must be greater than zero".to_string());
27 }
28 if !self.resource_api_endpoint.trim().is_empty()
29 && !(self.resource_api_endpoint.starts_with("http://")
30 || self.resource_api_endpoint.starts_with("https://"))
31 {
32 return Err("event.platform.resource_api_endpoint must be an http(s) URL".to_string());
33 }
34 Ok(())
35 }
36}
37
38impl Default for PlatformClientConfig {
39 fn default() -> Self {
40 Self {
41 rpc_endpoint: "http://127.0.0.1:7052".to_string(),
42 rpc_key: String::new(),
43 timeout_secs: 5,
44 resource_api_endpoint: "http://127.0.0.1:7051/api/v1".to_string(),
45 resource_api_service_key: String::new(),
46 }
47 }
48}
Added lib/config/src/event/rpc.rs lib/config/src/event/rpc.rs +63 −0 View lib/config/src/event/rpc.rs
@@ -0,0 +1,63 @@
1use serde::{Deserialize, Serialize};
2
3use crate::secret::SecretMaterial;
4
5pub const DEFAULT_LISTEN_ADDRESS: &str = "127.0.0.1:7032";
6pub const DEFAULT_NONCE_TTL_SECS: u64 = 120;
7pub const DEFAULT_CLOCK_SKEW_SECS: u64 = 30;
8
9/// 内部 RPC 协议参数。身份信封由平台公钥加密、HMAC 签名,event 用私钥解密并验签。
10#[derive(Debug, Clone, Deserialize, Serialize)]
11#[serde(default)]
12pub struct RpcConfig {
13 pub listen_address: String,
14 /// 入站门禁:`x-rpc-key`。至少 32 字符。
15 pub rpc_key: String,
16 pub identity_key_id: String,
17 #[serde(default)]
18 pub identity_private_key: SecretMaterial,
19 pub platform_mac_key_id: String,
20 #[serde(default)]
21 pub platform_mac_key: SecretMaterial,
22 pub nonce_ttl_secs: u64,
23 pub clock_skew_secs: u64,
24 /// 出站调用平台时使用的请求超时。
25 pub call_timeout_secs: u64,
26}
27
28impl RpcConfig {
29 pub fn validate(&self) -> Result<(), String> {
30 if self.listen_address.trim().is_empty() {
31 return Err("event.rpc.listen_address must not be blank".to_string());
32 }
33 if !self.rpc_key.is_empty() && self.rpc_key.len() < 32 {
34 return Err("event.rpc.rpc_key must be at least 32 characters".to_string());
35 }
36 if self.nonce_ttl_secs == 0 {
37 return Err("event.rpc.nonce_ttl_secs must be greater than zero".to_string());
38 }
39 if self.clock_skew_secs == 0 {
40 return Err("event.rpc.clock_skew_secs must be greater than zero".to_string());
41 }
42 if self.call_timeout_secs == 0 {
43 return Err("event.rpc.call_timeout_secs must be greater than zero".to_string());
44 }
45 Ok(())
46 }
47}
48
49impl Default for RpcConfig {
50 fn default() -> Self {
51 Self {
52 listen_address: DEFAULT_LISTEN_ADDRESS.to_string(),
53 rpc_key: String::new(),
54 identity_key_id: String::new(),
55 identity_private_key: SecretMaterial::default(),
56 platform_mac_key_id: String::new(),
57 platform_mac_key: SecretMaterial::default(),
58 nonce_ttl_secs: DEFAULT_NONCE_TTL_SECS,
59 clock_skew_secs: DEFAULT_CLOCK_SKEW_SECS,
60 call_timeout_secs: 5,
61 }
62 }
63}
Added lib/config/src/event/socket.rs lib/config/src/event/socket.rs +84 −0 View lib/config/src/event/socket.rs
@@ -0,0 +1,84 @@
1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5pub const DEFAULT_LISTEN_ADDRESS: &str = "0.0.0.0:7031";
6pub const MIN_HEARTBEAT_INTERVAL_SECS: u64 = 30;
7pub const MAX_HEARTBEAT_GAP_SECS: u64 = 60;
8pub const DEFAULT_HEARTBEAT_TIMEOUT_SECS: u64 = 30;
9pub const DEFAULT_AUTH_TIMEOUT_SECS: u64 = 5;
10pub const DEFAULT_MAX_PAYLOAD: usize = 1_000_000;
11/// KenApi 会话 cookie 名(`session.cookie_name`),event 只读不写。
12pub const DEFAULT_SESSION_COOKIE: &str = "session";
13
14#[derive(Debug, Clone, Deserialize, Serialize)]
15#[serde(default)]
16pub struct SocketConfig {
17 pub listen_address: String,
18 pub heartbeat_interval_secs: u64,
19 pub heartbeat_timeout_secs: u64,
20 pub auth_timeout_secs: u64,
21 pub max_payload: usize,
22 pub session_cookie: String,
23}
24
25impl SocketConfig {
26 pub fn heartbeat_interval(&self) -> Duration {
27 Duration::from_secs(self.heartbeat_interval_secs)
28 }
29
30 pub fn heartbeat_timeout(&self) -> Duration {
31 Duration::from_secs(self.heartbeat_timeout_secs)
32 }
33
34 pub fn auth_timeout(&self) -> Duration {
35 Duration::from_secs(self.auth_timeout_secs)
36 }
37
38 pub fn validate(&self) -> Result<(), String> {
39 if self.listen_address.trim().is_empty() {
40 return Err("event.socket.listen_address must not be blank".to_string());
41 }
42 if self.heartbeat_interval_secs < MIN_HEARTBEAT_INTERVAL_SECS {
43 return Err("event.socket.heartbeat_interval_secs must be at least 30".to_string());
44 }
45 if self.heartbeat_timeout_secs == 0 {
46 return Err(
47 "event.socket.heartbeat_timeout_secs must be greater than zero".to_string(),
48 );
49 }
50 let gap = self
51 .heartbeat_interval_secs
52 .checked_add(self.heartbeat_timeout_secs)
53 .ok_or_else(|| "event.socket heartbeat durations overflow".to_string())?;
54 if gap > MAX_HEARTBEAT_GAP_SECS {
55 return Err(
56 "event.socket heartbeat interval plus timeout must not exceed 60 seconds"
57 .to_string(),
58 );
59 }
60 if self.auth_timeout_secs == 0 {
61 return Err("event.socket.auth_timeout_secs must be greater than zero".to_string());
62 }
63 if self.max_payload == 0 {
64 return Err("event.socket.max_payload must be greater than zero".to_string());
65 }
66 if self.session_cookie.trim().is_empty() {
67 return Err("event.socket.session_cookie must not be blank".to_string());
68 }
69 Ok(())
70 }
71}
72
73impl Default for SocketConfig {
74 fn default() -> Self {
75 Self {
76 listen_address: DEFAULT_LISTEN_ADDRESS.to_string(),
77 heartbeat_interval_secs: MIN_HEARTBEAT_INTERVAL_SECS,
78 heartbeat_timeout_secs: DEFAULT_HEARTBEAT_TIMEOUT_SECS,
79 auth_timeout_secs: DEFAULT_AUTH_TIMEOUT_SECS,
80 max_payload: DEFAULT_MAX_PAYLOAD,
81 session_cookie: DEFAULT_SESSION_COOKIE.to_string(),
82 }
83 }
84}
Modified lib/config/src/internal.rs lib/config/src/internal.rs +58 −0 View lib/config/src/internal.rs
@@ -1,9 +1,13 @@
11use serde::{Deserialize, Serialize};
22
3use crate::secret::SecretMaterial;
4
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
46#[serde(default)]
57pub struct InternalConfig {
68 pub service_key: String,
9 pub rpc: InternalRpcConfig,
10 pub event_client: EventClientConfig,
711}
812
913impl InternalConfig {
@@ -11,6 +15,60 @@
1115 if !self.service_key.is_empty() && self.service_key.len() < 32 {
1216 return Err("internal.service_key must be at least 32 characters".to_string());
1317 }
18 self.rpc.validate()?;
19 self.event_client.validate()?;
20 Ok(())
21 }
22}
23
24/// serve 进程暴露的内部 RPC 服务端参数。
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26#[serde(default)]
27pub struct InternalRpcConfig {
28 pub listen_address: String,
29 pub rpc_key: String,
30}
31
32impl InternalRpcConfig {
33 pub fn validate(&self) -> Result<(), String> {
34 if self.listen_address.trim().is_empty() {
35 return Ok(());
36 }
37 if !self.rpc_key.is_empty() && self.rpc_key.len() < 32 {
38 return Err("internal.rpc.rpc_key must be at least 32 characters".to_string());
39 }
40 Ok(())
41 }
42}
43
44/// serve 进程访问 event 内部 RPC 的客户端参数(含身份信封公钥与 HMAC 密钥)。
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
46#[serde(default)]
47pub struct EventClientConfig {
48 pub endpoint: String,
49 pub rpc_key: String,
50 pub timeout_secs: u64,
51 pub key_id: String,
52 #[serde(default)]
53 pub public_key: SecretMaterial,
54 pub mac_key_id: String,
55 #[serde(default)]
56 pub mac_key: SecretMaterial,
57 pub clock_skew_secs: u64,
58 pub envelope_ttl_secs: u64,
59}
60
61impl EventClientConfig {
62 pub fn validate(&self) -> Result<(), String> {
63 if self.endpoint.trim().is_empty() {
64 return Ok(());
65 }
66 if !(self.endpoint.starts_with("http://") || self.endpoint.starts_with("https://")) {
67 return Err("internal.event_client.endpoint must be an http(s) URL".to_string());
68 }
69 if !self.rpc_key.is_empty() && self.rpc_key.len() < 32 {
70 return Err("internal.event_client.rpc_key must be at least 32 characters".to_string());
71 }
1472 Ok(())
1573 }
1674}
Modified lib/config/src/lib.rs lib/config/src/lib.rs +2 −0 View lib/config/src/lib.rs
@@ -3,6 +3,7 @@
33pub mod chat;
44pub mod database;
55pub mod email;
6pub mod event;
67pub mod git;
78pub mod internal;
89pub mod issues;
@@ -14,6 +15,7 @@
1415pub mod redis;
1516pub mod rocketmq;
1617pub mod s3;
18pub mod secret;
1719pub mod session;
1820pub mod smtp;
1921pub mod space;
Added lib/config/src/secret.rs lib/config/src/secret.rs +58 −0 View lib/config/src/secret.rs
@@ -0,0 +1,58 @@
1use std::fmt;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6/// 密钥材料的三种来源:文件、环境变量、内联(明文 PEM 或 base64)。
7#[derive(Clone, Deserialize, Serialize)]
8#[serde(untagged)]
9pub enum SecretMaterial {
10 File { file: PathBuf },
11 Env { env: String },
12 Inline(String),
13}
14
15impl SecretMaterial {
16 pub fn resolve(&self) -> Result<String, String> {
17 match self {
18 Self::File { file } => std::fs::read_to_string(file).map_err(|error| {
19 format!("secret file {} is not readable: {error}", file.display())
20 }),
21 Self::Env { env } => std::env::var(env)
22 .map_err(|_| format!("secret environment variable {env} is not set")),
23 Self::Inline(text) => Ok(text.trim().to_owned()),
24 }
25 }
26
27 pub fn is_empty(&self) -> bool {
28 match self {
29 Self::Inline(text) => text.trim().is_empty(),
30 _ => false,
31 }
32 }
33}
34
35impl Default for SecretMaterial {
36 fn default() -> Self {
37 Self::Inline(String::new())
38 }
39}
40
41impl fmt::Debug for SecretMaterial {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::File { file } => formatter
45 .debug_struct("SecretMaterial")
46 .field("file", file)
47 .finish_non_exhaustive(),
48 Self::Env { env } => formatter
49 .debug_struct("SecretMaterial")
50 .field("env", env)
51 .finish_non_exhaustive(),
52 Self::Inline(_) => formatter
53 .debug_struct("SecretMaterial")
54 .field("inline", &"<redacted>")
55 .finish(),
56 }
57 }
58}
Added lib/internal/src/client.rs lib/internal/src/client.rs +94 −0 View lib/internal/src/client.rs
@@ -0,0 +1,94 @@
1use std::time::Duration;
2
3use tonic::metadata::{Ascii, MetadataValue};
4use tonic::service::Interceptor as TonicInterceptor;
5use tonic::transport::{Channel, Endpoint};
6use tonic::{Request, Status};
7
8use crate::error::InternalError;
9use crate::interceptor::{RPC_KEY_HEADER, new_request_id};
10
11/// 到对端内部 RPC 的共享连接:统一注入 `x-rpc-key` 与请求号。
12#[derive(Clone)]
13pub struct Connection {
14 channel: Channel,
15 interceptor: RpcInterceptor,
16}
17
18impl Connection {
19 pub fn connect(
20 endpoint: &str,
21 rpc_key: &str,
22 timeout: Duration,
23 ) -> Result<Self, InternalError> {
24 let channel = Endpoint::from_shared(endpoint.to_owned())
25 .map_err(|error| InternalError::Config(format!("invalid rpc endpoint: {error}")))?
26 .connect_timeout(timeout)
27 .timeout(timeout)
28 .connect_lazy();
29 Ok(Self {
30 channel,
31 interceptor: RpcInterceptor::new(rpc_key)?,
32 })
33 }
34
35 pub fn channel(&self) -> Channel {
36 self.channel.clone()
37 }
38
39 pub fn interceptor(&self) -> RpcInterceptor {
40 self.interceptor.clone()
41 }
42}
43
44#[derive(Clone)]
45pub struct RpcInterceptor {
46 key: Option<MetadataValue<Ascii>>,
47 request_id: Option<fn() -> Option<String>>,
48}
49
50impl RpcInterceptor {
51 pub fn new(rpc_key: &str) -> Result<Self, InternalError> {
52 let key =
53 if rpc_key.trim().is_empty() {
54 // 空密钥与对端「拒绝一切」语义一致:这里仍然拒绝,避免静默降级。
55 return Err(InternalError::Config(
56 "internal rpc key must not be blank".to_string(),
57 ));
58 } else {
59 Some(MetadataValue::try_from(rpc_key).map_err(|_| {
60 InternalError::Config("rpc key is not a valid header".to_string())
61 })?)
62 };
63 Ok(Self {
64 key,
65 request_id: None,
66 })
67 }
68
69 pub fn with_request_id(mut self, provider: fn() -> Option<String>) -> Self {
70 self.request_id = Some(provider);
71 self
72 }
73}
74
75impl TonicInterceptor for RpcInterceptor {
76 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
77 if let Some(key) = &self.key {
78 request.metadata_mut().insert(RPC_KEY_HEADER, key.clone());
79 }
80 let request_id = self
81 .request_id
82 .and_then(|provider| provider())
83 .unwrap_or_else(new_request_id);
84 if let Ok(value) = MetadataValue::try_from(request_id) {
85 request
86 .metadata_mut()
87 .insert(crate::interceptor::REQUEST_ID_HEADER, value);
88 }
89 Ok(request)
90 }
91}
92
93/// 请求号提供函数别名,方便 `app` 传 `export::middleware::request_id`。
94pub type RequestIdProvider = fn() -> Option<String>;
Added lib/internal/src/envelope.rs lib/internal/src/envelope.rs +301 −0 View lib/internal/src/envelope.rs
@@ -0,0 +1,301 @@
1use hmac::{Hmac, KeyInit, Mac};
2use serde::{Deserialize, Serialize};
3use sha2::Sha256;
4use tonic::Status;
5
6use crate::error::InternalError;
7use crate::nonce::NonceStore;
8
9pub const AUDIENCE: &str = "event";
10const CLAIMS_VERSION: u8 = 1;
11
12type HmacSha256 = Hmac<Sha256>;
13
14/// 信封内层明文:用户标识 + 权威复核需要的最小字段。
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Claims {
17 pub v: u8,
18 pub aud: String,
19 pub uid: i64,
20 #[serde(default)]
21 pub sv: i64,
22 #[serde(default)]
23 pub sid: String,
24 pub iat: i64,
25 pub exp: i64,
26 pub nonce: String,
27 pub rid: String,
28}
29
30/// 解密 + 验签 + 去重之后的身份;后续权限校验只使用这里的 user_id/security_version。
31#[derive(Debug, Clone)]
32pub struct VerifiedIdentity {
33 pub user_id: i64,
34 pub security_version: i64,
35 pub session_id: String,
36 pub request_id: String,
37 pub key_id: String,
38}
39
40impl VerifiedIdentity {
41 pub fn user_id(&self) -> i64 {
42 self.user_id
43 }
44}
45
46fn now_ms() -> i64 {
47 use std::time::{SystemTime, UNIX_EPOCH};
48 SystemTime::now()
49 .duration_since(UNIX_EPOCH)
50 .map(|duration| duration.as_millis() as i64)
51 .unwrap_or_default()
52}
53
54/// 规范化签名输入:长度前缀避免字段拼接歧义。
55fn mac_input(
56 key_id: &str,
57 ciphertext: &str,
58 mac_key_id: &str,
59 issued_at: i64,
60 expires_at: i64,
61 nonce: &str,
62 audience: &str,
63) -> Vec<u8> {
64 let mut input = Vec::new();
65 for part in [
66 key_id.as_bytes(),
67 ciphertext.as_bytes(),
68 mac_key_id.as_bytes(),
69 &issued_at.to_be_bytes(),
70 &expires_at.to_be_bytes(),
71 nonce.as_bytes(),
72 audience.as_bytes(),
73 ] {
74 input.extend_from_slice(&(part.len() as u32).to_be_bytes());
75 input.extend_from_slice(part);
76 }
77 input
78}
79
80fn compute_mac(key: &[u8], input: &[u8]) -> Result<Vec<u8>, InternalError> {
81 let mut mac = HmacSha256::new_from_slice(key)
82 .map_err(|error| InternalError::Config(format!("invalid mac key: {error}")))?;
83 mac.update(input);
84 Ok(mac.finalize().into_bytes().to_vec())
85}
86
87fn verify_mac(key: &[u8], input: &[u8], presented: &[u8]) -> Result<(), InternalError> {
88 let expected = compute_mac(key, input)?;
89 if crypt::constant_time_eq(&expected, presented) {
90 Ok(())
91 } else {
92 Err(InternalError::Envelope("mac mismatch".to_string()))
93 }
94}
95
96/// 平台侧:用 event 公钥封装用户标识,并用共享密钥做完整性签名。
97pub struct EnvelopeSealer {
98 encryptor: crypt::RsaEncryptor,
99 key_id: String,
100 mac_key_id: String,
101 mac_key: Vec<u8>,
102 ttl_secs: u64,
103 clock_skew_secs: i64,
104 audience: String,
105}
106
107impl EnvelopeSealer {
108 pub fn new(
109 key_id: &str,
110 public_pem: &str,
111 mac_key_id: &str,
112 mac_key: &[u8],
113 ttl_secs: u64,
114 clock_skew_secs: u64,
115 ) -> Result<Self, InternalError> {
116 let encryptor = crypt::RsaEncryptor::from_public_pem(key_id, public_pem)
117 .map_err(|error| InternalError::Config(format!("invalid event public key: {error}")))?;
118 Ok(Self {
119 encryptor,
120 key_id: key_id.to_owned(),
121 mac_key_id: mac_key_id.to_owned(),
122 mac_key: mac_key.to_vec(),
123 ttl_secs: ttl_secs.max(1),
124 clock_skew_secs: clock_skew_secs as i64,
125 audience: AUDIENCE.to_string(),
126 })
127 }
128
129 pub fn seal(
130 &self,
131 user_id: i64,
132 security_version: i64,
133 session_id: &str,
134 request_id: &str,
135 ) -> Result<proto::internal::v1::IdentityEnvelope, InternalError> {
136 let issued_at = now_ms();
137 let expires_at = issued_at + (self.ttl_secs as i64) * 1000;
138 let claims = Claims {
139 v: CLAIMS_VERSION,
140 aud: self.audience.clone(),
141 uid: user_id,
142 sv: security_version,
143 sid: session_id.to_owned(),
144 iat: issued_at,
145 exp: expires_at,
146 nonce: crate::interceptor::new_request_id(),
147 rid: request_id.to_owned(),
148 };
149 let plaintext = serde_json::to_vec(&claims)
150 .map_err(|error| InternalError::Envelope(error.to_string()))?;
151 let ciphertext = crypt::encrypt(&self.encryptor, &plaintext)
152 .map_err(|error| InternalError::Crypt(error.to_string()))?;
153 let mac = compute_mac(
154 &self.mac_key,
155 &mac_input(
156 &self.key_id,
157 &ciphertext,
158 &self.mac_key_id,
159 issued_at,
160 expires_at,
161 &claims.nonce,
162 &self.audience,
163 ),
164 )?;
165 Ok(proto::internal::v1::IdentityEnvelope {
166 key_id: self.key_id.clone(),
167 ciphertext,
168 mac,
169 mac_key_id: self.mac_key_id.clone(),
170 issued_at_unix_ms: issued_at,
171 expires_at_unix_ms: expires_at,
172 nonce: claims.nonce,
173 audience: self.audience.clone(),
174 request_id: request_id.to_owned(),
175 })
176 }
177
178 pub fn clock_skew_secs(&self) -> i64 {
179 self.clock_skew_secs
180 }
181}
182
183/// event 侧:MAC 校验 -> 时间窗 -> nonce 去重 -> 私钥解密 -> claims 校验。
184pub struct EnvelopeVerifier {
185 decryptor: crypt::RsaDecryptor,
186 mac_key_id: String,
187 mac_key: Vec<u8>,
188 clock_skew_secs: i64,
189 nonce_ttl_secs: u64,
190 store: NonceStore,
191}
192
193impl EnvelopeVerifier {
194 pub fn new(
195 key_id: &str,
196 private_pem: &str,
197 mac_key_id: &str,
198 mac_key: &[u8],
199 clock_skew_secs: u64,
200 nonce_ttl_secs: u64,
201 store: NonceStore,
202 ) -> Result<Self, InternalError> {
203 let pair = crypt::RsaKeyPair::from_private_pem(key_id, private_pem).map_err(|error| {
204 InternalError::Config(format!("invalid identity private key: {error}"))
205 })?;
206 let decryptor = crypt::RsaDecryptor::new(vec![pair])
207 .map_err(|error| InternalError::Config(format!("invalid identity key set: {error}")))?;
208 Ok(Self {
209 decryptor,
210 mac_key_id: mac_key_id.to_owned(),
211 mac_key: mac_key.to_vec(),
212 clock_skew_secs: clock_skew_secs as i64,
213 nonce_ttl_secs,
214 store,
215 })
216 }
217
218 pub async fn verify(
219 &self,
220 envelope: &proto::internal::v1::IdentityEnvelope,
221 ) -> Result<VerifiedIdentity, Status> {
222 verified_identity_impl(self, envelope)
223 .await
224 .map_err(InternalError::into_status)
225 }
226}
227
228async fn verified_identity_impl(
229 verifier: &EnvelopeVerifier,
230 envelope: &proto::internal::v1::IdentityEnvelope,
231) -> Result<VerifiedIdentity, InternalError> {
232 if envelope.audience != AUDIENCE {
233 return Err(InternalError::Envelope("audience mismatch".to_string()));
234 }
235 if envelope.mac_key_id != verifier.mac_key_id {
236 return Err(InternalError::Envelope("unknown mac key".to_string()));
237 }
238 let now = now_ms();
239 if envelope.issued_at_unix_ms > now + verifier.clock_skew_secs * 1000 {
240 return Err(InternalError::Envelope(
241 "envelope issued in the future".to_string(),
242 ));
243 }
244 if envelope.expires_at_unix_ms + verifier.clock_skew_secs * 1000 < now {
245 return Err(InternalError::Envelope("envelope expired".to_string()));
246 }
247 verify_mac(
248 &verifier.mac_key,
249 &mac_input(
250 &envelope.key_id,
251 &envelope.ciphertext,
252 &envelope.mac_key_id,
253 envelope.issued_at_unix_ms,
254 envelope.expires_at_unix_ms,
255 &envelope.nonce,
256 &envelope.audience,
257 ),
258 &envelope.mac,
259 )?;
260 if envelope.nonce.trim().is_empty() || envelope.nonce.len() > 128 {
261 return Err(InternalError::Envelope("invalid nonce".to_string()));
262 }
263 if !verifier
264 .store
265 .accept(
266 &crate::nonce::key(&envelope.audience, &envelope.nonce),
267 verifier.nonce_ttl_secs,
268 )
269 .await?
270 {
271 return Err(InternalError::Replay);
272 }
273 let claims_json = crypt::decrypt_string(&verifier.decryptor, &envelope.ciphertext)
274 .map_err(|_| InternalError::Envelope("decryption failed".to_string()))?;
275 let claims: Claims = serde_json::from_str(&claims_json)
276 .map_err(|_| InternalError::Envelope("claims are malformed".to_string()))?;
277 if claims.v != CLAIMS_VERSION {
278 return Err(InternalError::Envelope(
279 "unsupported claims version".to_string(),
280 ));
281 }
282 if claims.aud != AUDIENCE || claims.uid <= 0 {
283 return Err(InternalError::Envelope(
284 "claims audience or user is invalid".to_string(),
285 ));
286 }
287 if claims.exp + verifier.clock_skew_secs * 1000 < now {
288 return Err(InternalError::Envelope("claims expired".to_string()));
289 }
290 Ok(VerifiedIdentity {
291 user_id: claims.uid,
292 security_version: claims.sv,
293 session_id: claims.sid,
294 request_id: if claims.rid.is_empty() {
295 envelope.request_id.clone()
296 } else {
297 claims.rid
298 },
299 key_id: envelope.key_id.clone(),
300 })
301}
Added lib/internal/src/error.rs lib/internal/src/error.rs +39 −0 View lib/internal/src/error.rs
@@ -0,0 +1,39 @@
1use thiserror::Error;
2use tonic::Status;
3
4#[derive(Debug, Error)]
5pub enum InternalError {
6 #[error("internal rpc config error: {0}")]
7 Config(String),
8 #[error("internal rpc crypt error: {0}")]
9 Crypt(String),
10 #[error("identity envelope is invalid: {0}")]
11 Envelope(String),
12 #[error("identity envelope was replayed")]
13 Replay,
14 #[error("redis error: {0}")]
15 Redis(#[from] redis::RedisError),
16 #[error("tonic transport error: {0}")]
17 Transport(#[from] tonic::transport::Error),
18 #[error("invalid address: {0}")]
19 Address(String),
20 #[error("health or reflection service: {0}")]
21 Reflection(String),
22}
23
24impl InternalError {
25 pub fn into_status(self) -> Status {
26 match self {
27 Self::Config(message) => Status::failed_precondition(message),
28 Self::Crypt(message) => Status::unauthenticated(format!("invalid identity: {message}")),
29 Self::Envelope(message) => {
30 Status::unauthenticated(format!("invalid identity: {message}"))
31 }
32 Self::Replay => Status::unauthenticated("identity envelope was replayed"),
33 Self::Redis(_) => Status::unavailable("internal state store is unavailable"),
34 Self::Transport(_) | Self::Reflection(_) | Self::Address(_) => {
35 Status::internal("internal rpc server error")
36 }
37 }
38 }
39}
Added lib/internal/src/health.rs lib/internal/src/health.rs +79 −0 View lib/internal/src/health.rs
@@ -0,0 +1,79 @@
1use std::collections::BTreeMap;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use proto::grpc::health::v1::health_check_response::ServingStatus;
6use proto::grpc::health::v1::health_server::{Health as HealthTrait, HealthServer};
7use proto::grpc::health::v1::{HealthCheckRequest, HealthCheckResponse};
8use tonic::{Request, Response, Status};
9
10pub const HEALTH_SERVICE: &str = "grpc.health.v1.Health";
11pub const REFLECTION_SERVICE: &str = "grpc.reflection.v1.ServerReflection";
12
13/// 标准 gRPC 健康检查:整体("")与每个已注册 service 都返回 SERVING。
14#[derive(Clone)]
15pub struct HealthService {
16 statuses: Arc<BTreeMap<String, i32>>,
17}
18
19impl HealthService {
20 pub fn new(service_names: &[&'static str]) -> Self {
21 let mut statuses = BTreeMap::new();
22 statuses.insert(String::new(), ServingStatus::Serving as i32);
23 for name in service_names {
24 statuses.insert((*name).to_string(), ServingStatus::Serving as i32);
25 }
26 Self {
27 statuses: Arc::new(statuses),
28 }
29 }
30
31 pub fn server(self) -> HealthServer<Self> {
32 HealthServer::new(self)
33 }
34
35 fn status(&self, service: &str) -> i32 {
36 self.statuses
37 .get(service)
38 .copied()
39 .unwrap_or(ServingStatus::ServiceUnknown as i32)
40 }
41}
42
43#[tonic::async_trait]
44impl HealthTrait for HealthService {
45 type WatchStream =
46 Pin<Box<dyn tokio_stream::Stream<Item = Result<HealthCheckResponse, Status>> + Send>>;
47
48 async fn check(
49 &self,
50 request: Request<HealthCheckRequest>,
51 ) -> Result<Response<HealthCheckResponse>, Status> {
52 let service = request.into_inner().service;
53 let status = self.status(&service);
54 if status == ServingStatus::ServiceUnknown as i32 {
55 return Err(Status::not_found("unknown service"));
56 }
57 Ok(Response::new(HealthCheckResponse { status }))
58 }
59
60 async fn watch(
61 &self,
62 request: Request<HealthCheckRequest>,
63 ) -> Result<Response<Self::WatchStream>, Status> {
64 let service = request.into_inner().service;
65 let status = self.status(&service);
66 if status == ServingStatus::ServiceUnknown as i32 {
67 return Err(Status::not_found("unknown service"));
68 }
69 let (tx, rx) = tokio::sync::mpsc::channel(1);
70 tokio::spawn(async move {
71 let _ = tx.send(Ok(HealthCheckResponse { status })).await;
72 // 状态不变:保持流打开直到客户端取消。
73 futures_util::future::pending::<()>().await;
74 });
75 Ok(Response::new(Box::pin(
76 tokio_stream::wrappers::ReceiverStream::new(rx),
77 )))
78 }
79}
Added lib/internal/src/interceptor.rs lib/internal/src/interceptor.rs +116 −0 View lib/internal/src/interceptor.rs
@@ -0,0 +1,116 @@
1use std::sync::Arc;
2
3use tonic::metadata::MetadataMap;
4use tonic::service::Interceptor as TonicInterceptor;
5use tonic::{Request, Status};
6use uuid::Uuid;
7
8pub const RPC_KEY_HEADER: &str = "x-rpc-key";
9pub const REQUEST_ID_HEADER: &str = "x-request-id";
10pub const LEGACY_REQUEST_ID_HEADER: &str = "gitdata-request-id";
11pub const CALLER_SERVICE_HEADER: &str = "gitdata-service-id";
12pub const MAX_REQUEST_ID_LEN: usize = 64;
13
14/// 门禁通过后挂到请求扩展上的元信息;只用于日志与审计。
15#[derive(Debug, Clone, Default)]
16pub struct RequestMeta {
17 pub request_id: String,
18 pub caller_service: String,
19}
20
21impl RequestMeta {
22 pub fn request_id(&self) -> Option<&str> {
23 (!self.request_id.is_empty()).then_some(self.request_id.as_str())
24 }
25}
26
27pub fn validate_request_id(value: &str) -> Option<String> {
28 let trimmed = value.trim();
29 if trimmed.is_empty() || trimmed.len() > MAX_REQUEST_ID_LEN {
30 return None;
31 }
32 trimmed
33 .bytes()
34 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
35 .then(|| trimmed.to_owned())
36}
37
38pub fn request_id_from(metadata: &MetadataMap) -> Option<String> {
39 metadata
40 .get(REQUEST_ID_HEADER)
41 .or_else(|| metadata.get(LEGACY_REQUEST_ID_HEADER))
42 .and_then(|value| value.to_str().ok())
43 .and_then(validate_request_id)
44}
45
46pub fn new_request_id() -> String {
47 Uuid::now_v7().to_string()
48}
49
50/// 共享密钥门禁:校验 `x-rpc-key`,通过后把 `RequestMeta` 挂到请求扩展。
51#[derive(Clone)]
52pub struct Gate {
53 key: Arc<str>,
54}
55
56impl Gate {
57 pub fn new(key: impl Into<Arc<str>>) -> Self {
58 Self { key: key.into() }
59 }
60
61 /// 空密钥会让所有请求通过,所以装配后必须先确认门禁真的能拒绝。
62 pub fn is_armed(&self) -> bool {
63 !self.key.trim().is_empty()
64 }
65}
66
67impl TonicInterceptor for Gate {
68 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
69 let metadata = request.metadata().clone();
70 let request_id = request_id_from(&metadata);
71 if !self.is_armed() {
72 return Err(unauthorized(
73 "rpc gate is not configured",
74 request_id.as_deref(),
75 ));
76 }
77 let presented = metadata
78 .get(RPC_KEY_HEADER)
79 .and_then(|value| value.to_str().ok())
80 .map(str::trim)
81 .unwrap_or_default();
82 if presented.is_empty()
83 || !crypt::constant_time_eq(presented.as_bytes(), self.key.as_bytes())
84 {
85 return Err(unauthorized(
86 "rpc key is missing or invalid",
87 request_id.as_deref(),
88 ));
89 }
90 let meta = RequestMeta {
91 request_id: request_id.unwrap_or_else(new_request_id),
92 caller_service: metadata
93 .get(CALLER_SERVICE_HEADER)
94 .and_then(|value| value.to_str().ok())
95 .map(|value| value.trim().to_owned())
96 .filter(|value| !value.is_empty())
97 .unwrap_or_default(),
98 };
99 request.extensions_mut().insert(meta);
100 Ok(request)
101 }
102}
103
104fn unauthorized(message: &str, request_id: Option<&str>) -> Status {
105 let _ = request_id;
106 Status::unauthenticated(message)
107}
108
109/// 读取门禁挂上的元信息;拦截器已校验密钥,这里只做扩展读取。
110pub fn meta<T>(request: &Request<T>) -> Result<RequestMeta, Status> {
111 request
112 .extensions()
113 .get::<RequestMeta>()
114 .cloned()
115 .ok_or_else(|| Status::unauthenticated("rpc gate did not attach request metadata"))
116}
Added lib/internal/src/lib.rs lib/internal/src/lib.rs +27 −0 View lib/internal/src/lib.rs
@@ -0,0 +1,27 @@
1//! 进程间内部 RPC:门禁、身份信封(加密 + HMAC + 防重放)、服务注册与启动。
2//!
3//! 依赖方向:本 crate 只依赖 `proto/config/crypt/oltp` 等基础设施,
4//! **不依赖任何业务 crate**;业务 crate 实现生成的 service trait,
5//! 由 `app` 在启动时把具体 service 追加到 [`server::InternalRouter`]。
6
7#![allow(clippy::result_large_err)]
8
9pub mod client;
10pub mod envelope;
11pub mod error;
12pub mod health;
13pub mod interceptor;
14pub mod nonce;
15pub mod reflection;
16pub mod server;
17
18pub use client::{Connection, RpcInterceptor};
19pub use envelope::{EnvelopeSealer, EnvelopeVerifier, VerifiedIdentity};
20pub use error::InternalError;
21pub use interceptor::{Gate, RequestMeta};
22pub use nonce::NonceStore;
23pub use server::{InternalRouter, ServerConfig};
24
25pub fn file_descriptor_set() -> &'static [u8] {
26 proto::FILE_DESCRIPTOR_SET
27}
Added lib/internal/src/nonce.rs lib/internal/src/nonce.rs +36 −0 View lib/internal/src/nonce.rs
@@ -0,0 +1,36 @@
1use cache::CacheConn;
2use config::redis::RedisConfig;
3
4use crate::error::InternalError;
5
6#[derive(Clone)]
7pub struct NonceStore {
8 conn: CacheConn,
9}
10
11impl NonceStore {
12 pub async fn connect(config: &RedisConfig, _prefix: &str) -> Result<Self, InternalError> {
13 let conn = CacheConn::init(config)
14 .await
15 .map_err(|error| InternalError::Config(format!("redis: {error}")))?;
16 Ok(Self { conn })
17 }
18
19 /// `SET key 1 NX EX ttl`:第一次出现返回 true,重放返回 false。
20 pub async fn accept(&self, key: &str, ttl_secs: u64) -> Result<bool, InternalError> {
21 let mut conn = self.conn.clone();
22 let result: Option<String> = redis::cmd("SET")
23 .arg(key)
24 .arg("1")
25 .arg("NX")
26 .arg("EX")
27 .arg(ttl_secs.max(1))
28 .query_async(&mut conn)
29 .await?;
30 Ok(result.is_some())
31 }
32}
33
34pub fn key(audience: &str, nonce: &str) -> String {
35 format!("event:identity:nonce:{audience}:{nonce}")
36}
Added lib/internal/src/reflection.rs lib/internal/src/reflection.rs +240 −0 View lib/internal/src/reflection.rs
@@ -0,0 +1,240 @@
1use std::collections::{BTreeMap, HashSet};
2use std::pin::Pin;
3use std::sync::Arc;
4
5use prost::Message;
6use proto::grpc::reflection::v1::server_reflection_request::MessageRequest;
7use proto::grpc::reflection::v1::server_reflection_response::MessageResponse;
8use proto::grpc::reflection::v1::server_reflection_server::{
9 ServerReflection as ReflectionTrait, ServerReflectionServer,
10};
11use proto::grpc::reflection::v1::{
12 ErrorResponse, FileDescriptorResponse, ListServiceResponse, ServerReflectionRequest,
13 ServerReflectionResponse, ServiceResponse,
14};
15use tonic::{Request, Response, Status};
16
17/// 基于 `lib/proto` 的 FILE_DESCRIPTOR_SET 实现标准 gRPC 反射(v1)。
18///
19/// 不引入 tonic-reflection:把 descriptor 解码成 FileDescriptorProto 后自行按
20/// list_services / file_by_filename / file_containing_symbol 响应。
21#[derive(Clone)]
22pub struct ReflectionService {
23 files: Arc<Vec<prost_types::FileDescriptorProto>>,
24 by_name: Arc<BTreeMap<String, usize>>,
25 services: Arc<Vec<String>>,
26}
27
28impl ReflectionService {
29 pub fn new(service_names: &[&'static str]) -> Result<Self, crate::error::InternalError> {
30 let descriptor = prost_types::FileDescriptorSet::decode(proto::FILE_DESCRIPTOR_SET)
31 .map_err(|error| {
32 crate::error::InternalError::Config(format!(
33 "decoding proto descriptor set: {error}"
34 ))
35 })?;
36 let files = descriptor.file;
37 let by_name = files
38 .iter()
39 .enumerate()
40 .filter_map(|(index, file)| file.name.clone().map(|name| (name, index)))
41 .collect();
42 let mut services = Vec::new();
43 for name in service_names {
44 services.push((*name).to_string());
45 }
46 for name in [
47 crate::health::HEALTH_SERVICE,
48 crate::health::REFLECTION_SERVICE,
49 ] {
50 if !services.iter().any(|service| service == name) {
51 services.push(name.to_string());
52 }
53 }
54 Ok(Self {
55 files: Arc::new(files),
56 by_name: Arc::new(by_name),
57 services: Arc::new(services),
58 })
59 }
60
61 pub fn server(self) -> ServerReflectionServer<Self> {
62 ServerReflectionServer::new(self)
63 }
64
65 fn response(
66 &self,
67 request: ServerReflectionRequest,
68 message_response: MessageResponse,
69 ) -> ServerReflectionResponse {
70 ServerReflectionResponse {
71 valid_host: request.host.clone(),
72 original_request: Some(request),
73 message_response: Some(message_response),
74 }
75 }
76
77 fn error(
78 &self,
79 request: ServerReflectionRequest,
80 code: i32,
81 message: &str,
82 ) -> ServerReflectionResponse {
83 self.response(
84 request,
85 MessageResponse::ErrorResponse(ErrorResponse {
86 error_code: code,
87 error_message: message.to_string(),
88 }),
89 )
90 }
91
92 fn file_response(
93 &self,
94 request: ServerReflectionRequest,
95 roots: Vec<usize>,
96 ) -> ServerReflectionResponse {
97 let mut seen = HashSet::new();
98 let mut queue = roots;
99 let mut encoded = Vec::new();
100 while let Some(index) = queue.pop() {
101 if !seen.insert(index) {
102 continue;
103 }
104 let Some(file) = self.files.get(index) else {
105 continue;
106 };
107 encoded.push(file.encode_to_vec());
108 for dependency in &file.dependency {
109 if let Some(next) = self.by_name.get(dependency) {
110 queue.push(*next);
111 }
112 }
113 }
114 self.response(
115 request,
116 MessageResponse::FileDescriptorResponse(FileDescriptorResponse {
117 file_descriptor_proto: encoded,
118 }),
119 )
120 }
121
122 fn handle(&self, request: ServerReflectionRequest) -> ServerReflectionResponse {
123 match request.message_request.clone() {
124 Some(MessageRequest::ListServices(_)) => {
125 let services = self
126 .services
127 .iter()
128 .map(|name| ServiceResponse { name: name.clone() })
129 .collect();
130 self.response(
131 request,
132 MessageResponse::ListServicesResponse(ListServiceResponse {
133 service: services,
134 }),
135 )
136 }
137 Some(MessageRequest::FileByFilename(name)) => {
138 let Some(index) = self.by_name.get(&name).copied() else {
139 return self.error(request, 5, "file not found");
140 };
141 self.file_response(request, vec![index])
142 }
143 Some(MessageRequest::FileContainingSymbol(symbol)) => {
144 match self.find_file_by_symbol(&symbol) {
145 Some(index) => self.file_response(request, vec![index]),
146 None => self.error(request, 5, "symbol not found"),
147 }
148 }
149 Some(MessageRequest::AllExtensionNumbersOfType(_))
150 | Some(MessageRequest::FileContainingExtension(_)) => {
151 self.error(request, 12, "extension reflection is not implemented")
152 }
153 None => self.error(request, 3, "invalid reflection request"),
154 }
155 }
156
157 fn find_file_by_symbol(&self, symbol: &str) -> Option<usize> {
158 let symbol = symbol.trim();
159 for (index, file) in self.files.iter().enumerate() {
160 if file.package.as_deref() == Some(symbol) {
161 return Some(index);
162 }
163 let package = file.package.as_deref().unwrap_or_default();
164 for service in &file.service {
165 let Some(name) = service.name.as_deref() else {
166 continue;
167 };
168 let full = if package.is_empty() {
169 name.to_string()
170 } else {
171 format!("{package}.{name}")
172 };
173 if full == symbol {
174 return Some(index);
175 }
176 if let Some(method) = symbol
177 .strip_prefix(&full)
178 .and_then(|rest| rest.strip_prefix('.'))
179 {
180 if service
181 .method
182 .iter()
183 .any(|item| item.name.as_deref() == Some(method))
184 {
185 return Some(index);
186 }
187 }
188 }
189 for message in &file.message_type {
190 if let Some(name) = message.name.as_deref() {
191 let full = if package.is_empty() {
192 name.to_string()
193 } else {
194 format!("{package}.{name}")
195 };
196 if full == symbol {
197 return Some(index);
198 }
199 }
200 }
201 }
202 None
203 }
204}
205
206#[tonic::async_trait]
207impl ReflectionTrait for ReflectionService {
208 type ServerReflectionInfoStream = Pin<
209 Box<
210 dyn tokio_stream::Stream<Item = Result<ServerReflectionResponse, Status>>
211 + Send
212 + 'static,
213 >,
214 >;
215
216 async fn server_reflection_info(
217 &self,
218 request: Request<tonic::Streaming<ServerReflectionRequest>>,
219 ) -> Result<Response<Self::ServerReflectionInfoStream>, Status> {
220 let mut inbound = request.into_inner();
221 let this = self.clone();
222 let (tx, rx) = tokio::sync::mpsc::channel(8);
223 tokio::spawn(async move {
224 loop {
225 match tokio_stream::StreamExt::next(&mut inbound).await {
226 None => break,
227 Some(Ok(request)) => {
228 if tx.send(Ok(this.handle(request))).await.is_err() {
229 break;
230 }
231 }
232 Some(Err(_)) => break,
233 }
234 }
235 });
236 Ok(Response::new(Box::pin(
237 tokio_stream::wrappers::ReceiverStream::new(rx),
238 )))
239 }
240}
Added lib/internal/src/server.rs lib/internal/src/server.rs +79 −0 View lib/internal/src/server.rs
@@ -0,0 +1,79 @@
1use std::net::SocketAddr;
2
3use tonic::transport::Server;
4use tonic::transport::server::Router;
5use tower::layer::util::{Identity, Stack};
6
7use crate::error::InternalError;
8use crate::health::HealthService;
9use crate::interceptor::Gate;
10use crate::reflection::ReflectionService;
11
12type Layer = Stack<oltp::GrpcRequestLog, Identity>;
13
14/// 统一装配后的内部 RPC 路由:health + reflection + 日志层已就绪,
15/// 业务 crate 只需继续 `add_service`。
16pub type InternalRouter = Router<Layer>;
17
18#[derive(Debug, Clone)]
19pub struct ServerConfig {
20 pub listen_address: String,
21 pub rpc_key: String,
22 pub service_names: Vec<&'static str>,
23}
24
25impl ServerConfig {
26 pub fn new(
27 listen_address: impl Into<String>,
28 rpc_key: impl Into<String>,
29 service_names: Vec<&'static str>,
30 ) -> Self {
31 Self {
32 listen_address: listen_address.into(),
33 rpc_key: rpc_key.into(),
34 service_names,
35 }
36 }
37
38 pub fn gate(&self) -> Gate {
39 Gate::new(self.rpc_key.clone())
40 }
41
42 pub fn listen_address(&self) -> &str {
43 &self.listen_address
44 }
45}
46
47pub fn router(config: &ServerConfig) -> Result<InternalRouter, InternalError> {
48 let gate = Gate::new(config.rpc_key.clone());
49 if !gate.is_armed() {
50 return Err(InternalError::Config(
51 "internal rpc key must not be blank".to_string(),
52 ));
53 }
54 let health = HealthService::new(&config.service_names).server();
55 let reflection = ReflectionService::new(&config.service_names)?.server();
56 Ok(Server::builder()
57 .layer(oltp::GrpcRequestLog)
58 .add_service(health)
59 .add_service(reflection))
60}
61
62pub fn parse_address(listen: &str) -> Result<SocketAddr, InternalError> {
63 listen
64 .parse()
65 .map_err(|error| InternalError::Address(format!("{listen}: {error}")))
66}
67
68/// 启动内部 RPC 服务端;`shutdown` 完成即优雅停止。
69pub async fn serve(
70 router: InternalRouter,
71 listen: &str,
72 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
73) -> Result<(), InternalError> {
74 let address = parse_address(listen)?;
75 router
76 .serve_with_shutdown(address, shutdown)
77 .await
78 .map_err(InternalError::Transport)
79}
Added lib/internal/Cargo.toml lib/internal/Cargo.toml +30 −0 View lib/internal/Cargo.toml
@@ -0,0 +1,30 @@
1[package]
2name = "internal"
3version.workspace = true
4edition.workspace = true
5publish = false
6description = "进程间内部 RPC:门禁、身份信封、服务注册与启动"
7
8[dependencies]
9axum = "0.8"
10cache = { workspace = true }
11config = { workspace = true }
12futures-util = { workspace = true }
13crypt = { workspace = true }
14hmac = { workspace = true }
15http = "1.5"
16oltp = { workspace = true }
17prost = { workspace = true }
18prost-types = { workspace = true }
19proto = { workspace = true }
20redis = { workspace = true }
21serde = { workspace = true, features = ["derive"] }
22serde_json = { workspace = true }
23sha2 = { workspace = true }
24thiserror = { workspace = true }
25tokio = { workspace = true, features = ["rt", "sync", "time", "macros"] }
26tonic = { workspace = true }
27tokio-stream = { version = "0.1", features = ["net"] }
28tower = { workspace = true }
29tracing = { workspace = true }
30uuid = { workspace = true }
Added lib/proto/proto/grpc/health/v1/health.proto lib/proto/proto/grpc/health/v1/health.proto +23 −0 View lib/proto/proto/grpc/health/v1/health.proto
@@ -0,0 +1,23 @@
1syntax = "proto3";
2
3package grpc.health.v1;
4
5// 标准 gRPC 健康检查协议(grpc.health.v1),供 k8s/负载均衡/grpcurl 使用。
6message HealthCheckRequest {
7 string service = 1;
8}
9
10message HealthCheckResponse {
11 enum ServingStatus {
12 UNKNOWN = 0;
13 SERVING = 1;
14 NOT_SERVING = 2;
15 SERVICE_UNKNOWN = 3;
16 }
17 ServingStatus status = 1;
18}
19
20service Health {
21 rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
22 rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
23}
Added lib/proto/proto/grpc/reflection/v1/reflection.proto lib/proto/proto/grpc/reflection/v1/reflection.proto +57 −0 View lib/proto/proto/grpc/reflection/v1/reflection.proto
@@ -0,0 +1,57 @@
1syntax = "proto3";
2
3package grpc.reflection.v1;
4
5// 标准 gRPC 服务端反射协议 v1(与 grpc-proto 的 reflection/v1 保持 wire 兼容)。
6service ServerReflection {
7 rpc ServerReflectionInfo(stream ServerReflectionRequest) returns (stream ServerReflectionResponse);
8}
9
10message ServerReflectionRequest {
11 string host = 1;
12 oneof message_request {
13 string file_by_filename = 3;
14 string file_containing_symbol = 4;
15 ExtensionRequest file_containing_extension = 5;
16 string all_extension_numbers_of_type = 6;
17 string list_services = 7;
18 }
19}
20
21message ExtensionRequest {
22 string containing_type = 1;
23 int32 extension_number = 2;
24}
25
26message ServerReflectionResponse {
27 string valid_host = 1;
28 ServerReflectionRequest original_request = 2;
29 oneof message_response {
30 FileDescriptorResponse file_descriptor_response = 4;
31 ExtensionNumberResponse all_extension_numbers_response = 5;
32 ListServiceResponse list_services_response = 6;
33 ErrorResponse error_response = 7;
34 }
35}
36
37message FileDescriptorResponse {
38 repeated bytes file_descriptor_proto = 1;
39}
40
41message ExtensionNumberResponse {
42 string base_type_name = 1;
43 repeated int32 extension_number = 2;
44}
45
46message ListServiceResponse {
47 repeated ServiceResponse service = 1;
48}
49
50message ServiceResponse {
51 string name = 1;
52}
53
54message ErrorResponse {
55 int32 error_code = 1;
56 string error_message = 2;
57}
Added lib/proto/proto/internal/v1/authorization.proto lib/proto/proto/internal/v1/authorization.proto +65 −0 View lib/proto/proto/internal/v1/authorization.proto
@@ -0,0 +1,65 @@
1syntax = "proto3";
2
3package internal.v1;
4
5import "internal/v1/common.proto";
6
7// 平台面(serve 进程托管):按域做"资源 id -> 目录 -> 权限内核"的解析。
8// 权限规则只有一份(lib/space 的 Access/Capability),这些 RPC 只做域内解析。
9service ChatAuthorizationService {
10 rpc AuthorizeRoom(AuthorizeRoomRequest) returns (AuthorizationDecision);
11}
12
13message AuthorizeRoomRequest {
14 int64 user_id = 1;
15 string room_id = 2;
16 // SUBSCRIBE / DELIVER / MANAGE
17 string action = 3;
18 string request_id = 4;
19}
20
21service IssueAuthorizationService {
22 rpc AuthorizeIssue(AuthorizeIssueRequest) returns (AuthorizationDecision);
23}
24
25message AuthorizeIssueRequest {
26 int64 user_id = 1;
27 string issue_id = 2;
28 string action = 3;
29 string request_id = 4;
30}
31
32service PullAuthorizationService {
33 rpc AuthorizePull(AuthorizePullRequest) returns (AuthorizationDecision);
34}
35
36message AuthorizePullRequest {
37 int64 user_id = 1;
38 string pull_id = 2;
39 string action = 3;
40 string request_id = 4;
41}
42
43service SpaceAuthorizationService {
44 rpc AuthorizeRepo(AuthorizeResourceRequest) returns (AuthorizationDecision);
45 rpc AuthorizeSpace(AuthorizeResourceRequest) returns (AuthorizationDecision);
46 // 内部事件扇出用:列出空间(组织)当前有效成员,不暴露给普通调用方。
47 rpc ListMembers(SpaceMembersRequest) returns (SpaceMembersResponse);
48}
49
50message SpaceMembersRequest {
51 string space_id = 1;
52 string request_id = 2;
53}
54
55message SpaceMembersResponse {
56 repeated int64 user_ids = 1;
57}
58
59message AuthorizeResourceRequest {
60 int64 user_id = 1;
61 string repo_id = 2;
62 string space_id = 3;
63 string action = 4;
64 string request_id = 5;
65}
Added lib/proto/proto/internal/v1/common.proto lib/proto/proto/internal/v1/common.proto +53 −0 View lib/proto/proto/internal/v1/common.proto
@@ -0,0 +1,53 @@
1syntax = "proto3";
2
3package internal.v1;
4
5// 平台 -> event 的身份声明:用户标识用 event 公钥加密(AES-256-GCM 混合信封),
6// 外层 HMAC-SHA256 绑定完整性;event 侧解密 + 验签 + nonce 去重后才可使用其中 uid。
7message IdentityEnvelope {
8 // event 公钥版本(对应 crypt 信封内的 key_id)。
9 string key_id = 1;
10 // crypt 信封 `app1.<base64url>`,明文为 Claims JSON。
11 string ciphertext = 2;
12 // HMAC-SHA256(mac_key, 规范化字段),覆盖 key_id/ciphertext/mac_key_id/iat/exp/nonce/aud。
13 bytes mac = 3;
14 string mac_key_id = 4;
15 int64 issued_at_unix_ms = 5;
16 int64 expires_at_unix_ms = 6;
17 string nonce = 7;
18 // 固定 "event",防止跨服务重放。
19 string audience = 8;
20 string request_id = 9;
21}
22
23// 订阅/投递过滤条件,字段与 MQ 事件属性同名。
24message EventFilter {
25 string user_id = 1;
26 string org_id = 2;
27 string request_id = 3;
28 string repo_id = 4;
29 string repo_name = 5;
30 string room_id = 6;
31 string message_id = 7;
32 string thread_id = 8;
33 string issue_id = 9;
34 string pull_id = 10;
35 int64 timestamp = 11;
36}
37
38// 平台确认过的会话主体。
39message SessionPrincipal {
40 int64 user_id = 1;
41 string session_id = 2;
42 int64 security_version = 3;
43 repeated string scopes = 4;
44 bool mfa_verified = 5;
45 int64 expires_at_unix_ms = 6;
46}
47
48// 资源授权结论。reason 只用于日志,不向浏览器暴露。
49message AuthorizationDecision {
50 bool allowed = 1;
51 string reason = 2;
52 string resource_id = 3;
53}
Added lib/proto/proto/internal/v1/event.proto lib/proto/proto/internal/v1/event.proto +49 −0 View lib/proto/proto/internal/v1/event.proto
@@ -0,0 +1,49 @@
1syntax = "proto3";
2
3package internal.v1;
4
5import "internal/v1/common.proto";
6
7// event 面(event 进程托管):平台带着加密用户标识请求 event 定向投递/控制。
8service EventIngressService {
9 // 向指定用户投递一条事件;event 解密身份、Attest、匹配订阅、二次授权后写入 actor。
10 rpc Publish(PublishRequest) returns (PublishResponse);
11
12 // 撤销用户连接/权限(踢下线、access-revoked 控制包)。
13 rpc Revoke(RevokeRequest) returns (RevokeResponse);
14}
15
16message PublishRequest {
17 IdentityEnvelope identity = 1;
18 string topic = 2;
19 string tag = 3;
20 EventFilter filter = 4;
21 // 与 MQ body 同构的 JSON({id,kind,occurred_at,summary} 或业务 payload)。
22 bytes body = 5;
23 bool personal = 6;
24 string dedup_key = 7;
25 string request_id = 8;
26}
27
28message PublishResponse {
29 uint32 delivered = 1;
30 string scope = 2;
31}
32
33enum RevokeScope {
34 REVOKE_SCOPE_UNSPECIFIED = 0;
35 REVOKE_SCOPE_DEVICE = 1;
36 REVOKE_SCOPE_SESSION = 2;
37 REVOKE_SCOPE_ALL = 3;
38}
39
40message RevokeRequest {
41 IdentityEnvelope identity = 1;
42 RevokeScope scope = 2;
43 string reason = 3;
44 string request_id = 4;
45}
46
47message RevokeResponse {
48 uint32 closed = 1;
49}
Added lib/proto/proto/internal/v1/identity.proto lib/proto/proto/internal/v1/identity.proto +43 −0 View lib/proto/proto/internal/v1/identity.proto
@@ -0,0 +1,43 @@
1syntax = "proto3";
2
3package internal.v1;
4
5import "internal/v1/common.proto";
6
7// 平台面(serve 进程托管):浏览器/服务凭证 -> 会话主体;信封身份 -> 权威复核。
8// 入口由 `x-rpc-key` 把守,不注册到公共网关。
9service IdentityService {
10 // 把会话键(KenApi cookie)解析成会话主体;event 握手与投递期复检使用。
11 rpc Resolve(ResolveIdentityRequest) returns (ResolveIdentityResponse);
12
13 // 信封身份的权威复核:用户仍有效且安全版本一致,否则拒绝。
14 rpc Attest(AttestIdentityRequest) returns (AttestIdentityResponse);
15}
16
17enum CredentialKind {
18 CREDENTIAL_KIND_UNSPECIFIED = 0;
19 // KenApi 服务端会话键(session cookie 的值)。
20 CREDENTIAL_KIND_SESSION = 1;
21}
22
23message ResolveIdentityRequest {
24 CredentialKind kind = 1;
25 string credential = 2;
26 repeated string required_scopes = 3;
27 string request_id = 4;
28}
29
30message ResolveIdentityResponse {
31 SessionPrincipal principal = 1;
32}
33
34message AttestIdentityRequest {
35 int64 user_id = 1;
36 int64 security_version = 2;
37 string request_id = 3;
38}
39
40message AttestIdentityResponse {
41 bool active = 1;
42 SessionPrincipal principal = 2;
43}
Added lib/proto/proto/internal/v1/subscription.proto lib/proto/proto/internal/v1/subscription.proto +64 −0 View lib/proto/proto/internal/v1/subscription.proto
@@ -0,0 +1,64 @@
1syntax = "proto3";
2
3package internal.v1;
4
5import "internal/v1/common.proto";
6
7// event 面:平台代用户管理订阅;与浏览器 socket subscribe 共用同一注册表与同一授权函数。
8service SubscriptionService {
9 rpc Subscribe(SubscribeRequest) returns (SubscribeResponse);
10 rpc Unsubscribe(UnsubscribeRequest) returns (UnsubscribeResponse);
11 rpc ListSubscriptions(ListSubscriptionsRequest) returns (ListSubscriptionsResponse);
12}
13
14message SubscribeRequest {
15 IdentityEnvelope identity = 1;
16 string device_id = 2;
17 string namespace = 3;
18 string topic = 4;
19 string tag = 5;
20 // 可选:幂等替换同一订阅。
21 string id = 6;
22 EventFilter filter = 7;
23 string permission_path = 8;
24 string request_id = 9;
25}
26
27message SubscribeResponse {
28 string id = 1;
29 string scope = 2;
30 int64 created_at_unix_ms = 3;
31}
32
33message UnsubscribeRequest {
34 IdentityEnvelope identity = 1;
35 string device_id = 2;
36 string namespace = 3;
37 string topic = 4;
38 string tag = 5;
39 string id = 6;
40 string request_id = 7;
41}
42
43message UnsubscribeResponse {
44 bool removed = 1;
45}
46
47message SubscriptionView {
48 string id = 1;
49 string topic = 2;
50 string tag = 3;
51 EventFilter filter = 4;
52 string scope = 5;
53 string device_id = 6;
54 string namespace = 7;
55}
56
57message ListSubscriptionsRequest {
58 IdentityEnvelope identity = 1;
59 string device_id = 2;
60}
61
62message ListSubscriptionsResponse {
63 repeated SubscriptionView subscriptions = 1;
64}
Added lib/socket/src/hub/core.rs lib/socket/src/hub/core.rs +213 −0 View lib/socket/src/hub/core.rs
@@ -0,0 +1,213 @@
1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use serde_json::Value;
6use tokio::sync::{Mutex, Notify, mpsc, oneshot, watch};
7use tokio::task::JoinSet;
8
9use crate::backend::{ConnectionPhase, SessionHandle, SocketBackend};
10
11use super::super::session::Input;
12use super::super::{EmitPacket, SocketEvent, SocketOptions, SocketPayload};
13
14const EVENT_CAPACITY: usize = 1024;
15
16#[derive(Clone)]
17pub struct SocketHub {
18 pub(super) inner: Arc<Inner>,
19}
20
21pub(super) struct Inner {
22 pub(super) options: SocketOptions,
23 pub(super) backend: Arc<dyn SocketBackend>,
24 pub(super) events: mpsc::Sender<SocketEvent>,
25 pub(super) sessions: Mutex<HashMap<String, mpsc::Sender<Input>>>,
26 pub(super) sockets: Mutex<HashMap<i64, UserSockets>>,
27 pub(crate) shutdown: watch::Sender<bool>,
28 pub(super) transports: Mutex<JoinSet<()>>,
29}
30
31#[derive(Default)]
32pub(super) struct UserSockets {
33 pub(super) namespaces: HashMap<String, HashMap<String, mpsc::Sender<Input>>>,
34 pub(super) changed: Arc<Notify>,
35}
36
37impl SocketHub {
38 pub fn new(
39 options: SocketOptions,
40 backend: Arc<dyn SocketBackend>,
41 ) -> Result<(Self, mpsc::Receiver<SocketEvent>), String> {
42 options.validate()?;
43 let (events, receiver) = mpsc::channel(EVENT_CAPACITY);
44 let hub = Self {
45 inner: Arc::new(Inner {
46 options,
47 backend,
48 events,
49 sessions: Mutex::new(HashMap::new()),
50 sockets: Mutex::new(HashMap::new()),
51 shutdown: watch::channel(false).0,
52 transports: Mutex::new(JoinSet::new()),
53 }),
54 };
55 Ok((hub, receiver))
56 }
57
58 pub fn shutdown(&self) {
59 self.inner.shutdown.send_replace(true);
60 }
61
62 pub fn shutdown_receiver(&self) -> watch::Receiver<bool> {
63 self.inner.shutdown.subscribe()
64 }
65
66 pub async fn emit(
67 &self,
68 user_id: i64,
69 namespace: &str,
70 event: &str,
71 args: Vec<Value>,
72 ) -> usize {
73 self.emit_binary(user_id, namespace, event, args, Vec::new())
74 .await
75 }
76
77 pub async fn emit_binary(
78 &self,
79 user_id: i64,
80 namespace: &str,
81 event: &str,
82 args: Vec<Value>,
83 binaries: Vec<Vec<u8>>,
84 ) -> usize {
85 let packet = EmitPacket {
86 namespace: namespace_key(namespace),
87 event: event.to_string(),
88 args,
89 binaries,
90 };
91 let namespace = packet.namespace.clone();
92 self.deliver(user_id, &namespace, packet).await
93 }
94
95 pub async fn emit_with_ack(
96 &self,
97 user_id: i64,
98 namespace: &str,
99 event: &str,
100 args: Vec<Value>,
101 timeout: Duration,
102 ) -> Result<SocketPayload, String> {
103 let deadline = tokio::time::Instant::now() + timeout;
104 let packet = EmitPacket {
105 namespace: namespace_key(namespace),
106 event: event.to_string(),
107 args,
108 binaries: Vec::new(),
109 };
110 let inputs = self.inputs(user_id, &packet.namespace).await;
111 if inputs.is_empty() {
112 return Err("no connected socket".to_string());
113 }
114 let mut tasks = tokio::task::JoinSet::new();
115 for input in inputs {
116 let packet = packet.clone();
117 tasks.spawn(async move {
118 tokio::time::timeout_at(deadline, async {
119 let (ack_tx, ack_rx) = oneshot::channel();
120 input
121 .send(Input::Emit {
122 packet,
123 ack: Some((ack_tx, deadline)),
124 accepted: None,
125 })
126 .await
127 .map_err(|_| "socket closed")?;
128 ack_rx.await.map_err(|_| "socket closed")
129 })
130 .await
131 });
132 }
133 while let Some(result) = tasks.join_next().await {
134 if let Ok(Ok(Ok(payload))) = result {
135 tasks.abort_all();
136 return Ok(payload);
137 }
138 }
139 Err("ack timeout or socket closed".to_string())
140 }
141
142 pub async fn wake_user(&self, user_id: i64) {
143 if let Some(user) = self.inner.sockets.lock().await.get(&user_id) {
144 user.changed.notify_waiters();
145 }
146 }
147
148 pub async fn notification(&self, user_id: i64) -> Arc<Notify> {
149 self.inner
150 .sockets
151 .lock()
152 .await
153 .entry(user_id)
154 .or_default()
155 .changed
156 .clone()
157 }
158
159 /// 聊天连接生命周期上报(Open / Heartbeat / Close)。
160 ///
161 /// 失败只记日志:在线态是旁路信息,不能因为平台不可用就把 socket 踢掉。
162 pub async fn report_connection(&self, phase: ConnectionPhase, user_id: i64, session: &str) {
163 let _ = tokio::time::timeout(
164 self.inner.options.auth_timeout,
165 self.inner
166 .backend
167 .report_connection(phase, user_id, session),
168 )
169 .await;
170 }
171
172 /// 关闭某用户的全部在线会话(踢下线 / 权限撤销)。
173 pub async fn close_user(&self, user_id: i64) -> usize {
174 let sockets = self.inner.sockets.lock().await;
175 let mut closed = 0;
176 if let Some(user) = sockets.get(&user_id) {
177 for entries in user.namespaces.values() {
178 for input in entries.values() {
179 if input.try_send(Input::Close).is_ok() {
180 closed += 1;
181 }
182 }
183 }
184 }
185 closed
186 }
187
188 pub(crate) fn backend(&self) -> &Arc<dyn SocketBackend> {
189 &self.inner.backend
190 }
191
192 pub(crate) fn session_handle(&self, input: mpsc::Sender<Input>) -> SessionHandle {
193 SessionHandle::new(input)
194 }
195
196 pub(crate) fn options(&self) -> &SocketOptions {
197 &self.inner.options
198 }
199
200 pub(crate) fn events(&self) -> &mpsc::Sender<SocketEvent> {
201 &self.inner.events
202 }
203}
204
205pub(super) fn namespace_key(namespace: &str) -> String {
206 if namespace.is_empty() {
207 "/".to_string()
208 } else if namespace.starts_with('/') {
209 namespace.to_string()
210 } else {
211 format!("/{namespace}")
212 }
213}
Added lib/socket/src/hub/mod.rs lib/socket/src/hub/mod.rs +4 −0 View lib/socket/src/hub/mod.rs
@@ -0,0 +1,4 @@
1mod core;
2mod registry;
3
4pub use core::SocketHub;
Added lib/socket/src/hub/registry.rs lib/socket/src/hub/registry.rs +151 −0 View lib/socket/src/hub/registry.rs
@@ -0,0 +1,151 @@
1use tokio::sync::mpsc;
2
3use crate::backend::SocketError;
4
5use super::super::EmitPacket;
6use super::super::session::{self, Input, Output};
7use super::core::{SocketHub, namespace_key};
8
9const INPUT_CAPACITY: usize = 256;
10
11impl SocketHub {
12 pub(crate) async fn spawn_transport(
13 &self,
14 future: impl std::future::Future<Output = ()> + Send + 'static,
15 ) {
16 let mut transports = self.inner.transports.lock().await;
17 while let Some(result) = transports.try_join_next() {
18 if let Err(error) = result {
19 eprintln!("kenevent: WebSocket task failed: {error}");
20 }
21 }
22 transports.spawn(future);
23 }
24
25 pub(crate) async fn finish_transports(&self) {
26 let mut transports = self.inner.transports.lock().await;
27 if tokio::time::timeout(std::time::Duration::from_secs(5), async {
28 while transports.join_next().await.is_some() {}
29 })
30 .await
31 .is_err()
32 {
33 transports.abort_all();
34 while transports.join_next().await.is_some() {}
35 }
36 }
37
38 pub(crate) async fn authenticate_token(&self, token: &str) -> Result<i64, SocketError> {
39 tokio::time::timeout(
40 self.inner.options.auth_timeout,
41 self.inner.backend.authenticate(token),
42 )
43 .await
44 .map_err(|_| SocketError::Unavailable("authentication timed out".to_string()))?
45 }
46
47 pub(crate) async fn create_session(
48 &self,
49 sid: String,
50 user_id: i64,
51 token: String,
52 output: Output,
53 send_open: bool,
54 ) -> mpsc::Sender<Input> {
55 let (input, receiver) = mpsc::channel(INPUT_CAPACITY);
56 self.inner
57 .sessions
58 .lock()
59 .await
60 .insert(sid.clone(), input.clone());
61
62 let hub = self.clone();
63 let session_input = input.clone();
64 tokio::spawn(async move {
65 session::run(
66 hub,
67 sid,
68 user_id,
69 token,
70 session_input,
71 receiver,
72 output,
73 send_open,
74 )
75 .await;
76 });
77
78 input
79 }
80
81 pub(crate) async fn remove_session(&self, sid: &str) {
82 self.inner.sessions.lock().await.remove(sid);
83 }
84
85 pub(crate) async fn input(&self, sid: &str) -> Option<mpsc::Sender<Input>> {
86 self.inner.sessions.lock().await.get(sid).cloned()
87 }
88
89 pub(crate) async fn register(
90 &self,
91 user_id: i64,
92 namespace: &str,
93 session_id: String,
94 input: mpsc::Sender<Input>,
95 ) {
96 self.inner
97 .sockets
98 .lock()
99 .await
100 .entry(user_id)
101 .or_default()
102 .namespaces
103 .entry(namespace_key(namespace))
104 .or_default()
105 .insert(session_id, input);
106 }
107
108 pub(crate) async fn unregister(&self, user_id: i64, namespace: &str, session_id: &str) {
109 let mut sockets = self.inner.sockets.lock().await;
110 if let Some(user) = sockets.get_mut(&user_id) {
111 let key = namespace_key(namespace);
112 if let Some(entries) = user.namespaces.get_mut(&key) {
113 entries.remove(session_id);
114 if entries.is_empty() {
115 user.namespaces.remove(&key);
116 }
117 }
118 if user.namespaces.is_empty() {
119 sockets.remove(&user_id);
120 }
121 }
122 }
123
124 pub(super) async fn inputs(&self, user_id: i64, namespace: &str) -> Vec<mpsc::Sender<Input>> {
125 self.inner
126 .sockets
127 .lock()
128 .await
129 .get(&user_id)
130 .and_then(|user| user.namespaces.get(namespace))
131 .map(|entries| entries.values().cloned().collect())
132 .unwrap_or_default()
133 }
134
135 pub(super) async fn deliver(&self, user_id: i64, namespace: &str, packet: EmitPacket) -> usize {
136 let mut delivered = 0;
137 for input in self.inputs(user_id, namespace).await {
138 if input
139 .try_send(Input::Emit {
140 packet: packet.clone(),
141 ack: None,
142 accepted: None,
143 })
144 .is_ok()
145 {
146 delivered += 1;
147 }
148 }
149 delivered
150 }
151}
Added lib/socket/src/session/events.rs lib/socket/src/session/events.rs +206 −0 View lib/socket/src/session/events.rs
@@ -0,0 +1,206 @@
1use serde_json::Value;
2use tokio::sync::oneshot;
3
4use super::io::Input;
5use super::state::Session;
6use crate::packet::{self, Kind, Packet as SocketPacket};
7use crate::{EmitPacket, SocketEvent, SocketPayload};
8
9const RESERVED_EVENTS: [&str; 6] = [
10 "connect",
11 "connect_error",
12 "disconnect",
13 "disconnecting",
14 "newListener",
15 "removeListener",
16];
17
18impl Session {
19 pub(super) fn dispatch(&mut self, packet: SocketPacket, binaries: Vec<Vec<u8>>) -> bool {
20 let mut values = match packet.data {
21 Value::Array(values) => values.into_iter(),
22 _ => return false,
23 };
24 let Some(Value::String(event)) = values.next() else {
25 return false;
26 };
27 if RESERVED_EVENTS.contains(&event.as_str()) {
28 return false;
29 }
30
31 let session_id = self
32 .namespaces
33 .get(&packet.namespace)
34 .cloned()
35 .unwrap_or_default();
36 let ack = packet.id.map(|id| {
37 let (sender, receiver) = oneshot::channel::<SocketPayload>();
38 let input = self.input.clone();
39 let namespace = packet.namespace.clone();
40 tokio::spawn(async move {
41 if let Ok(Ok(payload)) =
42 tokio::time::timeout(std::time::Duration::from_secs(30), receiver).await
43 {
44 let _ = tokio::time::timeout(
45 std::time::Duration::from_secs(5),
46 input.send(Input::Ack {
47 namespace,
48 id,
49 payload,
50 }),
51 )
52 .await;
53 }
54 });
55 sender
56 });
57
58 let event = SocketEvent {
59 user_id: self.user_id,
60 device_id: self.device_id.clone(),
61 access_token: self.access_token.clone(),
62 namespace: packet.namespace,
63 session_id,
64 event,
65 data: values.collect(),
66 binaries,
67 ack,
68 };
69 self.hub.events().try_send(event).is_ok()
70 }
71
72 pub(super) fn handle_binary(&mut self, data: &[u8]) -> bool {
73 let mut overflow = false;
74 {
75 let Some(pending) = self.pending_binary.as_mut() else {
76 return true;
77 };
78 match pending.bytes.checked_add(data.len()) {
79 Some(total) if total <= self.max_payload => {
80 pending.bytes = total;
81 pending.data.push(data.to_vec());
82 }
83 _ => overflow = true,
84 }
85 }
86
87 if overflow {
88 self.pending_binary = None;
89 return false;
90 }
91
92 let complete = self
93 .pending_binary
94 .as_ref()
95 .is_some_and(|pending| pending.data.len() == pending.packet.attachments);
96 if !complete {
97 return true;
98 }
99
100 let pending = self.pending_binary.take().expect("pending binary packet");
101 match pending.packet.kind {
102 Kind::BinaryEvent => return self.dispatch(pending.packet, pending.data),
103 Kind::BinaryAck => {
104 let payload = payload(pending.packet.data, pending.data);
105 self.resolve_ack(&pending.packet.namespace, pending.packet.id, payload);
106 }
107 _ => {}
108 }
109 true
110 }
111
112 pub(super) fn resolve_ack(&mut self, namespace: &str, id: Option<u64>, payload: SocketPayload) {
113 let Some(id) = id else {
114 return;
115 };
116 if self
117 .pending_acks
118 .get(&id)
119 .is_some_and(|(pending_namespace, deadline, _)| {
120 pending_namespace == namespace && *deadline > tokio::time::Instant::now()
121 })
122 {
123 if let Some((_, _, sender)) = self.pending_acks.remove(&id) {
124 let _ = sender.send(payload);
125 }
126 }
127 }
128
129 pub(super) async fn emit(
130 &mut self,
131 packet: EmitPacket,
132 ack: Option<(oneshot::Sender<SocketPayload>, tokio::time::Instant)>,
133 ) -> bool {
134 if !self.namespaces.contains_key(&packet.namespace) {
135 return true;
136 }
137 if ack.as_ref().is_some_and(|(sender, deadline)| {
138 sender.is_closed() || *deadline <= tokio::time::Instant::now()
139 }) {
140 return true;
141 }
142
143 let id = if ack.is_some() {
144 let id = self.next_ack;
145 self.next_ack = self.next_ack.wrapping_add(1);
146 Some(id)
147 } else {
148 None
149 };
150 if let (Some(id), Some((ack, deadline))) = (id, ack) {
151 self.pending_acks
152 .insert(id, (packet.namespace.clone(), deadline, ack));
153 }
154
155 let mut data = Vec::with_capacity(packet.args.len() + 1);
156 data.push(Value::String(packet.event));
157 data.extend(packet.args);
158
159 let text = if packet.binaries.is_empty() {
160 packet::encode_event(&packet.namespace, id, &data)
161 } else {
162 packet::encode_binary_event(&packet.namespace, id, &data, packet.binaries.len())
163 };
164 let mut packets = Vec::with_capacity(packet.binaries.len() + 1);
165 packets.push(crate::engine::text(text));
166 packets.extend(packet.binaries.into_iter().map(crate::engine::binary));
167
168 let sent = self.output.send(packets).await;
169 if !sent {
170 if let Some(id) = id {
171 self.pending_acks.remove(&id);
172 }
173 }
174 sent
175 }
176
177 pub(super) async fn send_ack(
178 &mut self,
179 namespace: String,
180 id: u64,
181 payload: SocketPayload,
182 ) -> bool {
183 if !self.namespaces.contains_key(&namespace) {
184 return true;
185 }
186
187 let SocketPayload { data, binaries } = payload;
188 let text = if binaries.is_empty() {
189 packet::encode_ack(&namespace, id, &data)
190 } else {
191 packet::encode_binary_ack(&namespace, id, &data, binaries.len())
192 };
193 let mut packets = Vec::with_capacity(binaries.len() + 1);
194 packets.push(crate::engine::text(text));
195 packets.extend(binaries.into_iter().map(crate::engine::binary));
196 self.output.send(packets).await
197 }
198}
199
200pub(super) fn payload(data: Value, binaries: Vec<Vec<u8>>) -> SocketPayload {
201 let data = match data {
202 Value::Array(values) => values,
203 _ => Vec::new(),
204 };
205 SocketPayload { data, binaries }
206}
Added lib/socket/src/session/io.rs lib/socket/src/session/io.rs +135 −0 View lib/socket/src/session/io.rs
@@ -0,0 +1,135 @@
1use std::collections::VecDeque;
2
3use tokio::sync::{mpsc, oneshot};
4
5use crate::Frame;
6use crate::engine::{self, Packet as EnginePacket};
7
8use super::super::{EmitPacket, SocketPayload};
9
10const MAX_POLL_QUEUE: usize = 1024;
11
12pub(crate) enum Input {
13 Packets(Vec<EnginePacket>),
14 Poll(oneshot::Sender<Vec<EnginePacket>>),
15 Upgrade {
16 tx: mpsc::Sender<Vec<Frame>>,
17 result: oneshot::Sender<bool>,
18 },
19 Emit {
20 packet: EmitPacket,
21 ack: Option<(oneshot::Sender<SocketPayload>, tokio::time::Instant)>,
22 accepted: Option<oneshot::Sender<bool>>,
23 },
24 Ack {
25 namespace: String,
26 id: u64,
27 payload: SocketPayload,
28 },
29 Close,
30}
31
32pub(crate) enum Output {
33 Polling {
34 queue: VecDeque<EnginePacket>,
35 waiter: Option<oneshot::Sender<Vec<EnginePacket>>>,
36 },
37 WebSocket {
38 tx: mpsc::Sender<Vec<Frame>>,
39 },
40}
41
42impl Output {
43 pub(crate) fn polling() -> Self {
44 Self::Polling {
45 queue: VecDeque::new(),
46 waiter: None,
47 }
48 }
49
50 pub(crate) fn websocket(tx: mpsc::Sender<Vec<Frame>>) -> Self {
51 Self::WebSocket { tx }
52 }
53
54 pub(super) async fn send(&mut self, packets: Vec<EnginePacket>) -> bool {
55 if packets.is_empty() {
56 return true;
57 }
58
59 match self {
60 Self::Polling { queue, waiter } => {
61 if queue.len() + packets.len() > MAX_POLL_QUEUE {
62 return false;
63 }
64 queue.extend(packets);
65 if let Some(waiter) = waiter.take() {
66 let packets = queue.drain(..).collect();
67 if let Err(packets) = waiter.send(packets) {
68 queue.extend(packets);
69 }
70 }
71 true
72 }
73 Self::WebSocket { tx } => {
74 let frames = engine::encode_frames(&packets);
75 if frames.is_empty() {
76 return true;
77 }
78 tx.try_send(frames).is_ok()
79 }
80 }
81 }
82
83 pub(super) fn poll(&mut self, responder: oneshot::Sender<Vec<EnginePacket>>) -> bool {
84 match self {
85 Self::Polling { queue, waiter } => {
86 if queue.is_empty() {
87 if let Some(previous) = waiter.replace(responder) {
88 let _ = previous.send(Vec::new());
89 }
90 } else {
91 let packets = queue.drain(..).collect();
92 if let Err(packets) = responder.send(packets) {
93 queue.extend(packets);
94 }
95 }
96 true
97 }
98 Self::WebSocket { .. } => {
99 let _ = responder.send(vec![EnginePacket::Noop]);
100 true
101 }
102 }
103 }
104
105 pub(super) async fn upgrade(&mut self, tx: mpsc::Sender<Vec<Frame>>) -> bool {
106 let pending = match self {
107 Self::Polling { queue, waiter } => {
108 if let Some(waiter) = waiter.take() {
109 let _ = waiter.send(vec![EnginePacket::Noop]);
110 }
111 queue.drain(..).collect::<Vec<_>>()
112 }
113 Self::WebSocket { .. } => return false,
114 };
115
116 *self = Self::WebSocket { tx };
117 self.send(pending).await
118 }
119
120 pub(super) async fn close(&mut self) -> bool {
121 match self {
122 Self::Polling { queue, waiter } => {
123 queue.push_back(EnginePacket::Close);
124 if let Some(waiter) = waiter.take() {
125 let packets = queue.drain(..).collect();
126 if let Err(packets) = waiter.send(packets) {
127 queue.extend(packets);
128 }
129 }
130 true
131 }
132 Self::WebSocket { tx } => tx.try_send(vec![Frame::Text("1".to_string())]).is_ok(),
133 }
134 }
135}
Added lib/socket/src/session/mod.rs lib/socket/src/session/mod.rs +7 −0 View lib/socket/src/session/mod.rs
@@ -0,0 +1,7 @@
1mod events;
2mod io;
3mod run;
4mod state;
5
6pub(crate) use io::{Input, Output};
7pub(crate) use run::run;
Added lib/socket/src/session/run.rs lib/socket/src/session/run.rs +100 −0 View lib/socket/src/session/run.rs
@@ -0,0 +1,100 @@
1use std::collections::HashMap;
2use std::time::Instant;
3
4use tokio::sync::mpsc;
5use tokio::time::{MissedTickBehavior, interval};
6
7use super::io::{Input, Output};
8use super::state::Session;
9use crate::SocketHub;
10use crate::engine::{self, Packet as EnginePacket};
11
12pub(crate) async fn run(
13 hub: SocketHub,
14 sid: String,
15 user_id: i64,
16 access_token: String,
17 input: mpsc::Sender<Input>,
18 mut receiver: mpsc::Receiver<Input>,
19 output: Output,
20 send_open: bool,
21) {
22 let options = hub.options().clone();
23 let mut shutdown = hub.shutdown_receiver();
24 let mut session = Session {
25 hub,
26 sid,
27 user_id,
28 access_token,
29 device_id: String::new(),
30 deliveries: HashMap::new(),
31 input,
32 output,
33 namespaces: HashMap::new(),
34 pending_binary: None,
35 pending_acks: HashMap::new(),
36 next_ack: 0,
37 last_pong: Instant::now(),
38 last_actor_heartbeat: Instant::now(),
39 max_payload: options.max_payload,
40 auth_deadline: (user_id == 0).then(|| tokio::time::Instant::now() + options.auth_timeout),
41 };
42
43 if send_open {
44 let open = engine::open(
45 &session.sid,
46 options.heartbeat_interval,
47 options.heartbeat_timeout,
48 options.max_payload,
49 &[],
50 );
51 if !session.output.send(vec![open]).await {
52 session.cleanup().await;
53 return;
54 }
55 }
56
57 let mut heartbeat = interval(options.heartbeat_interval);
58 heartbeat.set_missed_tick_behavior(MissedTickBehavior::Delay);
59 heartbeat.tick().await;
60 let mut maintenance = interval(std::time::Duration::from_secs(1));
61
62 loop {
63 if *shutdown.borrow() {
64 break;
65 }
66 tokio::select! {
67 _ = shutdown.changed() => break,
68 _ = maintenance.tick() => {
69 let now = tokio::time::Instant::now();
70 if session.auth_deadline.is_some_and(|deadline| now >= deadline) {
71 break;
72 }
73 session.pending_acks.retain(|_, (_, deadline, sender)| *deadline > now && !sender.is_closed());
74 }
75 incoming = receiver.recv() => {
76 let Some(input) = incoming else {
77 break;
78 };
79 if !session.handle(input).await {
80 break;
81 }
82 }
83 _ = heartbeat.tick() => {
84 let elapsed = session.last_pong.elapsed();
85 if elapsed >= options.heartbeat_interval + options.heartbeat_timeout {
86 break;
87 }
88 if !session
89 .output
90 .send(vec![EnginePacket::Ping(String::new())])
91 .await
92 {
93 break;
94 }
95 }
96 }
97 }
98
99 session.cleanup().await;
100}
Added lib/socket/src/session/state.rs lib/socket/src/session/state.rs +343 −0 View lib/socket/src/session/state.rs
@@ -0,0 +1,343 @@
1use std::collections::HashMap;
2use std::time::Instant;
3
4use tokio::sync::{mpsc, oneshot};
5use uuid::Uuid;
6
7use super::events::payload;
8use super::io::{Input, Output};
9use crate::backend::{ConnectedSession, ConnectionPhase, DeliveryContext};
10use crate::engine::{self, Data, Packet as EnginePacket};
11use crate::packet::{self, Kind, Packet as SocketPacket};
12use crate::{SocketHub, SocketPayload};
13
14pub(super) struct Session {
15 pub(super) hub: SocketHub,
16 pub(super) sid: String,
17 pub(super) user_id: i64,
18 pub(super) access_token: String,
19 pub(super) device_id: String,
20 pub(super) deliveries: HashMap<String, (oneshot::Sender<()>, tokio::task::JoinHandle<()>)>,
21 pub(super) input: mpsc::Sender<Input>,
22 pub(super) output: Output,
23 pub(super) namespaces: HashMap<String, String>,
24 pub(super) pending_binary: Option<PendingBinary>,
25 pub(super) pending_acks:
26 HashMap<u64, (String, tokio::time::Instant, oneshot::Sender<SocketPayload>)>,
27 pub(super) next_ack: u64,
28 pub(super) last_pong: Instant,
29 pub(super) last_actor_heartbeat: Instant,
30 pub(super) max_payload: usize,
31 pub(super) auth_deadline: Option<tokio::time::Instant>,
32}
33
34pub(super) struct PendingBinary {
35 pub(super) packet: SocketPacket,
36 pub(super) data: Vec<Vec<u8>>,
37 pub(super) bytes: usize,
38}
39
40impl Session {
41 pub(super) async fn handle(&mut self, input: Input) -> bool {
42 match input {
43 Input::Packets(packets) => {
44 for packet in packets {
45 if !self.handle_packet(packet).await {
46 return false;
47 }
48 }
49 true
50 }
51 Input::Poll(responder) => self.output.poll(responder),
52 Input::Upgrade { tx, result } => {
53 if result.is_closed() {
54 return true;
55 }
56 if matches!(&self.output, Output::WebSocket { .. }) {
57 let _ = result.send(false);
58 return true;
59 }
60 let upgraded = self.output.upgrade(tx).await;
61 let _ = result.send(upgraded);
62 upgraded
63 }
64 Input::Emit {
65 packet,
66 ack,
67 accepted,
68 } => {
69 let connected = self.namespaces.contains_key(&packet.namespace);
70 let sent = self.emit(packet, ack).await;
71 if let Some(accepted) = accepted {
72 let _ = accepted.send(connected && sent);
73 }
74 sent
75 }
76 Input::Ack {
77 namespace,
78 id,
79 payload,
80 } => self.send_ack(namespace, id, payload).await,
81 Input::Close => false,
82 }
83 }
84
85 async fn handle_packet(&mut self, packet: EnginePacket) -> bool {
86 match packet {
87 EnginePacket::Ping(payload) => {
88 self.output.send(vec![EnginePacket::Pong(payload)]).await
89 }
90 EnginePacket::Pong(_) => {
91 self.last_pong = Instant::now();
92 if !self.namespaces.is_empty()
93 && self.last_actor_heartbeat.elapsed() >= self.hub.options().heartbeat_interval
94 {
95 self.last_actor_heartbeat = Instant::now();
96 self.heartbeat_actor();
97 }
98 true
99 }
100 EnginePacket::Message(Data::Text(text)) => self.handle_socket(&text).await,
101 EnginePacket::Message(Data::Binary(data)) => self.handle_binary(&data),
102 EnginePacket::Close => false,
103 EnginePacket::Open(_) | EnginePacket::Upgrade(_) | EnginePacket::Noop => true,
104 }
105 }
106
107 async fn handle_socket(&mut self, text: &str) -> bool {
108 if self.pending_binary.is_some() {
109 return false;
110 }
111
112 let Some(packet) = packet::decode(text) else {
113 return false;
114 };
115
116 match packet.kind {
117 Kind::Connect => self.connect(packet).await,
118 Kind::Disconnect => {
119 self.disconnect(&packet.namespace).await;
120 true
121 }
122 Kind::Event => {
123 if !self.namespaces.contains_key(&packet.namespace) {
124 return self
125 .connect_error(&packet.namespace, "namespace is not connected")
126 .await;
127 }
128 self.dispatch(packet, Vec::new())
129 }
130 Kind::Ack => {
131 self.resolve_ack(
132 &packet.namespace,
133 packet.id,
134 payload(packet.data, Vec::new()),
135 );
136 true
137 }
138 Kind::BinaryEvent => {
139 if !self.namespaces.contains_key(&packet.namespace) {
140 return self
141 .connect_error(&packet.namespace, "namespace is not connected")
142 .await;
143 }
144 self.pending_binary = Some(PendingBinary {
145 packet,
146 data: Vec::new(),
147 bytes: 0,
148 });
149 true
150 }
151 Kind::BinaryAck => {
152 self.pending_binary = Some(PendingBinary {
153 packet,
154 data: Vec::new(),
155 bytes: 0,
156 });
157 true
158 }
159 Kind::ConnectError => true,
160 }
161 }
162
163 async fn connect(&mut self, packet: SocketPacket) -> bool {
164 if self.user_id == 0 {
165 let token = packet
166 .data
167 .get("token")
168 .or_else(|| packet.data.get("access_token"))
169 .and_then(serde_json::Value::as_str);
170 let Some(token) = token else {
171 return self
172 .connect_error(&packet.namespace, "missing access token")
173 .await;
174 };
175 match self.hub.authenticate_token(token).await {
176 Ok(user_id) => {
177 self.user_id = user_id;
178 self.access_token = token.to_string();
179 self.auth_deadline = None;
180 }
181 Err(_) => {
182 return self
183 .connect_error(&packet.namespace, "authentication failed")
184 .await;
185 }
186 }
187 }
188 let namespace = packet.namespace;
189 let device = packet
190 .data
191 .get("device_id")
192 .and_then(serde_json::Value::as_str);
193 if self.device_id.is_empty() {
194 self.device_id = match device {
195 Some(device) => match Uuid::parse_str(device) {
196 Ok(device) => device.to_string(),
197 Err(_) => {
198 return self
199 .connect_error(&namespace, "device_id must be a UUID")
200 .await;
201 }
202 },
203 None => Uuid::new_v4().to_string(),
204 };
205 } else if device.is_some_and(|device| {
206 Uuid::parse_str(device)
207 .map(|id| id.to_string())
208 .ok()
209 .as_deref()
210 != Some(self.device_id.as_str())
211 }) {
212 return self
213 .connect_error(&namespace, "device_id cannot change within a session")
214 .await;
215 }
216 if self.namespaces.len() >= 32 || namespace.len() > 256 {
217 return self
218 .connect_error(&namespace, "namespace limit exceeded")
219 .await;
220 }
221 if self.namespaces.contains_key(&namespace) {
222 return self
223 .connect_error(&namespace, "namespace already connected")
224 .await;
225 }
226
227 let session_id = Uuid::new_v4().to_string();
228 let handle = self.hub.session_handle(self.input.clone());
229 let connected = ConnectedSession {
230 user_id: self.user_id,
231 device_id: self.device_id.clone(),
232 namespace: namespace.clone(),
233 session_id: session_id.clone(),
234 session_sid: self.sid.clone(),
235 access_token: self.access_token.clone(),
236 first_namespace: self.namespaces.is_empty(),
237 hub: self.hub.clone(),
238 handle: handle.clone(),
239 };
240 if let Err(error) = self.hub.backend().on_connect(&connected).await {
241 return self
242 .connect_error(&namespace, &format!("connection setup failed: {error}"))
243 .await;
244 }
245 self.hub
246 .register(
247 self.user_id,
248 &namespace,
249 session_id.clone(),
250 self.input.clone(),
251 )
252 .await;
253 self.namespaces
254 .insert(namespace.clone(), session_id.clone());
255 self.hub.backend().heartbeat(self.user_id).await;
256
257 self.hub
258 .report_connection(ConnectionPhase::Open, self.user_id, &self.sid)
259 .await;
260
261 let packet = packet::encode_connect(&namespace, &session_id, &self.device_id);
262 self.last_actor_heartbeat = Instant::now();
263 if !self.output.send(vec![engine::text(packet)]).await {
264 return false;
265 }
266 let (stop, stopped) = oneshot::channel();
267 let backend = self.hub.backend().clone();
268 let context = DeliveryContext {
269 user_id: self.user_id,
270 device_id: self.device_id.clone(),
271 namespace: namespace.clone(),
272 session_id: session_id.clone(),
273 session_sid: self.sid.clone(),
274 access_token: self.access_token.clone(),
275 hub: self.hub.clone(),
276 handle,
277 stop: stopped,
278 };
279 let task = tokio::spawn(async move {
280 backend.run_delivery(context).await;
281 });
282 self.deliveries.insert(namespace, (stop, task));
283 true
284 }
285
286 async fn connect_error(&mut self, namespace: &str, message: &str) -> bool {
287 let packet = packet::encode_connect_error(namespace, message);
288 self.output.send(vec![engine::text(packet)]).await
289 }
290
291 async fn disconnect(&mut self, namespace: &str) {
292 if let Some((stop, _)) = self.deliveries.remove(namespace) {
293 let _ = stop.send(());
294 }
295 self.pending_acks
296 .retain(|_, (pending_namespace, _, _)| pending_namespace != namespace);
297 if let Some(session_id) = self.namespaces.remove(namespace) {
298 self.hub
299 .unregister(self.user_id, namespace, &session_id)
300 .await;
301 }
302 }
303
304 fn heartbeat_actor(&self) {
305 let backend = self.hub.backend().clone();
306 let hub = self.hub.clone();
307 let user_id = self.user_id;
308 let session = self.sid.clone();
309 tokio::spawn(async move {
310 let _ = tokio::time::timeout(
311 std::time::Duration::from_secs(5),
312 backend.heartbeat(user_id),
313 )
314 .await;
315 hub.report_connection(ConnectionPhase::Heartbeat, user_id, &session)
316 .await;
317 });
318 }
319
320 pub(super) async fn cleanup(&mut self) {
321 let mut tasks = Vec::new();
322 for (_, (stop, task)) in self.deliveries.drain() {
323 let _ = stop.send(());
324 tasks.push(task);
325 }
326 let _ = tokio::time::timeout(
327 std::time::Duration::from_secs(5),
328 futures_util::future::join_all(tasks),
329 )
330 .await;
331 for (namespace, session_id) in self.namespaces.drain() {
332 self.hub
333 .unregister(self.user_id, &namespace, &session_id)
334 .await;
335 }
336 self.pending_acks.clear();
337 self.output.close().await;
338 self.hub
339 .report_connection(ConnectionPhase::Close, self.user_id, &self.sid)
340 .await;
341 self.hub.remove_session(&self.sid).await;
342 }
343}
Added lib/socket/src/transport/direct.rs lib/socket/src/transport/direct.rs +71 −0 View lib/socket/src/transport/direct.rs
@@ -0,0 +1,71 @@
1use futures_util::{SinkExt, StreamExt};
2use tokio_tungstenite::tungstenite::Message;
3
4use super::WebSocket;
5use tokio::sync::mpsc;
6
7use super::frames::write_frames;
8use crate::Frame;
9use crate::engine::{self, Data, Packet as EnginePacket};
10use crate::session::Input;
11
12pub(super) async fn run_direct(
13 input: mpsc::Sender<Input>,
14 mut socket: WebSocket,
15 mut outbound: mpsc::Receiver<Vec<Frame>>,
16) {
17 loop {
18 tokio::select! {
19 _ = input.closed() => break,
20 incoming = socket.next() => {
21 let Some(Ok(message)) = incoming else {
22 break;
23 };
24 match message {
25 Message::Text(text) => {
26 let Some(packet) = engine::decode_packet(&text) else {
27 break;
28 };
29 if input.try_send(Input::Packets(vec![packet])).is_err() {
30 break;
31 }
32 }
33 Message::Binary(data) => {
34 if input
35 .try_send(Input::Packets(vec![EnginePacket::Message(Data::Binary(
36 data.to_vec(),
37 ))]))
38 .is_err()
39 {
40 break;
41 }
42 }
43 Message::Ping(_) => {
44 if !matches!(tokio::time::timeout(std::time::Duration::from_secs(5), socket.flush()).await, Ok(Ok(()))) {
45 break;
46 }
47 }
48 Message::Pong(_) | Message::Frame(_) => {}
49 Message::Close(_) => break,
50 }
51 }
52 frames = outbound.recv() => {
53 match frames {
54 Some(frames) => {
55 if !write_frames(&mut socket, frames).await {
56 break;
57 }
58 }
59 None => break,
60 }
61 }
62 }
63 }
64
65 let _ = tokio::time::timeout(std::time::Duration::from_secs(5), input.send(Input::Close)).await;
66 let _ = tokio::time::timeout(
67 std::time::Duration::from_secs(5),
68 SinkExt::close(&mut socket),
69 )
70 .await;
71}
Added lib/socket/src/transport/frames.rs lib/socket/src/transport/frames.rs +21 −0 View lib/socket/src/transport/frames.rs
@@ -0,0 +1,21 @@
1use futures_util::SinkExt;
2use tokio_tungstenite::tungstenite::Message;
3
4use super::WebSocket;
5use crate::Frame;
6
7pub(super) async fn write_frames(socket: &mut WebSocket, frames: Vec<Frame>) -> bool {
8 for frame in frames {
9 let message = match frame {
10 Frame::Text(text) => Message::Text(text.into()),
11 Frame::Binary(data) => Message::Binary(data.into()),
12 };
13 if !matches!(
14 tokio::time::timeout(std::time::Duration::from_secs(5), socket.send(message)).await,
15 Ok(Ok(()))
16 ) {
17 return false;
18 }
19 }
20 true
21}
Added lib/socket/src/transport/handle.rs lib/socket/src/transport/handle.rs +90 −0 View lib/socket/src/transport/handle.rs
@@ -0,0 +1,90 @@
1use std::time::Duration;
2
3use base64::Engine as _;
4use base64::engine::general_purpose::STANDARD;
5use http_body_util::Full;
6use hyper::body::Bytes;
7use hyper_util::rt::TokioIo;
8use tokio::sync::mpsc;
9use tokio_tungstenite::WebSocketStream;
10use tokio_tungstenite::tungstenite::handshake::server::create_response_with_body;
11use tokio_tungstenite::tungstenite::protocol::{Role, WebSocketConfig};
12use uuid::Uuid;
13
14use super::direct::run_direct;
15use super::upgrade::run_upgrade;
16use crate::response::{HttpRequest, HttpResponse, authenticate, engine_error};
17use crate::session::Output;
18use crate::{OUTBOUND_CAPACITY, SocketHub};
19
20pub(crate) async fn handle_websocket(
21 mut request: HttpRequest,
22 hub: SocketHub,
23 sid: Option<String>,
24) -> HttpResponse {
25 let valid_key = request
26 .headers()
27 .get("sec-websocket-key")
28 .and_then(|key| STANDARD.decode(key.as_bytes()).ok())
29 .is_some_and(|key| key.len() == 16);
30 if !valid_key {
31 return engine_error(3, "Bad request");
32 }
33 let response = match create_response_with_body(&request, || Full::new(Bytes::new())) {
34 Ok(response) => response,
35 Err(_) => return engine_error(3, "Bad request"),
36 };
37 let input = match sid {
38 Some(sid) => match hub.input(&sid).await {
39 Some(input) => Some(input),
40 None => return engine_error(1, "Session ID unknown"),
41 },
42 None => None,
43 };
44 let (user_id, token) = if input.is_none() {
45 match authenticate(&request, &hub).await {
46 Ok(user_id) => user_id,
47 Err(response) => return response,
48 }
49 } else {
50 (0, String::new())
51 };
52 let upgrade = hyper::upgrade::on(&mut request);
53 let mut shutdown = hub.shutdown_receiver();
54 let transport_hub = hub.clone();
55 hub.spawn_transport(async move {
56 let hub = transport_hub;
57 if *shutdown.borrow() {
58 return;
59 }
60 let upgraded = tokio::select! {
61 _ = shutdown.changed() => return,
62 result = tokio::time::timeout(Duration::from_secs(5), upgrade) => match result {
63 Ok(Ok(upgraded)) => upgraded,
64 _ => return,
65 },
66 };
67 let max_payload = hub.options().max_payload;
68 let config = WebSocketConfig::default()
69 .read_buffer_size(8192)
70 .max_message_size(Some(max_payload))
71 .max_frame_size(Some(max_payload))
72 .max_write_buffer_size(max_payload.saturating_add(256 * 1024));
73 let socket =
74 WebSocketStream::from_raw_socket(TokioIo::new(upgraded), Role::Server, Some(config))
75 .await;
76 let (out_tx, out_rx) = mpsc::channel(OUTBOUND_CAPACITY);
77 match input {
78 Some(input) => run_upgrade(input, socket, out_tx, out_rx).await,
79 None => {
80 let sid = Uuid::new_v4().to_string();
81 let input = hub
82 .create_session(sid, user_id, token, Output::websocket(out_tx), true)
83 .await;
84 run_direct(input, socket, out_rx).await;
85 }
86 }
87 })
88 .await;
89 response
90}
Added lib/socket/src/transport/mod.rs lib/socket/src/transport/mod.rs +9 −0 View lib/socket/src/transport/mod.rs
@@ -0,0 +1,9 @@
1mod direct;
2mod frames;
3mod handle;
4mod upgrade;
5
6type WebSocket =
7 tokio_tungstenite::WebSocketStream<hyper_util::rt::TokioIo<hyper::upgrade::Upgraded>>;
8
9pub(crate) use handle::handle_websocket;
Added lib/socket/src/transport/upgrade.rs lib/socket/src/transport/upgrade.rs +104 −0 View lib/socket/src/transport/upgrade.rs
@@ -0,0 +1,104 @@
1use futures_util::{SinkExt, StreamExt};
2use tokio_tungstenite::tungstenite::Message;
3
4use super::WebSocket;
5use tokio::sync::{mpsc, oneshot};
6
7use super::frames::write_frames;
8use crate::Frame;
9use crate::engine::{self, Data, Packet as EnginePacket};
10use crate::session::Input;
11
12pub(super) async fn run_upgrade(
13 input: mpsc::Sender<Input>,
14 mut socket: WebSocket,
15 out_tx: mpsc::Sender<Vec<Frame>>,
16 mut outbound: mpsc::Receiver<Vec<Frame>>,
17) {
18 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
19 let mut out_tx = Some(out_tx);
20 let mut probed = false;
21 let mut upgraded = false;
22
23 loop {
24 tokio::select! {
25 _ = input.closed() => break,
26 _ = tokio::time::sleep_until(deadline), if !upgraded => break,
27 incoming = socket.next() => {
28 let Some(Ok(message)) = incoming else {
29 break;
30 };
31 match message {
32 Message::Text(text) => {
33 let Some(packet) = engine::decode_packet(&text) else {
34 break;
35 };
36 match packet {
37 EnginePacket::Ping(payload) if payload == "probe" && !upgraded => {
38 if !write_frames(&mut socket, vec![Frame::Text("3probe".to_string())]).await {
39 break;
40 }
41 probed = true;
42 }
43 EnginePacket::Upgrade(_) if probed && !upgraded => {
44 let Some(tx) = out_tx.take() else { break };
45 let (result, receiver) = oneshot::channel();
46 if input.try_send(Input::Upgrade { tx, result }).is_err() {
47 break;
48 }
49 if !matches!(tokio::time::timeout_at(deadline, receiver).await, Ok(Ok(true))) {
50 break;
51 }
52 upgraded = true;
53 }
54 other if upgraded => {
55 if input.try_send(Input::Packets(vec![other])).is_err() {
56 break;
57 }
58 }
59 _ => {}
60 }
61 }
62 Message::Binary(data) if upgraded => {
63 if input
64 .try_send(Input::Packets(vec![EnginePacket::Message(Data::Binary(
65 data.to_vec(),
66 ))]))
67 .is_err()
68 {
69 break;
70 }
71 }
72 Message::Binary(_) => {}
73 Message::Ping(_) => {
74 if !matches!(tokio::time::timeout(std::time::Duration::from_secs(5), socket.flush()).await, Ok(Ok(()))) {
75 break;
76 }
77 }
78 Message::Pong(_) | Message::Frame(_) => {}
79 Message::Close(_) => break,
80 }
81 }
82 frames = outbound.recv() => {
83 match frames {
84 Some(frames) => {
85 if !write_frames(&mut socket, frames).await {
86 break;
87 }
88 }
89 None => break,
90 }
91 }
92 }
93 }
94
95 if upgraded {
96 let _ =
97 tokio::time::timeout(std::time::Duration::from_secs(5), input.send(Input::Close)).await;
98 }
99 let _ = tokio::time::timeout(
100 std::time::Duration::from_secs(5),
101 SinkExt::close(&mut socket),
102 )
103 .await;
104}
Added lib/socket/src/backend.rs lib/socket/src/backend.rs +127 −0 View lib/socket/src/backend.rs
@@ -0,0 +1,127 @@
1use std::fmt;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use tokio::sync::{mpsc, oneshot};
6
7use crate::hub::SocketHub;
8use crate::session::Input;
9use crate::{EmitPacket, SocketPayload};
10
11pub type SocketResult<T> = Result<T, SocketError>;
12
13#[derive(Debug)]
14pub enum SocketError {
15 Rejected(String),
16 Unavailable(String),
17 Config(String),
18}
19
20impl fmt::Display for SocketError {
21 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 Self::Rejected(message) => write!(formatter, "rejected: {message}"),
24 Self::Unavailable(message) => write!(formatter, "unavailable: {message}"),
25 Self::Config(message) => write!(formatter, "config: {message}"),
26 }
27 }
28}
29
30impl std::error::Error for SocketError {}
31
32/// 聊天连接生命周期阶段(在线态旁路上报)。
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ConnectionPhase {
35 Open,
36 Heartbeat,
37 Close,
38}
39
40/// 会话出站句柄:业务层(事件投递)只通过它把事件推给具体客户端。
41#[derive(Clone)]
42pub struct SessionHandle {
43 input: mpsc::Sender<Input>,
44}
45
46impl SessionHandle {
47 pub(crate) fn new(input: mpsc::Sender<Input>) -> Self {
48 Self { input }
49 }
50
51 /// 投递一个事件并等待客户端 ACK;返回是否被接受。
52 pub async fn deliver(&self, packet: EmitPacket, timeout: Duration) -> bool {
53 let deadline = tokio::time::Instant::now() + timeout;
54 let (ack, receipt) = oneshot::channel::<SocketPayload>();
55 if self
56 .input
57 .send(Input::Emit {
58 packet,
59 ack: Some((ack, deadline)),
60 accepted: None,
61 })
62 .await
63 .is_err()
64 {
65 return false;
66 }
67 matches!(tokio::time::timeout_at(deadline, receipt).await, Ok(Ok(_)))
68 }
69
70 pub async fn close(&self) {
71 let _ = self.input.send(Input::Close).await;
72 }
73
74 pub fn is_closed(&self) -> bool {
75 self.input.is_closed()
76 }
77
78 pub async fn closed(&self) {
79 self.input.closed().await
80 }
81}
82
83/// 命名空间连接建立后的会话快照,交给业务层做设备注册/在线态登记。
84#[derive(Clone)]
85pub struct ConnectedSession {
86 pub user_id: i64,
87 pub device_id: String,
88 pub namespace: String,
89 pub session_id: String,
90 pub session_sid: String,
91 pub access_token: String,
92 pub first_namespace: bool,
93 pub hub: SocketHub,
94 pub handle: SessionHandle,
95}
96
97/// 每个 namespace 一个投递 worker 的上下文。
98pub struct DeliveryContext {
99 pub user_id: i64,
100 pub device_id: String,
101 pub namespace: String,
102 pub session_id: String,
103 pub session_sid: String,
104 pub access_token: String,
105 pub hub: SocketHub,
106 pub handle: SessionHandle,
107 pub stop: oneshot::Receiver<()>,
108}
109
110/// 通道与业务的分界:socket 只负责协议/会话,身份、订阅与投递策略由业务层实现。
111#[async_trait]
112pub trait SocketBackend: Send + Sync + 'static {
113 /// 会话凭证(KenApi 会话键)→ user_id。
114 async fn authenticate(&self, token: &str) -> SocketResult<i64>;
115
116 /// 命名空间连接建立后:设备游标注册、组织成员缓存、在线心跳。
117 async fn on_connect(&self, session: &ConnectedSession) -> SocketResult<()>;
118
119 /// 连接心跳(PONG 触发)。
120 async fn heartbeat(&self, user_id: i64);
121
122 /// 在线态旁路上报;失败只记日志。
123 async fn report_connection(&self, phase: ConnectionPhase, user_id: i64, session: &str);
124
125 /// 每个 namespace 的投递 worker(订阅匹配 + 二次鉴权 + 积压消费)。
126 async fn run_delivery(&self, context: DeliveryContext);
127}
Added lib/socket/src/engine.rs lib/socket/src/engine.rs +131 −0 View lib/socket/src/engine.rs
@@ -0,0 +1,131 @@
1use std::time::Duration;
2
3use base64::Engine as _;
4use base64::engine::general_purpose::STANDARD;
5
6use super::Frame;
7
8pub(crate) const SEPARATOR: char = '\u{1e}';
9const SEPARATOR_TEXT: &str = "\u{1e}";
10
11pub(crate) const OPEN: char = '0';
12pub(crate) const CLOSE: char = '1';
13pub(crate) const PING: char = '2';
14pub(crate) const PONG: char = '3';
15pub(crate) const MESSAGE: char = '4';
16pub(crate) const UPGRADE: char = '5';
17pub(crate) const NOOP: char = '6';
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub(crate) enum Data {
21 Text(String),
22 Binary(Vec<u8>),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub(crate) enum Packet {
27 Open(String),
28 Close,
29 Ping(String),
30 Pong(String),
31 Message(Data),
32 Upgrade(String),
33 Noop,
34}
35
36pub(crate) fn open(
37 sid: &str,
38 ping_interval: Duration,
39 ping_timeout: Duration,
40 max_payload: usize,
41 upgrades: &[&str],
42) -> Packet {
43 let payload = serde_json::json!({
44 "sid": sid,
45 "upgrades": upgrades,
46 "pingInterval": millis(ping_interval),
47 "pingTimeout": millis(ping_timeout),
48 "maxPayload": max_payload,
49 });
50 Packet::Open(payload.to_string())
51}
52
53pub(crate) fn encode_text(packet: &Packet) -> Option<String> {
54 match packet {
55 Packet::Open(payload) => Some(format!("{OPEN}{payload}")),
56 Packet::Close => Some(CLOSE.to_string()),
57 Packet::Ping(payload) => Some(format!("{PING}{payload}")),
58 Packet::Pong(payload) => Some(format!("{PONG}{payload}")),
59 Packet::Message(Data::Text(payload)) => Some(format!("{MESSAGE}{payload}")),
60 Packet::Message(Data::Binary(_)) => None,
61 Packet::Upgrade(payload) => Some(format!("{UPGRADE}{payload}")),
62 Packet::Noop => Some(NOOP.to_string()),
63 }
64}
65
66pub(crate) fn encode_payload(packets: &[Packet]) -> String {
67 packets
68 .iter()
69 .filter_map(|packet| match packet {
70 Packet::Message(Data::Binary(data)) => Some(format!("b{}", STANDARD.encode(data))),
71 other => encode_text(other),
72 })
73 .collect::<Vec<_>>()
74 .join(SEPARATOR_TEXT)
75}
76
77pub(crate) fn encode_frames(packets: &[Packet]) -> Vec<Frame> {
78 let mut frames = Vec::with_capacity(packets.len());
79 for packet in packets {
80 match packet {
81 Packet::Message(Data::Binary(data)) => frames.push(Frame::Binary(data.clone())),
82 other => {
83 if let Some(text) = encode_text(other) {
84 frames.push(Frame::Text(text));
85 }
86 }
87 }
88 }
89 frames
90}
91
92pub(crate) fn decode_packet(text: &str) -> Option<Packet> {
93 if let Some(encoded) = text.strip_prefix('b') {
94 let data = STANDARD.decode(encoded).ok()?;
95 return Some(Packet::Message(Data::Binary(data)));
96 }
97
98 let mut chars = text.chars();
99 let kind = chars.next()?;
100 let payload = chars.as_str();
101
102 match kind {
103 OPEN => Some(Packet::Open(payload.to_string())),
104 CLOSE if payload.is_empty() => Some(Packet::Close),
105 PING => Some(Packet::Ping(payload.to_string())),
106 PONG => Some(Packet::Pong(payload.to_string())),
107 MESSAGE => Some(Packet::Message(Data::Text(payload.to_string()))),
108 UPGRADE => Some(Packet::Upgrade(payload.to_string())),
109 NOOP if payload.is_empty() => Some(Packet::Noop),
110 _ => None,
111 }
112}
113
114pub(crate) fn decode_payload(text: &str) -> Option<Vec<Packet>> {
115 if text.is_empty() {
116 return None;
117 }
118 text.split(SEPARATOR).map(decode_packet).collect()
119}
120
121pub(crate) fn binary(data: Vec<u8>) -> Packet {
122 Packet::Message(Data::Binary(data))
123}
124
125pub(crate) fn text(data: String) -> Packet {
126 Packet::Message(Data::Text(data))
127}
128
129fn millis(duration: Duration) -> u64 {
130 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
131}
Added lib/socket/src/handle.rs lib/socket/src/handle.rs +90 −0 View lib/socket/src/handle.rs
@@ -0,0 +1,90 @@
1use hyper::{Method, StatusCode, header};
2
3use super::polling::{polling_open, polling_poll, polling_post};
4use super::response::{HttpRequest, HttpResponse, cors, empty_response, engine_error};
5use super::{SocketHub, transport};
6
7pub(super) async fn handle(request: HttpRequest, hub: SocketHub) -> HttpResponse {
8 let headers = request.headers().clone();
9 let response = route(request, &hub).await;
10 cors(&headers, response)
11}
12
13async fn route(request: HttpRequest, hub: &SocketHub) -> HttpResponse {
14 if !matches!(request.uri().path(), "/socket.io/" | "/socket.io") {
15 return empty_response(StatusCode::NOT_FOUND);
16 }
17 if request.method() == Method::OPTIONS {
18 return empty_response(StatusCode::NO_CONTENT);
19 }
20
21 let query = Query::parse(request.uri().query().unwrap_or_default());
22 if query.eio.as_deref() != Some("4") {
23 let (code, message) = if query.eio.is_some() {
24 (5, "Unsupported protocol version")
25 } else {
26 (3, "Bad request")
27 };
28 return engine_error(code, message);
29 }
30
31 if is_websocket(&request) {
32 if query.transport.as_deref() != Some("websocket") {
33 return engine_error(0, "Transport unknown");
34 }
35 return transport::handle_websocket(request, hub.clone(), query.sid).await;
36 }
37
38 match *request.method() {
39 Method::GET => {
40 if query.transport.as_deref() != Some("polling") {
41 return engine_error(0, "Transport unknown");
42 }
43 match query.sid {
44 Some(sid) => polling_poll(hub, &sid).await,
45 None => polling_open(&request, hub).await,
46 }
47 }
48 Method::POST => {
49 if query.transport.as_deref() != Some("polling") {
50 return engine_error(0, "Transport unknown");
51 }
52 polling_post(request.into_body(), hub, query.sid.as_deref()).await
53 }
54 _ => empty_response(StatusCode::METHOD_NOT_ALLOWED),
55 }
56}
57
58fn is_websocket(request: &HttpRequest) -> bool {
59 request
60 .headers()
61 .get(header::UPGRADE)
62 .and_then(|value| value.to_str().ok())
63 .is_some_and(|value| value.eq_ignore_ascii_case("websocket"))
64}
65
66struct Query {
67 eio: Option<String>,
68 transport: Option<String>,
69 sid: Option<String>,
70}
71
72impl Query {
73 fn parse(query: &str) -> Self {
74 let mut parsed = Self {
75 eio: None,
76 transport: None,
77 sid: None,
78 };
79 for pair in query.split('&') {
80 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
81 match name {
82 "EIO" => parsed.eio = Some(value.to_string()),
83 "transport" => parsed.transport = Some(value.to_string()),
84 "sid" => parsed.sid = Some(value.to_string()),
85 _ => {}
86 }
87 }
88 parsed
89 }
90}
Added lib/socket/src/lib.rs lib/socket/src/lib.rs +25 −0 View lib/socket/src/lib.rs
@@ -0,0 +1,25 @@
1mod backend;
2mod engine;
3mod handle;
4mod hub;
5mod options;
6mod packet;
7mod polling;
8mod response;
9mod server;
10mod session;
11mod transport;
12mod types;
13
14pub use backend::{
15 ConnectedSession, ConnectionPhase, DeliveryContext, SessionHandle, SocketBackend, SocketError,
16 SocketResult,
17};
18pub use hub::SocketHub;
19pub use options::{
20 DEFAULT_AUTH_TIMEOUT, DEFAULT_HEARTBEAT_TIMEOUT, DEFAULT_MAX_PAYLOAD, DEFAULT_SESSION_COOKIE,
21 MAX_HEARTBEAT_GAP, MIN_HEARTBEAT_INTERVAL, SocketOptions,
22};
23pub use types::{EmitPacket, SocketEvent, SocketPayload};
24
25pub(crate) use types::{Frame, OUTBOUND_CAPACITY};
Added lib/socket/src/mod.rs lib/socket/src/mod.rs +20 −0 View lib/socket/src/mod.rs
@@ -0,0 +1,20 @@
1mod engine;
2mod handle;
3mod hub;
4mod options;
5mod packet;
6mod polling;
7mod response;
8mod server;
9mod session;
10mod transport;
11mod types;
12
13pub use hub::SocketHub;
14pub use options::{
15 DEFAULT_AUTH_TIMEOUT, DEFAULT_HEARTBEAT_TIMEOUT, DEFAULT_MAX_PAYLOAD, DEFAULT_SESSION_COOKIE,
16 MAX_HEARTBEAT_GAP, MIN_HEARTBEAT_INTERVAL, SocketOptions,
17};
18pub use types::{SocketEvent, SocketPayload};
19
20pub(crate) use types::{EmitPacket, Frame, OUTBOUND_CAPACITY};
Added lib/socket/src/options.rs lib/socket/src/options.rs +73 −0 View lib/socket/src/options.rs
@@ -0,0 +1,73 @@
1use std::time::Duration;
2
3pub const MIN_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
4pub const MAX_HEARTBEAT_GAP: Duration = Duration::from_secs(60);
5pub const DEFAULT_HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(30);
6pub const DEFAULT_AUTH_TIMEOUT: Duration = Duration::from_secs(5);
7pub const DEFAULT_MAX_PAYLOAD: usize = 1_000_000;
8/// 浏览器握手带不了 `Authorization`,所以允许从会话 cookie 取同一个 access token。
9pub const DEFAULT_SESSION_COOKIE: &str = "gitdata_session";
10
11#[derive(Debug, Clone)]
12pub struct SocketOptions {
13 pub heartbeat_interval: Duration,
14 pub heartbeat_timeout: Duration,
15 pub auth_timeout: Duration,
16 pub max_payload: usize,
17 /// 承载 KenApi 会话键的 cookie 名(与平台 `session.cookie_name` 一致)。
18 pub session_cookie: String,
19}
20
21impl Default for SocketOptions {
22 fn default() -> Self {
23 Self {
24 heartbeat_interval: MIN_HEARTBEAT_INTERVAL,
25 heartbeat_timeout: DEFAULT_HEARTBEAT_TIMEOUT,
26 auth_timeout: DEFAULT_AUTH_TIMEOUT,
27 max_payload: DEFAULT_MAX_PAYLOAD,
28 session_cookie: DEFAULT_SESSION_COOKIE.to_string(),
29 }
30 }
31}
32
33impl SocketOptions {
34 pub fn validate(&self) -> Result<(), String> {
35 if self.heartbeat_interval < MIN_HEARTBEAT_INTERVAL {
36 return Err("socket.heartbeat_interval must be at least 30 seconds".to_string());
37 }
38 if self.heartbeat_timeout.is_zero() {
39 return Err("socket.heartbeat_timeout must be greater than zero".to_string());
40 }
41 let gap = self
42 .heartbeat_interval
43 .checked_add(self.heartbeat_timeout)
44 .ok_or_else(|| "socket heartbeat durations overflow".to_string())?;
45 if gap > MAX_HEARTBEAT_GAP {
46 return Err(
47 "socket heartbeat interval and timeout must not exceed 60 seconds".to_string(),
48 );
49 }
50 if self.auth_timeout.is_zero() {
51 return Err("socket.auth_timeout must be greater than zero".to_string());
52 }
53 if self.max_payload == 0 {
54 return Err("socket.max_payload must be greater than zero".to_string());
55 }
56 if self.session_cookie.trim().is_empty() {
57 return Err("socket.session_cookie must not be blank".to_string());
58 }
59 Ok(())
60 }
61}
62
63impl From<&config::event::socket::SocketConfig> for SocketOptions {
64 fn from(config: &config::event::socket::SocketConfig) -> Self {
65 Self {
66 heartbeat_interval: config.heartbeat_interval(),
67 heartbeat_timeout: config.heartbeat_timeout(),
68 auth_timeout: config.auth_timeout(),
69 max_payload: config.max_payload,
70 session_cookie: config.session_cookie.clone(),
71 }
72 }
73}
Added lib/socket/src/packet.rs lib/socket/src/packet.rs +183 −0 View lib/socket/src/packet.rs
@@ -0,0 +1,183 @@
1use serde_json::Value;
2
3const MAX_ATTACHMENTS: usize = 10;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub(crate) enum Kind {
7 Connect,
8 Disconnect,
9 Event,
10 Ack,
11 ConnectError,
12 BinaryEvent,
13 BinaryAck,
14}
15
16impl Kind {
17 fn from_char(value: char) -> Option<Self> {
18 match value {
19 '0' => Some(Self::Connect),
20 '1' => Some(Self::Disconnect),
21 '2' => Some(Self::Event),
22 '3' => Some(Self::Ack),
23 '4' => Some(Self::ConnectError),
24 '5' => Some(Self::BinaryEvent),
25 '6' => Some(Self::BinaryAck),
26 _ => None,
27 }
28 }
29
30 fn code(self) -> char {
31 match self {
32 Self::Connect => '0',
33 Self::Disconnect => '1',
34 Self::Event => '2',
35 Self::Ack => '3',
36 Self::ConnectError => '4',
37 Self::BinaryEvent => '5',
38 Self::BinaryAck => '6',
39 }
40 }
41
42 fn is_binary(self) -> bool {
43 matches!(self, Self::BinaryEvent | Self::BinaryAck)
44 }
45}
46
47#[derive(Debug, Clone)]
48pub(crate) struct Packet {
49 pub kind: Kind,
50 pub namespace: String,
51 pub id: Option<u64>,
52 pub data: Value,
53 pub attachments: usize,
54}
55
56pub(crate) fn decode(text: &str) -> Option<Packet> {
57 let mut chars = text.chars();
58 let kind = Kind::from_char(chars.next()?)?;
59 let mut rest = chars.as_str();
60
61 let mut attachments = 0;
62 if kind.is_binary() {
63 let (count, tail) = rest.split_once('-')?;
64 attachments = count.parse().ok()?;
65 if !(1..=MAX_ATTACHMENTS).contains(&attachments) {
66 return None;
67 }
68 rest = tail;
69 }
70
71 let mut namespace = "/".to_string();
72 if let Some(tail) = rest.strip_prefix('/') {
73 let (name, tail) = tail.split_once(',').unwrap_or((tail, ""));
74 namespace = format!("/{name}");
75 rest = tail;
76 }
77
78 let digits = rest
79 .bytes()
80 .take_while(|byte| byte.is_ascii_digit())
81 .count();
82 let (id, payload) = rest.split_at(digits);
83 let id = if id.is_empty() {
84 None
85 } else {
86 Some(id.parse().ok()?)
87 };
88 let data = if payload.is_empty() {
89 Value::Null
90 } else {
91 serde_json::from_str(payload).ok()?
92 };
93
94 let valid = match kind {
95 Kind::Connect => data.is_null() || data.is_object(),
96 Kind::Disconnect => data.is_null(),
97 Kind::Event | Kind::Ack | Kind::BinaryEvent | Kind::BinaryAck => data.is_array(),
98 Kind::ConnectError => data.is_object(),
99 };
100
101 valid.then_some(Packet {
102 kind,
103 namespace,
104 id,
105 data,
106 attachments,
107 })
108}
109
110pub(crate) fn encode_connect(namespace: &str, sid: &str, device_id: &str) -> String {
111 format!(
112 "{}{}",
113 prefix(Kind::Connect, namespace, None, 0),
114 serde_json::json!({ "sid": sid, "device_id": device_id })
115 )
116}
117
118pub(crate) fn encode_connect_error(namespace: &str, message: &str) -> String {
119 format!(
120 "{}{}",
121 prefix(Kind::ConnectError, namespace, None, 0),
122 serde_json::json!({ "message": message })
123 )
124}
125
126pub(crate) fn encode_event(namespace: &str, id: Option<u64>, data: &[Value]) -> String {
127 format!(
128 "{}{}",
129 prefix(Kind::Event, namespace, id, 0),
130 Value::Array(data.to_vec())
131 )
132}
133
134pub(crate) fn encode_ack(namespace: &str, id: u64, data: &[Value]) -> String {
135 format!(
136 "{}{}",
137 prefix(Kind::Ack, namespace, Some(id), 0),
138 Value::Array(data.to_vec())
139 )
140}
141
142pub(crate) fn encode_binary_event(
143 namespace: &str,
144 id: Option<u64>,
145 data: &[Value],
146 attachments: usize,
147) -> String {
148 format!(
149 "{}{}",
150 prefix(Kind::BinaryEvent, namespace, id, attachments),
151 Value::Array(data.to_vec())
152 )
153}
154
155pub(crate) fn encode_binary_ack(
156 namespace: &str,
157 id: u64,
158 data: &[Value],
159 attachments: usize,
160) -> String {
161 format!(
162 "{}{}",
163 prefix(Kind::BinaryAck, namespace, Some(id), attachments),
164 Value::Array(data.to_vec())
165 )
166}
167
168fn prefix(kind: Kind, namespace: &str, id: Option<u64>, attachments: usize) -> String {
169 let mut text = String::new();
170 text.push(kind.code());
171 if kind.is_binary() {
172 text.push_str(&attachments.to_string());
173 text.push('-');
174 }
175 if namespace != "/" {
176 text.push_str(namespace);
177 text.push(',');
178 }
179 if let Some(id) = id {
180 text.push_str(&id.to_string());
181 }
182 text
183}
Added lib/socket/src/polling.rs lib/socket/src/polling.rs +102 −0 View lib/socket/src/polling.rs
@@ -0,0 +1,102 @@
1use std::time::Duration;
2
3use http_body_util::{BodyExt, Limited};
4use hyper::StatusCode;
5use hyper::body::Incoming;
6use tokio::sync::oneshot;
7use uuid::Uuid;
8
9use super::response::{
10 HttpRequest, HttpResponse, authenticate, engine_error, json_response, text_response,
11};
12use super::session::{Input, Output};
13use super::{SocketHub, engine};
14
15pub(super) async fn polling_open(request: &HttpRequest, hub: &SocketHub) -> HttpResponse {
16 let (user_id, token) = match authenticate(request, hub).await {
17 Ok(user_id) => user_id,
18 Err(response) => return response,
19 };
20
21 let sid = Uuid::new_v4().to_string();
22 let options = hub.options();
23 let open = engine::open(
24 &sid,
25 options.heartbeat_interval,
26 options.heartbeat_timeout,
27 options.max_payload,
28 &["websocket"],
29 );
30 hub.create_session(sid, user_id, token, Output::polling(), false)
31 .await;
32
33 text_response(engine::encode_payload(&[open]))
34}
35
36pub(super) async fn polling_poll(hub: &SocketHub, sid: &str) -> HttpResponse {
37 let Some(input) = hub.input(sid).await else {
38 return engine_error(1, "Session ID unknown");
39 };
40
41 let (sender, receiver) = oneshot::channel();
42 if input.send(Input::Poll(sender)).await.is_err() {
43 return engine_error(1, "Session ID unknown");
44 }
45
46 let options = hub.options();
47 let wait = options.heartbeat_interval + options.heartbeat_timeout + Duration::from_secs(5);
48 match tokio::time::timeout(wait, receiver).await {
49 Ok(Ok(packets)) => {
50 let payload = if packets.is_empty() {
51 "6".to_string()
52 } else {
53 engine::encode_payload(&packets)
54 };
55 text_response(payload)
56 }
57 _ => text_response("6".to_string()),
58 }
59}
60
61pub(super) async fn polling_post(
62 body: Incoming,
63 hub: &SocketHub,
64 sid: Option<&str>,
65) -> HttpResponse {
66 let Some(sid) = sid else {
67 return engine_error(3, "Bad request");
68 };
69 let Some(input) = hub.input(sid).await else {
70 return engine_error(1, "Session ID unknown");
71 };
72
73 let bytes = match Limited::new(body, hub.options().max_payload)
74 .collect()
75 .await
76 {
77 Ok(body) => body.to_bytes(),
78 Err(error) if error.is::<http_body_util::LengthLimitError>() => {
79 return json_response(
80 StatusCode::PAYLOAD_TOO_LARGE,
81 serde_json::json!({
82 "code": 3,
83 "message": "Payload too large",
84 }),
85 );
86 }
87 Err(_) => return engine_error(3, "Bad request"),
88 };
89
90 let Ok(text) = std::str::from_utf8(&bytes) else {
91 return engine_error(3, "Bad request");
92 };
93 let Some(packets) = engine::decode_payload(text) else {
94 return engine_error(3, "Bad request");
95 };
96
97 if input.send(Input::Packets(packets)).await.is_err() {
98 return engine_error(1, "Session ID unknown");
99 }
100
101 text_response("ok".to_string())
102}
Added lib/socket/src/response.rs lib/socket/src/response.rs +167 −0 View lib/socket/src/response.rs
@@ -0,0 +1,167 @@
1use http_body_util::Full;
2use hyper::body::{Bytes, Incoming};
3use hyper::header::{self, HeaderValue};
4use hyper::{HeaderMap, Request, Response, StatusCode};
5use serde_json::Value;
6
7use crate::SocketError;
8
9use super::SocketHub;
10
11pub(super) type HttpRequest = Request<Incoming>;
12pub(super) type HttpResponse = Response<Full<Bytes>>;
13
14pub(super) async fn authenticate(
15 request: &HttpRequest,
16 hub: &SocketHub,
17) -> Result<(i64, String), HttpResponse> {
18 // 三条来源,按可信度排序:
19 // 1. `Authorization: Bearer`(服务端/Node 客户端);
20 // 2. 会话 cookie(浏览器握手拿不到自定义头,这是唯一可用的带 token 方式);
21 // 3. 都不带 → 返回 0,等客户端在 Socket.IO CONNECT 包里用 `auth.token` 补交
22 // (有 `socket.auth_timeout` 兜底)。
23 let token = bearer_token(request)
24 .map(str::to_owned)
25 .or_else(|| session_token(request, &hub.options().session_cookie));
26 let Some(token) = token else {
27 if request.headers().contains_key(header::AUTHORIZATION) {
28 return Err(unauthorized("missing bearer token"));
29 }
30 return Ok((0, String::new()));
31 };
32 hub.authenticate_token(&token)
33 .await
34 .map(|user| (user, token.clone()))
35 .map_err(auth_error_response)
36}
37
38/// 从 `Cookie` 头里取会话 token(名字由 `socket.session_cookie` 指定)。
39fn session_token(request: &HttpRequest, cookie: &str) -> Option<String> {
40 if cookie.trim().is_empty() {
41 return None;
42 }
43 let header = request.headers().get(header::COOKIE)?.to_str().ok()?;
44 header
45 .split(';')
46 .filter_map(|pair| pair.split_once('='))
47 .find_map(|(name, value)| {
48 (name.trim() == cookie && !value.trim().is_empty())
49 .then(|| value.trim().trim_matches('"').to_string())
50 })
51}
52
53pub(super) fn empty_response(status: StatusCode) -> HttpResponse {
54 let mut response = Response::new(Full::new(Bytes::new()));
55 *response.status_mut() = status;
56 response
57}
58
59pub(super) fn json_response(status: StatusCode, body: Value) -> HttpResponse {
60 let mut response = Response::new(Full::new(Bytes::from(body.to_string())));
61 *response.status_mut() = status;
62 response.headers_mut().insert(
63 header::CONTENT_TYPE,
64 HeaderValue::from_static("application/json"),
65 );
66 response
67 .headers_mut()
68 .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
69 response
70}
71
72pub(super) fn engine_error(code: u16, message: &str) -> HttpResponse {
73 json_response(
74 StatusCode::BAD_REQUEST,
75 serde_json::json!({
76 "code": code,
77 "message": message,
78 }),
79 )
80}
81
82pub(super) fn text_response(body: String) -> HttpResponse {
83 let mut response = Response::new(Full::new(Bytes::from(body)));
84 response.headers_mut().insert(
85 header::CONTENT_TYPE,
86 HeaderValue::from_static("text/plain; charset=utf-8"),
87 );
88 response
89 .headers_mut()
90 .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
91 response
92}
93
94fn unauthorized(message: &str) -> HttpResponse {
95 json_response(
96 StatusCode::UNAUTHORIZED,
97 serde_json::json!({
98 "code": "UNAUTHENTICATED",
99 "message": message,
100 }),
101 )
102}
103
104fn auth_error_response(error: SocketError) -> HttpResponse {
105 match error {
106 SocketError::Rejected(message) => unauthorized(&message),
107 SocketError::Unavailable(message) => json_response(
108 StatusCode::SERVICE_UNAVAILABLE,
109 serde_json::json!({
110 "code": "UNAVAILABLE",
111 "message": message,
112 }),
113 ),
114 SocketError::Config(message) => json_response(
115 StatusCode::INTERNAL_SERVER_ERROR,
116 serde_json::json!({
117 "code": "INTERNAL",
118 "message": message,
119 }),
120 ),
121 }
122}
123
124pub(super) fn cors(headers: &HeaderMap, mut response: HttpResponse) -> HttpResponse {
125 let origin = headers
126 .get(header::ORIGIN)
127 .and_then(|value| value.to_str().ok())
128 .unwrap_or("*");
129
130 if let Ok(value) = HeaderValue::from_str(origin) {
131 response
132 .headers_mut()
133 .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, value);
134 if origin != "*" {
135 response.headers_mut().insert(
136 header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
137 HeaderValue::from_static("true"),
138 );
139 }
140 }
141 response
142 .headers_mut()
143 .insert(header::VARY, HeaderValue::from_static("Origin"));
144 response.headers_mut().insert(
145 header::ACCESS_CONTROL_ALLOW_METHODS,
146 HeaderValue::from_static("GET, POST, OPTIONS"),
147 );
148 response.headers_mut().insert(
149 header::ACCESS_CONTROL_ALLOW_HEADERS,
150 HeaderValue::from_static("authorization, content-type"),
151 );
152 response
153}
154
155fn bearer_token(request: &HttpRequest) -> Option<&str> {
156 let value = request
157 .headers()
158 .get(header::AUTHORIZATION)?
159 .to_str()
160 .ok()?;
161 let (scheme, token) = value.split_once(' ')?;
162 if !scheme.eq_ignore_ascii_case("bearer") {
163 return None;
164 }
165 let token = token.trim();
166 (!token.is_empty()).then_some(token)
167}
Added lib/socket/src/server.rs lib/socket/src/server.rs +86 −0 View lib/socket/src/server.rs
@@ -0,0 +1,86 @@
1use std::convert::Infallible;
2use std::io;
3use std::time::Duration;
4
5use hyper::server::conn::http1;
6use hyper::service::service_fn;
7use hyper_util::rt::{TokioIo, TokioTimer};
8use tokio::net::TcpListener;
9use tokio::sync::watch;
10use tokio::task::JoinSet;
11
12use super::SocketHub;
13use super::handle::handle;
14
15impl SocketHub {
16 pub async fn serve(
17 &self,
18 listener: TcpListener,
19 mut shutdown: watch::Receiver<bool>,
20 ) -> io::Result<()> {
21 let mut connections = JoinSet::new();
22 let mut stopped = self.shutdown_receiver();
23 let result = loop {
24 if *shutdown.borrow() || *stopped.borrow() {
25 break Ok(());
26 }
27 tokio::select! {
28 _ = shutdown.changed() => break Ok(()),
29 _ = stopped.changed() => break Ok(()),
30 accepted = listener.accept() => {
31 let (stream, _) = match accepted {
32 Ok(connection) => connection,
33 Err(error) => break Err(error),
34 };
35 let hub = self.clone();
36 let mut stop = self.shutdown_receiver();
37 connections.spawn(async move {
38 let service = service_fn(move |request| {
39 let hub = hub.clone();
40 async move { Ok::<_, Infallible>(handle(request, hub).await) }
41 });
42 let connection = http1::Builder::new()
43 .timer(TokioTimer::new())
44 .header_read_timeout(Duration::from_secs(10))
45 .max_buf_size(32 * 1024)
46 .serve_connection(TokioIo::new(stream), service)
47 .with_upgrades();
48 tokio::pin!(connection);
49 if *stop.borrow() {
50 return;
51 }
52 tokio::select! {
53 result = &mut connection => {
54 if let Err(error) = result {
55 eprintln!("kenevent: HTTP connection failed: {error}");
56 }
57 }
58 _ = stop.changed() => {
59 connection.as_mut().graceful_shutdown();
60 let _ = tokio::time::timeout(Duration::from_secs(5), &mut connection).await;
61 }
62 }
63 });
64 }
65 Some(result) = connections.join_next(), if !connections.is_empty() => {
66 if let Err(error) = result {
67 eprintln!("kenevent: HTTP task failed: {error}");
68 }
69 }
70 }
71 };
72 drop(listener);
73 self.shutdown();
74 if tokio::time::timeout(Duration::from_secs(5), async {
75 while connections.join_next().await.is_some() {}
76 })
77 .await
78 .is_err()
79 {
80 connections.abort_all();
81 while connections.join_next().await.is_some() {}
82 }
83 self.finish_transports().await;
84 result
85 }
86}
Added lib/socket/src/types.rs lib/socket/src/types.rs +56 −0 View lib/socket/src/types.rs
@@ -0,0 +1,56 @@
1use serde_json::Value;
2use tokio::sync::oneshot;
3
4pub(crate) const OUTBOUND_CAPACITY: usize = 256;
5
6#[derive(Debug, Clone, Default)]
7pub struct SocketPayload {
8 pub data: Vec<Value>,
9 pub binaries: Vec<Vec<u8>>,
10}
11
12impl SocketPayload {
13 pub fn binary(&self, placeholder: &Value) -> Option<&[u8]> {
14 if !placeholder.get("_placeholder")?.as_bool()? {
15 return None;
16 }
17 let index = usize::try_from(placeholder.get("num")?.as_u64()?).ok()?;
18 self.binaries.get(index).map(Vec::as_slice)
19 }
20}
21
22pub struct SocketEvent {
23 pub user_id: i64,
24 pub device_id: String,
25 pub access_token: String,
26 pub namespace: String,
27 pub session_id: String,
28 pub event: String,
29 pub data: Vec<Value>,
30 pub binaries: Vec<Vec<u8>>,
31 pub ack: Option<oneshot::Sender<SocketPayload>>,
32}
33
34impl SocketEvent {
35 pub fn binary(&self, placeholder: &Value) -> Option<&[u8]> {
36 if !placeholder.get("_placeholder")?.as_bool()? {
37 return None;
38 }
39 let index = usize::try_from(placeholder.get("num")?.as_u64()?).ok()?;
40 self.binaries.get(index).map(Vec::as_slice)
41 }
42}
43
44#[derive(Debug, Clone)]
45pub struct EmitPacket {
46 pub namespace: String,
47 pub event: String,
48 pub args: Vec<Value>,
49 pub binaries: Vec<Vec<u8>>,
50}
51
52#[derive(Debug, Clone)]
53pub(crate) enum Frame {
54 Text(String),
55 Binary(Vec<u8>),
56}
Added lib/socket/Cargo.toml lib/socket/Cargo.toml +20 −0 View lib/socket/Cargo.toml
@@ -0,0 +1,20 @@
1[package]
2name = "socket"
3version.workspace = true
4edition.workspace = true
5publish = false
6description = "Engine.IO v4 / Socket.IO 子集协议与会话管理"
7
8[dependencies]
9async-trait = { workspace = true }
10base64 = { workspace = true }
11config = { workspace = true }
12futures-util = { workspace = true }
13http-body-util = { workspace = true }
14hyper = { workspace = true }
15hyper-util = { workspace = true }
16serde = { workspace = true }
17serde_json = { workspace = true }
18tokio = { workspace = true, features = ["rt", "sync", "time", "macros", "net", "io-util"] }
19tokio-tungstenite = { workspace = true }
20uuid = { workspace = true }
Modified .gitignore .gitignore +2 −0 View .gitignore
@@ -12,3 +12,5 @@
1212node_modules
1313view/dist
1414config/api/keys/
15actor.node
16config/dev-keys/
Modified Cargo.lock Cargo.lock +102 −0 View Cargo.lock
@@ -201,6 +201,20 @@
201201]
202202
203203[[package]]
204name = "actor"
205version = "1.0.0"
206dependencies = [
207 "base64 0.23.1",
208 "cache",
209 "config 1.0.0",
210 "redis",
211 "serde",
212 "serde_json",
213 "tokio",
214 "uuid",
215]
216
217[[package]]
204218name = "adler2"
205219version = "2.0.1"
206220source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -438,6 +452,7 @@
438452version = "1.0.0"
439453dependencies = [
440454 "actix-web",
455 "actor",
441456 "anyhow",
442457 "auth",
443458 "chat",
@@ -446,6 +461,7 @@
446461 "deletion",
447462 "events",
448463 "export",
464 "internal",
449465 "models",
450466 "oltp",
451467 "push",
@@ -453,6 +469,8 @@
453469 "sea-orm",
454470 "serde_json",
455471 "session",
472 "socket",
473 "space",
456474 "tokio",
457475 "tokio-util",
458476 "tracing",
@@ -812,6 +830,7 @@
812830 "http 1.5.0",
813831 "http-body 1.1.0",
814832 "i18n",
833 "internal",
815834 "lettre",
816835 "maud",
817836 "models",
@@ -844,6 +863,7 @@
844863 "tokio",
845864 "tokio-stream",
846865 "tokio-util",
866 "tonic",
847867 "tower",
848868 "tracing",
849869 "tracing-opentelemetry",
@@ -916,10 +936,13 @@
916936dependencies = [
917937 "axum-core",
918938 "bytes",
939 "form_urlencoded",
919940 "futures-util",
920941 "http 1.5.0",
921942 "http-body 1.1.0",
922943 "http-body-util",
944 "hyper 1.12.0",
945 "hyper-util",
923946 "itoa",
924947 "matchit",
925948 "memchr",
@@ -927,10 +950,15 @@
927950 "percent-encoding 2.3.2",
928951 "pin-project-lite",
929952 "serde_core",
953 "serde_json",
954 "serde_path_to_error",
955 "serde_urlencoded",
930956 "sync_wrapper",
957 "tokio",
931958 "tower",
932959 "tower-layer",
933960 "tower-service",
961 "tracing",
934962]
935963
936964[[package]]
@@ -949,6 +977,7 @@
949977 "sync_wrapper",
950978 "tower-layer",
951979 "tower-service",
980 "tracing",
952981]
953982
954983[[package]]
@@ -1297,10 +1326,12 @@
12971326 "http 0.2.12",
12981327 "i18n",
12991328 "imaging",
1329 "internal",
13001330 "livekit-api",
13011331 "livekit-protocol",
13021332 "models",
13031333 "oltp",
1334 "proto",
13041335 "queue",
13051336 "rand 0.8.8",
13061337 "redis",
@@ -1316,6 +1347,7 @@
13161347 "time 0.3.55",
13171348 "tokio",
13181349 "tokio-util",
1350 "tonic",
13191351 "tracing",
13201352 "url 2.5.8",
13211353 "utoipa 5.5.0",
@@ -2352,20 +2384,31 @@
23522384name = "events"
23532385version = "1.0.0"
23542386dependencies = [
2387 "actor",
2388 "async-trait",
2389 "base64 0.23.1",
23552390 "chrono",
23562391 "config 1.0.0",
23572392 "db",
23582393 "envelope",
23592394 "futures-util",
2395 "internal",
23602396 "models",
23612397 "opentelemetry",
2398 "proto",
23622399 "queue",
2400 "redis",
2401 "reqwest 0.12.28",
2402 "rocketmq-client-rust",
2403 "rocketmq-common",
23632404 "sea-orm",
23642405 "serde",
23652406 "serde_json",
2407 "socket",
23662408 "thiserror 2.0.21",
23672409 "tokio",
23682410 "tokio-util",
2411 "tonic",
23692412 "tracing",
23702413 "uuid",
23712414]
@@ -3580,6 +3623,34 @@
35803623]
35813624
35823625[[package]]
3626name = "internal"
3627version = "1.0.0"
3628dependencies = [
3629 "axum",
3630 "cache",
3631 "config 1.0.0",
3632 "crypt",
3633 "futures-util",
3634 "hmac 0.13.0",
3635 "http 1.5.0",
3636 "oltp",
3637 "prost 0.14.4",
3638 "prost-types 0.14.4",
3639 "proto",
3640 "redis",
3641 "serde",
3642 "serde_json",
3643 "sha2 0.11.1",
3644 "thiserror 2.0.21",
3645 "tokio",
3646 "tokio-stream",
3647 "tonic",
3648 "tower",
3649 "tracing",
3650 "uuid",
3651]
3652
3653[[package]]
35833654name = "inventory"
35843655version = "0.3.25"
35853656source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6928,6 +6999,17 @@
69286999]
69297000
69307001[[package]]
7002name = "serde_path_to_error"
7003version = "0.1.20"
7004source = "registry+https://github.com/rust-lang/crates.io-index"
7005checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
7006dependencies = [
7007 "itoa",
7008 "serde",
7009 "serde_core",
7010]
7011
7012[[package]]
69317013name = "serde_spanned"
69327014version = "0.6.9"
69337015source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7173,6 +7255,24 @@
71737255]
71747256
71757257[[package]]
7258name = "socket"
7259version = "1.0.0"
7260dependencies = [
7261 "async-trait",
7262 "base64 0.23.1",
7263 "config 1.0.0",
7264 "futures-util",
7265 "http-body-util",
7266 "hyper 1.12.0",
7267 "hyper-util",
7268 "serde",
7269 "serde_json",
7270 "tokio",
7271 "tokio-tungstenite",
7272 "uuid",
7273]
7274
7275[[package]]
71767276name = "socket2"
71777277version = "0.5.10"
71787278source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7211,6 +7311,7 @@
72117311 "export",
72127312 "i18n",
72137313 "imaging",
7314 "internal",
72147315 "models",
72157316 "prost-types 0.14.4",
72167317 "proto",
@@ -7222,6 +7323,7 @@
72227323 "sha2 0.11.1",
72237324 "storage",
72247325 "time 0.3.55",
7326 "tonic",
72257327 "tracing",
72267328 "utoipa 5.5.0",
72277329 "uuid",
Modified Cargo.toml Cargo.toml +10 −1 View Cargo.toml
@@ -41,6 +41,9 @@
4141pulls = { path = "lib/pulls" }
4242social = { path = "lib/social" }
4343queue = { path = "lib/queue" }
44actor = { path = "lib/actor" }
45socket = { path = "lib/socket" }
46internal = { path = "lib/internal" }
4447
4548
4649
@@ -65,7 +68,7 @@
6568rand = "0.8"
6669sha2 = "0.11.0"
6770hmac = "0.13"
68uuid = { version = "1", features = ["v7", "serde"] }
71uuid = { version = "1", features = ["v4", "v7", "serde"] }
6972time = { version = "0.3", features = ["serde", "serde-human-readable"] }
7073bytes = "1"
7174futures = "0.3"
@@ -96,3 +99,9 @@
9699tonic-prost-build = "0.14"
97100rocketmq-client-rust = "0.9.0"
98101rocketmq-common = "0.9.0"
102
103hyper = { version = "1.12.0", features = ["http1", "server"] }
104hyper-util = { version = "0.1.21", features = ["tokio", "client-legacy", "http1"] }
105http-body-util = "0.1.5"
106tokio-tungstenite = { version = "0.29.0", default-features = false, features = ["handshake"] }
107futures-util = { version = "0.3.34", features = ["sink"] }