openzeppelin_relayer/services/plugins/
shared_socket.rs

1//! Shared Socket Service
2//!
3//! This module provides a unified bidirectional Unix socket service for plugin communication.
4//! Instead of creating separate sockets for registration and API calls, all communication
5//! happens over a single shared socket, dramatically reducing overhead and complexity.
6//!
7//! ## Architecture
8//!
9//! **Single Shared Socket**: All plugins connect to `/tmp/relayer-plugin-shared.sock`
10//!
11//! **Bidirectional Communication**:
12//! - Plugins → Host: Register, ApiRequest, Trace, Shutdown
13//! - Host → Plugins: ApiResponse
14//!
15//! **Connection Tagging (Security)**: Each connection is "tagged" with an execution_id
16//! after the first Register message. All subsequent messages are validated against this
17//! tagged ID to prevent spoofing attacks (Plugin A cannot impersonate Plugin B).
18//!
19//! ## Message Protocol
20//!
21//! All messages are JSON objects with a `type` field that discriminates the message type:
22//!
23//! ### Plugin → Host Messages
24//!
25//! **Register** (first message, required):
26//! ```json
27//! {
28//!   "type": "register",
29//!   "execution_id": "abc-123"
30//! }
31//! ```
32//!
33//! **ApiRequest** (call Relayer API):
34//! ```json
35//! {
36//!   "type": "api_request",
37//!   "request_id": "req-1",
38//!   "relayer_id": "relayer-1",
39//!   "method": "sendTransaction",
40//!   "payload": { "to": "0x...", "value": "100" }
41//! }
42//! ```
43//!
44//! **Trace** (observability event):
45//! ```json
46//! {
47//!   "type": "trace",
48//!   "trace": { "event": "processing", "timestamp": 1234567890 }
49//! }
50//! ```
51//!
52//! **Shutdown** (graceful close):
53//! ```json
54//! {
55//!   "type": "shutdown"
56//! }
57//! ```
58//!
59//! ### Host → Plugin Messages
60//!
61//! **ApiResponse** (Relayer API result):
62//! ```json
63//! {
64//!   "type": "api_response",
65//!   "request_id": "req-1",
66//!   "result": { "id": "tx-123", "status": "success" },
67//!   "error": null
68//! }
69//! ```
70//!
71//! ## Security Model
72//!
73//! The connection tagging mechanism prevents execution_id spoofing:
74//!
75//! 1. Plugin connects to shared socket
76//! 2. Plugin sends Register message with execution_id
77//! 3. Host "tags" the connection (file descriptor) with that execution_id
78//! 4. All subsequent messages are validated against the tagged ID
79//! 5. Attempts to change execution_id are rejected and connection is closed
80//!
81//! This ensures Plugin A cannot send requests pretending to be Plugin B, even though
82//! they share the same socket file.
83//!
84//! ## Backward Compatibility
85//!
86//! The handle_connection method maintains backward compatibility with the legacy
87//! Request/Response format from socket.rs. If a message doesn't parse as PluginMessage,
88//! it attempts to parse as the legacy Request format and handles it accordingly.
89//!
90//! ## Performance Benefits vs Per-Execution Sockets
91//!
92//! | Metric | Shared Socket | Per-Execution Socket |
93//! |--------|---------------|----------------------|
94//! | File descriptors | 1 per plugin | 2 per plugin |
95//! | Syscalls | ~50% fewer | Baseline |
96//! | Connection setup | Reuse existing | Create new each time |
97//! | Memory overhead | O(active executions) | O(active executions × 2) |
98//! | Debugging | Single stream | Two separate streams |
99//!
100//! ## Example Usage
101//!
102//! ```rust,no_run
103//! use openzeppelin_relayer::services::plugins::shared_socket::{
104//!     get_shared_socket_service, ensure_shared_socket_started
105//! };
106//!
107//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
108//! // Get the global shared socket instance
109//! let service = get_shared_socket_service()?;
110//!
111//! // Register an execution (returns RAII guard)
112//! let guard = service.register_execution("exec-123".to_string(), true).await;
113//!
114//! // Plugin connects and sends messages over the shared socket...
115//! // (handled automatically by the background listener)
116//!
117//! // Collect traces when done (returns Some when emit_traces=true)
118//! if let Some(mut traces_rx) = guard.into_receiver() {
119//!     let traces = traces_rx.recv().await;
120//! }
121//! # Ok(())
122//! # }
123//! ```
124
125use super::config::get_config;
126use crate::jobs::JobProducerTrait;
127use crate::models::{
128    NetworkRepoModel, NotificationRepoModel, RelayerRepoModel, SignerRepoModel, ThinDataAppState,
129    TransactionRepoModel,
130};
131use crate::repositories::{
132    ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository, Repository,
133    TransactionCounterTrait, TransactionRepository,
134};
135use crate::services::plugins::relayer_api::{RelayerApi, Request};
136use scc::HashMap as SccHashMap;
137use serde::{Deserialize, Serialize};
138use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
139use std::sync::Arc;
140use std::time::{Duration, Instant};
141use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
142use tokio::net::{UnixListener, UnixStream};
143use tokio::sync::{mpsc, watch, Semaphore};
144use tracing::{debug, info, warn};
145
146use super::PluginError;
147
148/// Log socket write errors at the appropriate level.
149/// Broken pipe and connection reset are expected when a plugin times out
150/// while an RPC call is still in-flight, so they're logged at DEBUG.
151fn log_socket_write_error(context: &str, error: &std::io::Error) {
152    match error.kind() {
153        std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::ConnectionReset => {
154            debug!(
155                "Failed to write {}: {} (plugin likely timed out)",
156                context, error
157            );
158        }
159        _ => {
160            warn!("Failed to write {}: {}", context, error);
161        }
162    }
163}
164
165/// Unified message protocol for bidirectional communication
166#[derive(Debug, Serialize, Deserialize, Clone)]
167#[serde(tag = "type", rename_all = "snake_case")]
168pub enum PluginMessage {
169    /// Plugin registers its execution_id (first message from plugin)
170    Register { execution_id: String },
171    /// Plugin requests a Relayer API call
172    ApiRequest {
173        request_id: String,
174        relayer_id: String,
175        method: crate::services::plugins::relayer_api::PluginMethod,
176        payload: serde_json::Value,
177    },
178    /// Host responds to an API request
179    ApiResponse {
180        request_id: String,
181        result: Option<serde_json::Value>,
182        error: Option<String>,
183    },
184    /// Plugin sends a trace event (for observability)
185    Trace { trace: serde_json::Value },
186    /// Plugin signals completion
187    Shutdown,
188}
189
190/// Execution context for trace collection
191struct ExecutionContext {
192    /// Channel to send traces back to the execution (None when emit_traces=false)
193    /// When None, connection handler skips trace collection entirely for better performance
194    traces_tx: Option<mpsc::Sender<Vec<serde_json::Value>>>,
195    /// Creation timestamp for TTL cleanup
196    created_at: Instant,
197    /// The execution_id bound to this connection (for security)
198    /// Once set, all messages must match this ID to prevent spoofing
199    #[allow(dead_code)] // Used for security validation, not directly read
200    bound_execution_id: String,
201}
202
203/// RAII guard for execution registration that auto-unregisters on drop
204pub struct ExecutionGuard {
205    execution_id: String,
206    executions: Arc<SccHashMap<String, ExecutionContext>>,
207    rx: Option<mpsc::Receiver<Vec<serde_json::Value>>>,
208    /// Shared counter for tracking active executions (lock-free)
209    active_count: Arc<AtomicUsize>,
210    /// Whether this guard was successfully registered (insertion succeeded)
211    /// Only registered guards should decrement active_count on drop
212    registered: bool,
213}
214
215impl ExecutionGuard {
216    /// Get the trace receiver if tracing was enabled
217    /// Returns None if emit_traces=false was passed to register_execution
218    pub fn into_receiver(mut self) -> Option<mpsc::Receiver<Vec<serde_json::Value>>> {
219        self.rx.take()
220    }
221}
222
223impl Drop for ExecutionGuard {
224    fn drop(&mut self) {
225        // Auto-unregister on drop - synchronous with scc::HashMap (no spawn needed!)
226        // This eliminates the overhead of spawning a task for every request
227        //
228        // Only registered guards should remove entries and decrement counters.
229        // Non-registered guards (from duplicate execution_id) don't own the entry.
230        //
231        // For registered guards, only decrement if we actually removed the entry.
232        // This prevents double-decrement: if a long-running execution is GC'd by the
233        // stale entry cleanup task (which decrements the counter), and then the guard
234        // drops later, we must NOT decrement again.
235        if self.registered && self.executions.remove(&self.execution_id).is_some() {
236            self.active_count.fetch_sub(1, Ordering::AcqRel);
237        }
238    }
239}
240
241/// Shared socket service that handles multiple concurrent plugin executions
242pub struct SharedSocketService {
243    /// Socket path
244    socket_path: String,
245    /// Active execution contexts (execution_id -> ExecutionContext)
246    /// scc::HashMap provides lock-free reads and optimistic locking for writes
247    executions: Arc<SccHashMap<String, ExecutionContext>>,
248    /// Lock-free counter for active executions
249    active_count: Arc<AtomicUsize>,
250    /// Whether the listener has been started (instance-level flag)
251    started: AtomicBool,
252    /// Shutdown signal sender
253    shutdown_tx: watch::Sender<bool>,
254    /// Semaphore for connection limiting (prevents race conditions)
255    connection_semaphore: Arc<Semaphore>,
256}
257
258/// Handle to the multi-thread pipeline runtime. Set once at startup
259/// ([`set_pipeline_handle`]) so the shared socket service's background tasks
260/// (cleanup, listener, per-connection handlers) are spawned onto the pipeline
261/// runtime's worker threads instead of the actix System arbiter's single thread
262/// — the same single-thread saturation that caused the plugin-endpoint 504 storms.
263/// Falls back to `tokio::spawn` (the current runtime) when unset, e.g. in tests.
264static PIPELINE_HANDLE: std::sync::OnceLock<tokio::runtime::Handle> = std::sync::OnceLock::new();
265
266/// Register the pipeline runtime handle used to re-home socket service tasks.
267/// Idempotent: the first call wins (later calls are ignored).
268pub fn set_pipeline_handle(handle: tokio::runtime::Handle) {
269    let _ = PIPELINE_HANDLE.set(handle);
270}
271
272/// Spawn a future onto the pipeline runtime when its handle has been registered,
273/// otherwise onto the current runtime (`tokio::spawn`).
274fn spawn_on_pipeline<F>(fut: F) -> tokio::task::JoinHandle<F::Output>
275where
276    F: std::future::Future + Send + 'static,
277    F::Output: Send + 'static,
278{
279    match PIPELINE_HANDLE.get() {
280        Some(handle) => handle.spawn(fut),
281        None => tokio::spawn(fut),
282    }
283}
284
285impl SharedSocketService {
286    /// Create a new shared socket service
287    pub fn new(socket_path: &str) -> Result<Self, PluginError> {
288        // Remove existing socket file if it exists (from previous runs or crashed processes)
289        let _ = std::fs::remove_file(socket_path);
290
291        let (shutdown_tx, _) = watch::channel(false);
292
293        // Use centralized config
294        let config = get_config();
295        let max_connections = config.socket_max_connections;
296
297        let executions: Arc<SccHashMap<String, ExecutionContext>> = Arc::new(SccHashMap::new());
298        let active_count = Arc::new(AtomicUsize::new(0));
299
300        // Spawn background cleanup task for stale executions (prevents memory leaks)
301        let executions_clone = executions.clone();
302        let active_count_clone = active_count.clone();
303        let mut cleanup_shutdown_rx = shutdown_tx.subscribe();
304        spawn_on_pipeline(async move {
305            let mut interval = tokio::time::interval(Duration::from_secs(60));
306            loop {
307                tokio::select! {
308                    _ = interval.tick() => {}
309                    _ = cleanup_shutdown_rx.changed() => {
310                        if *cleanup_shutdown_rx.borrow() {
311                            break;
312                        }
313                    }
314                }
315                let now = Instant::now();
316                // scc::HashMap retain is lock-free per entry
317                let mut removed = 0usize;
318                executions_clone.retain(|_, ctx| {
319                    let keep = now.duration_since(ctx.created_at) < Duration::from_secs(300);
320                    if !keep {
321                        removed += 1;
322                    }
323                    keep
324                });
325                if removed > 0 {
326                    active_count_clone.fetch_sub(removed, Ordering::AcqRel);
327                }
328            }
329        });
330
331        Ok(Self {
332            socket_path: socket_path.to_string(),
333            executions,
334            active_count,
335            started: AtomicBool::new(false),
336            shutdown_tx,
337            connection_semaphore: Arc::new(Semaphore::new(max_connections)),
338        })
339    }
340
341    pub fn socket_path(&self) -> &str {
342        &self.socket_path
343    }
344
345    /// Register an execution and return a guard that auto-unregisters on drop
346    /// This prevents memory leaks from forgotten unregister calls
347    ///
348    /// # Arguments
349    /// * `execution_id` - Unique identifier for this execution
350    /// * `emit_traces` - If false, skips channel creation and trace collection for better performance
351    pub async fn register_execution(
352        &self,
353        execution_id: String,
354        emit_traces: bool,
355    ) -> ExecutionGuard {
356        // Only create channel when traces are needed - saves allocation and channel overhead
357        let (tx, rx) = if emit_traces {
358            let (tx, rx) = mpsc::channel(1);
359            (Some(tx), Some(rx))
360        } else {
361            (None, None)
362        };
363
364        let ctx = ExecutionContext {
365            traces_tx: tx,
366            created_at: Instant::now(),
367            bound_execution_id: execution_id.clone(),
368        };
369
370        // scc::HashMap insert - returns Ok if new, Err if key existed (duplicate)
371        let registered = match self.executions.insert(execution_id.clone(), ctx) {
372            Ok(_) => {
373                self.active_count.fetch_add(1, Ordering::AcqRel);
374                true
375            }
376            Err((existing_key, _)) => {
377                tracing::warn!(
378                    execution_id = %existing_key,
379                    "Duplicate execution_id detected during registration, guard will not decrement counter"
380                );
381                false
382            }
383        };
384
385        ExecutionGuard {
386            execution_id,
387            executions: self.executions.clone(),
388            rx,
389            registered,
390            active_count: self.active_count.clone(),
391        }
392    }
393
394    /// Get current number of available connection slots
395    pub fn available_connection_slots(&self) -> usize {
396        self.connection_semaphore.available_permits()
397    }
398
399    /// Get current active connection count
400    pub fn active_connection_count(&self) -> usize {
401        get_config().socket_max_connections - self.connection_semaphore.available_permits()
402    }
403
404    /// Get current number of registered executions (lock-free via atomic counter)
405    pub async fn registered_executions_count(&self) -> usize {
406        self.active_count.load(Ordering::Relaxed)
407    }
408
409    /// Signal shutdown to the listener and wait for active connections to drain
410    pub async fn shutdown(&self) {
411        let _ = self.shutdown_tx.send(true);
412        info!("Shared socket service: shutdown signal sent");
413
414        // Wait for active connections to drain (max 30 seconds)
415        let max_wait = Duration::from_secs(30);
416        let start = Instant::now();
417
418        while start.elapsed() < max_wait {
419            let available = self.connection_semaphore.available_permits();
420            if available == get_config().socket_max_connections {
421                // All permits returned - no active connections
422                break;
423            }
424            tokio::time::sleep(Duration::from_millis(100)).await;
425        }
426
427        // Remove socket file after connections drained
428        let _ = std::fs::remove_file(&self.socket_path);
429        info!("Shared socket service: shutdown complete");
430    }
431
432    /// Start the shared socket service
433    /// This spawns a background task that listens for connections
434    /// Safe to call multiple times - will only start once per instance
435    #[allow(clippy::type_complexity)]
436    pub async fn start<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
437        self: Arc<Self>,
438        state: Arc<ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>>,
439    ) -> Result<(), PluginError>
440    where
441        J: JobProducerTrait + Send + Sync + 'static,
442        RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
443        TR: TransactionRepository
444            + Repository<TransactionRepoModel, String>
445            + Send
446            + Sync
447            + 'static,
448        NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
449        NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
450        SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
451        TCR: TransactionCounterTrait + Send + Sync + 'static,
452        PR: PluginRepositoryTrait + Send + Sync + 'static,
453        AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
454    {
455        // Check if already started (instance-level flag)
456        if self.started.swap(true, Ordering::Acquire) {
457            return Ok(());
458        }
459
460        // Create the listener and move it into the task
461        let listener = UnixListener::bind(&self.socket_path)
462            .map_err(|e| PluginError::SocketError(format!("Failed to bind listener: {e}")))?;
463        let executions = self.executions.clone();
464        let relayer_api = Arc::new(RelayerApi);
465        let socket_path = self.socket_path.clone();
466        let mut shutdown_rx = self.shutdown_tx.subscribe();
467        let connection_semaphore = self.connection_semaphore.clone();
468
469        debug!(
470            "Shared socket service: starting listener on {}",
471            socket_path
472        );
473
474        // Spawn the listener task onto the pipeline runtime.
475        spawn_on_pipeline(async move {
476            debug!("Shared socket service: listener task started");
477            // Bound consecutive accept() failures so a shutting-down runtime can never
478            // spin this loop (see the Err arm below).
479            const MAX_CONSECUTIVE_ACCEPT_ERRORS: u32 = 10;
480            let mut consecutive_accept_errors: u32 = 0;
481            loop {
482                tokio::select! {
483                    // Check for shutdown signal
484                    _ = shutdown_rx.changed() => {
485                        if *shutdown_rx.borrow() {
486                            info!("Shared socket service: shutting down listener");
487                            break;
488                        }
489                    }
490                    // Accept new connections
491                    accept_result = listener.accept() => {
492                        match accept_result {
493                            Ok((stream, _)) => {
494                                consecutive_accept_errors = 0;
495                                // Try to acquire semaphore permit (no race condition!)
496                                match connection_semaphore.clone().try_acquire_owned() {
497                                    Ok(permit) => {
498                                        debug!("Shared socket service: accepted new connection");
499
500                                        let relayer_api_clone = relayer_api.clone();
501                                        let state_clone = Arc::clone(&state);
502                                        let executions_clone = executions.clone();
503
504                                        spawn_on_pipeline(async move {
505                                            // Permit held until task completes (auto-released on drop)
506                                            let _permit = permit;
507
508                                            let result = Self::handle_connection(
509                                                stream,
510                                                relayer_api_clone,
511                                                state_clone,
512                                                executions_clone,
513                                            )
514                                            .await;
515
516                                            if let Err(e) = result {
517                                                debug!("Connection handler finished with error: {}", e);
518                                            }
519                                        });
520                                    }
521                                    Err(_) => {
522                                        warn!(
523                                            "Connection limit reached, rejecting new connection. \
524                                            Consider increasing PLUGIN_MAX_CONCURRENCY or PLUGIN_SOCKET_MAX_CONCURRENT_CONNECTIONS."
525                                        );
526                                        drop(stream);
527                                    }
528                                }
529                            }
530                            Err(e) => {
531                                // If shutdown was requested, exit cleanly rather than
532                                // treating it as an accept failure.
533                                if *shutdown_rx.borrow() {
534                                    info!("Shared socket service: shutting down listener (accept interrupted)");
535                                    break;
536                                }
537                                consecutive_accept_errors += 1;
538                                warn!(
539                                    "Error accepting connection (attempt {}): {}",
540                                    consecutive_accept_errors, e
541                                );
542                                // Guard against a hot spin when accept() fails
543                                // persistently (e.g. the pipeline runtime is being torn
544                                // down out from under this loop): stop after a bounded
545                                // number of consecutive failures instead of logging
546                                // unboundedly.
547                                if consecutive_accept_errors >= MAX_CONSECUTIVE_ACCEPT_ERRORS {
548                                    warn!(
549                                        "Shared socket service: {} consecutive accept errors; stopping listener",
550                                        consecutive_accept_errors
551                                    );
552                                    break;
553                                }
554                                tokio::time::sleep(Duration::from_millis(20)).await;
555                            }
556                        }
557                    }
558                }
559            }
560
561            // Cleanup on shutdown
562            let _ = std::fs::remove_file(&socket_path);
563            info!("Shared socket service: listener stopped");
564        });
565
566        Ok(())
567    }
568
569    /// Handle a connection from a plugin.
570    ///
571    /// The inactivity timeout resets on every message — no hard wall-clock cap.
572    /// Connections stay alive as long as they're active, which is essential for
573    /// reused/pooled sockets and plugins making external calls.
574    ///
575    /// Security: The first message must be a Register message. Once registered,
576    /// the connection is "tagged" with that execution_id and cannot be changed.
577    /// This prevents Plugin A from spoofing Plugin B's execution_id.
578    #[allow(clippy::type_complexity)]
579    async fn handle_connection<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
580        stream: UnixStream,
581        relayer_api: Arc<RelayerApi>,
582        state: Arc<ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>>,
583        executions: Arc<SccHashMap<String, ExecutionContext>>,
584    ) -> Result<(), PluginError>
585    where
586        J: JobProducerTrait + Send + Sync + 'static,
587        RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
588        TR: TransactionRepository
589            + Repository<TransactionRepoModel, String>
590            + Send
591            + Sync
592            + 'static,
593        NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
594        NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
595        SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
596        TCR: TransactionCounterTrait + Send + Sync + 'static,
597        PR: PluginRepositoryTrait + Send + Sync + 'static,
598        AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
599    {
600        let (r, mut w) = stream.into_split();
601        let mut reader = BufReader::new(r).lines();
602
603        // Only allocate traces Vec when tracing is enabled (determined on Register)
604        let mut traces: Option<Vec<serde_json::Value>> = None;
605        // Track whether traces are enabled for this connection (set on Register)
606        let mut traces_enabled = false;
607
608        // Connection-bound execution_id (prevents spoofing)
609        // Once set, this cannot be changed for the lifetime of the connection
610        let mut bound_execution_id: Option<String> = None;
611
612        // Safety timeout: reap connections that are silent for longer than the maximum
613        // plugin execution timeout + margin. This prevents permit exhaustion from stuck
614        // or rogue clients. Not configurable — it's derived from DEFAULT_PLUGIN_TIMEOUT_SECONDS
615        // so it can never desync with execution timeouts.
616        let safety_timeout =
617            Duration::from_secs(crate::constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS + 60);
618
619        loop {
620            // Read next line with safety timeout. In normal operation the client sends
621            // messages or closes the socket (EOF) well within this window. The timeout
622            // only fires for truly orphaned connections (stuck event loop, rogue client).
623            let line = match tokio::time::timeout(safety_timeout, reader.next_line()).await {
624                Ok(Ok(Some(line))) => line,
625                Ok(Ok(None)) => break, // EOF — client closed connection
626                Ok(Err(e)) => {
627                    warn!("Error reading from connection: {}", e);
628                    break;
629                }
630                Err(_) => {
631                    debug!(
632                        "Connection safety timeout reached ({}s with no message)",
633                        safety_timeout.as_secs()
634                    );
635                    break;
636                }
637            };
638
639            debug!("Shared socket service: received message");
640
641            // Parse once, discriminate on "type" field for efficiency
642            let json_value: serde_json::Value = match serde_json::from_str(&line) {
643                Ok(v) => v,
644                Err(e) => {
645                    warn!("Failed to parse JSON: {}", e);
646                    continue;
647                }
648            };
649
650            let has_type_field = json_value.get("type").is_some();
651
652            if has_type_field {
653                // New unified protocol
654                let message: PluginMessage = match serde_json::from_value(json_value) {
655                    Ok(msg) => msg,
656                    Err(e) => {
657                        warn!("Failed to parse PluginMessage: {}", e);
658                        continue;
659                    }
660                };
661
662                // Handle message based on type
663                match message {
664                    PluginMessage::Register { execution_id } => {
665                        // First message must be Register
666                        if bound_execution_id.is_some() {
667                            warn!("Attempted to re-register connection (security violation)");
668                            break;
669                        }
670
671                        // Validate execution_id exists in registry and check if tracing is enabled
672                        // scc::HashMap read() is lock-free
673                        if let Some(has_traces) =
674                            executions.read(&execution_id, |_, ctx| ctx.traces_tx.is_some())
675                        {
676                            traces_enabled = has_traces;
677                        } else {
678                            warn!("Unknown execution_id: {}", execution_id);
679                            break;
680                        }
681
682                        debug!(
683                            execution_id = %execution_id,
684                            traces_enabled = traces_enabled,
685                            "Connection registered"
686                        );
687                        bound_execution_id = Some(execution_id);
688                    }
689
690                    PluginMessage::ApiRequest {
691                        request_id,
692                        relayer_id,
693                        method,
694                        payload,
695                    } => {
696                        // Must be registered first
697                        let exec_id = match &bound_execution_id {
698                            Some(id) => id,
699                            None => {
700                                warn!("ApiRequest before Register (security violation)");
701                                break;
702                            }
703                        };
704
705                        // Create Request for RelayerApi (method is already PluginMethod)
706                        let request = Request {
707                            request_id: request_id.clone(),
708                            relayer_id,
709                            method,
710                            payload,
711                            http_request_id: Some(exec_id.clone()),
712                        };
713
714                        // Handle the request
715                        let response = relayer_api.handle_request(request, &state).await;
716
717                        // Send ApiResponse back
718                        let api_response = PluginMessage::ApiResponse {
719                            request_id: response.request_id,
720                            result: response.result,
721                            error: response.error,
722                        };
723
724                        let response_str = serde_json::to_string(&api_response)
725                            .map_err(|e| PluginError::PluginError(e.to_string()))?
726                            + "\n";
727
728                        if let Err(e) = w.write_all(response_str.as_bytes()).await {
729                            log_socket_write_error("API response", &e);
730                            break;
731                        }
732
733                        if let Err(e) = w.flush().await {
734                            log_socket_write_error("API response flush", &e);
735                            break;
736                        }
737                    }
738
739                    PluginMessage::Trace { trace } => {
740                        // Only collect traces if tracing is enabled for this execution
741                        if traces_enabled {
742                            if traces.is_none() {
743                                traces = Some(Vec::new());
744                            }
745                            if let Some(ref mut t) = traces {
746                                t.push(trace);
747                            }
748                        }
749                        // When traces_enabled=false, silently discard trace messages
750                    }
751
752                    PluginMessage::Shutdown => {
753                        debug!("Plugin requested shutdown");
754                        break;
755                    }
756
757                    PluginMessage::ApiResponse { .. } => {
758                        warn!("Received ApiResponse from plugin (invalid direction)");
759                        continue;
760                    }
761                }
762            } else {
763                // Legacy protocol (no "type" field)
764                if let Ok(request) = serde_json::from_value::<Request>(json_value.clone()) {
765                    // Legacy format - API requests are not trace events
766
767                    // Set execution_id from http_request_id or request_id if not bound
768                    if bound_execution_id.is_none() {
769                        let candidate_id = request
770                            .http_request_id
771                            .clone()
772                            .or_else(|| Some(request.request_id.clone()));
773
774                        // Validate execution_id exists (same as new protocol)
775                        // scc::HashMap read() is lock-free
776                        if let Some(ref id) = candidate_id {
777                            if let Some(has_traces) =
778                                executions.read(id, |_, ctx| ctx.traces_tx.is_some())
779                            {
780                                traces_enabled = has_traces;
781                                bound_execution_id = candidate_id;
782                            } else {
783                                debug!("Legacy request with unknown execution_id: {}", id);
784                            }
785                        }
786                    }
787
788                    // Handle legacy request
789                    let response = relayer_api.handle_request(request, &state).await;
790                    let response_str = serde_json::to_string(&response)
791                        .map_err(|e| PluginError::PluginError(e.to_string()))?
792                        + "\n";
793
794                    if let Err(e) = w.write_all(response_str.as_bytes()).await {
795                        log_socket_write_error("response", &e);
796                        break;
797                    }
798
799                    if let Err(e) = w.flush().await {
800                        log_socket_write_error("response flush", &e);
801                        break;
802                    }
803                } else {
804                    warn!("Failed to parse message as either PluginMessage or legacy Request");
805                }
806            }
807        }
808
809        // Send traces back to caller if tracing was enabled
810        if traces_enabled {
811            if let Some(exec_id) = bound_execution_id {
812                // Get the sender from execution context (lock-free read)
813                let traces_tx = executions
814                    .read(&exec_id, |_, ctx| ctx.traces_tx.clone())
815                    .flatten();
816
817                if let Some(tx) = traces_tx {
818                    let collected_traces = traces.unwrap_or_default();
819                    let trace_count = collected_traces.len();
820                    // Short timeout: in-process channel send should be nearly instant
821                    // If receiver isn't ready in 100ms, drop traces rather than blocking
822                    match tokio::time::timeout(
823                        Duration::from_millis(100),
824                        tx.send(collected_traces),
825                    )
826                    .await
827                    {
828                        Ok(Ok(())) => {}
829                        Ok(Err(_)) => {
830                            if trace_count > 0 {
831                                warn!(
832                                    "Trace channel closed for execution_id: {} ({} traces lost)",
833                                    exec_id, trace_count
834                                );
835                            }
836                        }
837                        Err(_) => warn!("Timeout sending traces for execution_id: {}", exec_id),
838                    }
839                }
840            }
841        }
842        // When traces_enabled=false, no channel exists and we skip all trace-related work
843
844        debug!("Shared socket service: connection closed");
845        Ok(())
846    }
847}
848
849impl Drop for SharedSocketService {
850    fn drop(&mut self) {
851        // Signal shutdown (cleanup happens in shutdown() method)
852        let _ = self.shutdown_tx.send(true);
853        // Note: Socket file cleanup happens in shutdown() after connections drain
854        // Drop can't be async, so proper cleanup should use shutdown() method
855    }
856}
857
858/// Global shared socket service instance with proper error handling
859static SHARED_SOCKET: std::sync::OnceLock<Result<Arc<SharedSocketService>, String>> =
860    std::sync::OnceLock::new();
861
862/// Get or create the global shared socket service
863/// Returns error if initialization fails instead of panicking
864pub fn get_shared_socket_service() -> Result<Arc<SharedSocketService>, PluginError> {
865    let socket_path = "/tmp/relayer-plugin-shared.sock";
866
867    let result = SHARED_SOCKET.get_or_init(|| {
868        // Remove existing socket file if it exists (from previous runs)
869        let _ = std::fs::remove_file(socket_path);
870
871        match SharedSocketService::new(socket_path) {
872            Ok(service) => Ok(Arc::new(service)),
873            Err(e) => Err(e.to_string()),
874        }
875    });
876
877    match result {
878        Ok(service) => Ok(service.clone()),
879        Err(e) => Err(PluginError::SocketError(format!(
880            "Failed to create shared socket service: {e}"
881        ))),
882    }
883}
884
885/// Signal the global shared socket listener to stop, if it was ever started.
886///
887/// Must be called during graceful shutdown BEFORE the pipeline runtime (which hosts
888/// the accept loop) is torn down, so the loop breaks on its `watch` signal instead of
889/// spinning on `accept()` errors from a shutting-down tokio context. Uses
890/// `SHARED_SOCKET.get()` (not `get_or_init`) so it is a true no-op when the service was
891/// never initialized (i.e. no plugin ever executed).
892pub async fn shutdown_shared_socket_service() {
893    if let Some(Ok(service)) = SHARED_SOCKET.get() {
894        service.shutdown().await;
895    }
896}
897
898/// Ensure the shared socket service is started
899#[allow(clippy::type_complexity)]
900pub async fn ensure_shared_socket_started<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
901    state: Arc<ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>>,
902) -> Result<(), PluginError>
903where
904    J: JobProducerTrait + Send + Sync + 'static,
905    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
906    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
907    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
908    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
909    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
910    TCR: TransactionCounterTrait + Send + Sync + 'static,
911    PR: PluginRepositoryTrait + Send + Sync + 'static,
912    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
913{
914    let service = get_shared_socket_service()?;
915    service.start(state).await
916}
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921    use crate::utils::mocks::mockutils::create_mock_app_state;
922    use actix_web::web;
923    use tempfile::tempdir;
924    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
925    use tokio::net::UnixStream;
926
927    #[tokio::test]
928    async fn test_unified_protocol_register_and_api_request() {
929        let temp_dir = tempdir().unwrap();
930        let socket_path = temp_dir.path().join("shared.sock");
931
932        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
933        let state = create_mock_app_state(None, None, None, None, None, None).await;
934
935        // Start the service
936        service
937            .clone()
938            .start(Arc::new(web::ThinData(state)))
939            .await
940            .unwrap();
941
942        // Register execution
943        let execution_id = "test-exec-123".to_string();
944        let _guard = service.register_execution(execution_id.clone(), true).await;
945
946        // Give the listener time to start
947        tokio::time::sleep(Duration::from_millis(50)).await;
948
949        // Connect as plugin
950        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
951            .await
952            .unwrap();
953
954        // Send Register message
955        let register_msg = PluginMessage::Register {
956            execution_id: execution_id.clone(),
957        };
958        let msg_json = serde_json::to_string(&register_msg).unwrap() + "\n";
959        client.write_all(msg_json.as_bytes()).await.unwrap();
960
961        // Send ApiRequest
962        let api_request = PluginMessage::ApiRequest {
963            request_id: "req-1".to_string(),
964            relayer_id: "relayer-1".to_string(),
965            method: crate::services::plugins::relayer_api::PluginMethod::GetRelayerStatus,
966            payload: serde_json::json!({}),
967        };
968        let req_json = serde_json::to_string(&api_request).unwrap() + "\n";
969        client.write_all(req_json.as_bytes()).await.unwrap();
970        client.flush().await.unwrap();
971
972        // Read ApiResponse
973        let (r, _w) = client.into_split();
974        let mut reader = BufReader::new(r);
975        let mut response_line = String::new();
976        reader.read_line(&mut response_line).await.unwrap();
977
978        let response: PluginMessage = serde_json::from_str(&response_line).unwrap();
979        match response {
980            PluginMessage::ApiResponse { request_id, .. } => {
981                assert_eq!(request_id, "req-1");
982            }
983            _ => panic!("Expected ApiResponse, got {response:?}"),
984        }
985
986        drop(reader);
987        drop(_w);
988        service.shutdown().await;
989    }
990
991    #[tokio::test]
992    async fn test_connection_tagging_prevents_spoofing() {
993        let temp_dir = tempdir().unwrap();
994        let socket_path = temp_dir.path().join("shared2.sock");
995
996        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
997        let state = create_mock_app_state(None, None, None, None, None, None).await;
998
999        service
1000            .clone()
1001            .start(Arc::new(web::ThinData(state)))
1002            .await
1003            .unwrap();
1004
1005        let execution_id = "test-exec-456".to_string();
1006        let _guard = service.register_execution(execution_id.clone(), true).await;
1007
1008        tokio::time::sleep(Duration::from_millis(50)).await;
1009
1010        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1011            .await
1012            .unwrap();
1013
1014        // Register with execution_id
1015        let register_msg = PluginMessage::Register {
1016            execution_id: execution_id.clone(),
1017        };
1018        let msg_json = serde_json::to_string(&register_msg).unwrap() + "\n";
1019        client.write_all(msg_json.as_bytes()).await.unwrap();
1020
1021        // Try to re-register with different execution_id (security violation)
1022        let spoofed_register = PluginMessage::Register {
1023            execution_id: "different-exec-id".to_string(),
1024        };
1025        let spoofed_json = serde_json::to_string(&spoofed_register).unwrap() + "\n";
1026        client.write_all(spoofed_json.as_bytes()).await.unwrap();
1027        client.flush().await.unwrap();
1028
1029        // Connection should be closed by server
1030        tokio::time::sleep(Duration::from_millis(100)).await;
1031
1032        // Try to read - should get EOF since connection was closed
1033        let (r, _w) = client.into_split();
1034        let mut reader = BufReader::new(r);
1035        let mut line = String::new();
1036        let result = reader.read_line(&mut line).await;
1037
1038        // Should either get an error or EOF (0 bytes)
1039        assert!(result.is_err() || result.unwrap() == 0);
1040
1041        drop(reader);
1042        drop(_w);
1043        service.shutdown().await;
1044    }
1045
1046    #[tokio::test]
1047    async fn test_backward_compatibility_with_legacy_format() {
1048        let temp_dir = tempdir().unwrap();
1049        let socket_path = temp_dir.path().join("shared3.sock");
1050
1051        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1052        let state = create_mock_app_state(None, None, None, None, None, None).await;
1053
1054        service
1055            .clone()
1056            .start(Arc::new(web::ThinData(state)))
1057            .await
1058            .unwrap();
1059
1060        let execution_id = "test-exec-789".to_string();
1061        let _guard = service.register_execution(execution_id.clone(), true).await;
1062
1063        tokio::time::sleep(Duration::from_millis(50)).await;
1064
1065        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1066            .await
1067            .unwrap();
1068
1069        // Send legacy Request format (without PluginMessage wrapper)
1070        let legacy_request = crate::services::plugins::relayer_api::Request {
1071            request_id: "legacy-1".to_string(),
1072            relayer_id: "relayer-1".to_string(),
1073            method: crate::services::plugins::relayer_api::PluginMethod::GetRelayerStatus,
1074            payload: serde_json::json!({}),
1075            http_request_id: Some(execution_id.clone()),
1076        };
1077        let legacy_json = serde_json::to_string(&legacy_request).unwrap() + "\n";
1078        client.write_all(legacy_json.as_bytes()).await.unwrap();
1079        client.flush().await.unwrap();
1080
1081        // Read legacy Response format
1082        let (r, _w) = client.into_split();
1083        let mut reader = BufReader::new(r);
1084        let mut response_line = String::new();
1085        reader.read_line(&mut response_line).await.unwrap();
1086
1087        let response: crate::services::plugins::relayer_api::Response =
1088            serde_json::from_str(&response_line).unwrap();
1089
1090        assert_eq!(response.request_id, "legacy-1");
1091        // Note: GetRelayerStatus might return an error if relayer doesn't exist
1092        // The important thing is we got a response in the correct format
1093        assert!(response.result.is_some() || response.error.is_some());
1094
1095        drop(reader);
1096        drop(_w);
1097        service.shutdown().await;
1098    }
1099
1100    #[tokio::test]
1101    async fn test_trace_collection() {
1102        let temp_dir = tempdir().unwrap();
1103        let socket_path = temp_dir.path().join("shared4.sock");
1104
1105        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1106        let state = create_mock_app_state(None, None, None, None, None, None).await;
1107
1108        service
1109            .clone()
1110            .start(Arc::new(web::ThinData(state)))
1111            .await
1112            .unwrap();
1113
1114        let execution_id = "test-exec-trace".to_string();
1115        let guard = service.register_execution(execution_id.clone(), true).await;
1116
1117        tokio::time::sleep(Duration::from_millis(50)).await;
1118
1119        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1120            .await
1121            .unwrap();
1122
1123        // Register
1124        let register_msg = PluginMessage::Register {
1125            execution_id: execution_id.clone(),
1126        };
1127        client
1128            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
1129            .await
1130            .unwrap();
1131
1132        // Send trace events
1133        let trace1 = PluginMessage::Trace {
1134            trace: serde_json::json!({"event": "start", "timestamp": 1000}),
1135        };
1136        client
1137            .write_all((serde_json::to_string(&trace1).unwrap() + "\n").as_bytes())
1138            .await
1139            .unwrap();
1140
1141        let trace2 = PluginMessage::Trace {
1142            trace: serde_json::json!({"event": "processing", "timestamp": 2000}),
1143        };
1144        client
1145            .write_all((serde_json::to_string(&trace2).unwrap() + "\n").as_bytes())
1146            .await
1147            .unwrap();
1148
1149        // Shutdown
1150        let shutdown_msg = PluginMessage::Shutdown;
1151        client
1152            .write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
1153            .await
1154            .unwrap();
1155        client.flush().await.unwrap();
1156
1157        drop(client);
1158
1159        // Wait for connection to close and traces to be sent
1160        tokio::time::sleep(Duration::from_millis(100)).await;
1161
1162        // Collect traces
1163        let mut traces_rx = guard.into_receiver().expect("Traces should be enabled");
1164        let traces = traces_rx.recv().await.unwrap();
1165
1166        // Should have collected 2 trace events
1167        assert_eq!(traces.len(), 2);
1168        assert_eq!(traces[0]["event"], "start");
1169        assert_eq!(traces[1]["event"], "processing");
1170
1171        service.shutdown().await;
1172    }
1173
1174    #[tokio::test]
1175    async fn test_execution_guard_auto_unregister() {
1176        let temp_dir = tempdir().unwrap();
1177        let socket_path = temp_dir.path().join("shared_guard.sock");
1178
1179        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1180        let execution_id = "test-exec-guard".to_string();
1181
1182        {
1183            let _guard = service.register_execution(execution_id.clone(), true).await;
1184
1185            // Verify execution is registered (use atomic counter)
1186            assert_eq!(service.registered_executions_count().await, 1);
1187        }
1188        // Guard dropped here - synchronous removal with scc (no sleep needed!)
1189
1190        // Verify execution was auto-unregistered immediately
1191        assert_eq!(service.registered_executions_count().await, 0);
1192    }
1193
1194    #[tokio::test]
1195    async fn test_api_request_without_register_rejected() {
1196        let temp_dir = tempdir().unwrap();
1197        let socket_path = temp_dir.path().join("shared_no_register.sock");
1198
1199        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1200        let state = create_mock_app_state(None, None, None, None, None, None).await;
1201
1202        service
1203            .clone()
1204            .start(Arc::new(web::ThinData(state)))
1205            .await
1206            .unwrap();
1207
1208        tokio::time::sleep(Duration::from_millis(50)).await;
1209
1210        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1211            .await
1212            .unwrap();
1213
1214        // Send ApiRequest WITHOUT registering first (security violation)
1215        let api_request = PluginMessage::ApiRequest {
1216            request_id: "req-1".to_string(),
1217            relayer_id: "relayer-1".to_string(),
1218            method: crate::services::plugins::relayer_api::PluginMethod::GetRelayerStatus,
1219            payload: serde_json::json!({}),
1220        };
1221        let req_json = serde_json::to_string(&api_request).unwrap() + "\n";
1222        client.write_all(req_json.as_bytes()).await.unwrap();
1223        client.flush().await.unwrap();
1224
1225        // Connection should be closed by server
1226        tokio::time::sleep(Duration::from_millis(100)).await;
1227
1228        let (r, _w) = client.into_split();
1229        let mut reader = BufReader::new(r);
1230        let mut line = String::new();
1231        let result = reader.read_line(&mut line).await;
1232
1233        // Should get EOF (connection closed)
1234        assert!(result.is_err() || result.unwrap() == 0);
1235
1236        drop(reader);
1237        drop(_w);
1238        service.shutdown().await;
1239    }
1240
1241    #[tokio::test]
1242    async fn test_register_with_unknown_execution_id_rejected() {
1243        let temp_dir = tempdir().unwrap();
1244        let socket_path = temp_dir.path().join("shared_unknown_exec.sock");
1245
1246        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1247        let state = create_mock_app_state(None, None, None, None, None, None).await;
1248
1249        service
1250            .clone()
1251            .start(Arc::new(web::ThinData(state)))
1252            .await
1253            .unwrap();
1254
1255        tokio::time::sleep(Duration::from_millis(50)).await;
1256
1257        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1258            .await
1259            .unwrap();
1260
1261        // Try to register with an execution_id that doesn't exist in registry
1262        let register_msg = PluginMessage::Register {
1263            execution_id: "unknown-exec-id".to_string(),
1264        };
1265        let msg_json = serde_json::to_string(&register_msg).unwrap() + "\n";
1266        client.write_all(msg_json.as_bytes()).await.unwrap();
1267        client.flush().await.unwrap();
1268
1269        // Connection should be closed
1270        tokio::time::sleep(Duration::from_millis(100)).await;
1271
1272        let (r, _w) = client.into_split();
1273        let mut reader = BufReader::new(r);
1274        let mut line = String::new();
1275        let result = reader.read_line(&mut line).await;
1276
1277        assert!(result.is_err() || result.unwrap() == 0);
1278
1279        drop(reader);
1280        drop(_w);
1281        service.shutdown().await;
1282    }
1283
1284    #[tokio::test]
1285    async fn test_connection_limit_enforcement() {
1286        let temp_dir = tempdir().unwrap();
1287        let socket_path = temp_dir.path().join("shared_connection_limit.sock");
1288
1289        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1290        let state = create_mock_app_state(None, None, None, None, None, None).await;
1291
1292        service
1293            .clone()
1294            .start(Arc::new(web::ThinData(state)))
1295            .await
1296            .unwrap();
1297
1298        tokio::time::sleep(Duration::from_millis(50)).await;
1299
1300        // Check initial connection count
1301        let initial_permits = service.connection_semaphore.available_permits();
1302        let max_connections = get_config().socket_max_connections;
1303        assert_eq!(initial_permits, max_connections);
1304
1305        // Create a connection (should reduce available permits)
1306        let _client = UnixStream::connect(socket_path.to_str().unwrap())
1307            .await
1308            .unwrap();
1309
1310        tokio::time::sleep(Duration::from_millis(50)).await;
1311
1312        // Available permits should be reduced
1313        let after_connect = service.connection_semaphore.available_permits();
1314        assert!(after_connect < initial_permits);
1315
1316        drop(_client);
1317        service.shutdown().await;
1318    }
1319
1320    #[tokio::test]
1321    async fn test_connection_stays_alive_within_safety_timeout() {
1322        let temp_dir = tempdir().unwrap();
1323        let socket_path = temp_dir.path().join("shared_safety.sock");
1324
1325        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1326        let state = create_mock_app_state(None, None, None, None, None, None).await;
1327
1328        service
1329            .clone()
1330            .start(Arc::new(web::ThinData(state)))
1331            .await
1332            .unwrap();
1333
1334        let execution_id = "test-exec-safety".to_string();
1335        let _guard = service.register_execution(execution_id.clone(), true).await;
1336
1337        tokio::time::sleep(Duration::from_millis(50)).await;
1338
1339        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1340            .await
1341            .unwrap();
1342
1343        // Register
1344        let register_msg = PluginMessage::Register { execution_id };
1345        client
1346            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
1347            .await
1348            .unwrap();
1349        client.flush().await.unwrap();
1350
1351        // Wait well below the safety timeout (DEFAULT_PLUGIN_TIMEOUT_SECONDS + 60s).
1352        // Connection must stay alive — the safety timeout only fires for truly stuck clients.
1353        tokio::time::sleep(Duration::from_millis(200)).await;
1354
1355        // Connection should still be alive — send a Shutdown message to verify
1356        let shutdown_msg = PluginMessage::Shutdown;
1357        let write_result = client
1358            .write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
1359            .await;
1360
1361        assert!(
1362            write_result.is_ok(),
1363            "Connection should still be alive within safety timeout"
1364        );
1365
1366        drop(client);
1367        service.shutdown().await;
1368    }
1369
1370    #[tokio::test]
1371    async fn test_idle_connection_cleaned_up_by_safety_timeout() {
1372        let temp_dir = tempdir().unwrap();
1373        let socket_path = temp_dir.path().join("shared_safety_timeout.sock");
1374
1375        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1376        let state = create_mock_app_state(None, None, None, None, None, None).await;
1377
1378        service
1379            .clone()
1380            .start(Arc::new(web::ThinData(state)))
1381            .await
1382            .unwrap();
1383
1384        let execution_id = "test-exec-safety-timeout".to_string();
1385        let _guard = service.register_execution(execution_id.clone(), true).await;
1386
1387        tokio::time::sleep(Duration::from_millis(50)).await;
1388
1389        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1390            .await
1391            .unwrap();
1392
1393        // Register
1394        let register_msg = PluginMessage::Register { execution_id };
1395        client
1396            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
1397            .await
1398            .unwrap();
1399        client.flush().await.unwrap();
1400
1401        // Don't send anything else - connection will eventually be reaped
1402        // by the safety timeout (DEFAULT_PLUGIN_TIMEOUT_SECONDS + 60s)
1403
1404        // Wait a bit - connection should still be alive well within safety timeout
1405        tokio::time::sleep(Duration::from_millis(200)).await;
1406
1407        // Connection should still be valid (safety timeout is ~360s)
1408        drop(client);
1409
1410        service.shutdown().await;
1411    }
1412
1413    #[tokio::test]
1414    async fn test_multiple_api_requests_same_connection() {
1415        let temp_dir = tempdir().unwrap();
1416        let socket_path = temp_dir.path().join("shared_multiple_requests.sock");
1417
1418        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1419        let state = create_mock_app_state(None, None, None, None, None, None).await;
1420
1421        service
1422            .clone()
1423            .start(Arc::new(web::ThinData(state)))
1424            .await
1425            .unwrap();
1426
1427        let execution_id = "test-exec-multi".to_string();
1428        let _guard = service.register_execution(execution_id.clone(), true).await;
1429
1430        tokio::time::sleep(Duration::from_millis(50)).await;
1431
1432        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1433            .await
1434            .unwrap();
1435
1436        // Register
1437        let register_msg = PluginMessage::Register {
1438            execution_id: execution_id.clone(),
1439        };
1440        client
1441            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
1442            .await
1443            .unwrap();
1444
1445        let (r, mut w) = client.into_split();
1446        let mut reader = BufReader::new(r);
1447
1448        // Send multiple API requests
1449        for i in 1..=3 {
1450            let api_request = PluginMessage::ApiRequest {
1451                request_id: format!("req-{i}"),
1452                relayer_id: "relayer-1".to_string(),
1453                method: crate::services::plugins::relayer_api::PluginMethod::GetRelayerStatus,
1454                payload: serde_json::json!({}),
1455            };
1456            w.write_all((serde_json::to_string(&api_request).unwrap() + "\n").as_bytes())
1457                .await
1458                .unwrap();
1459            w.flush().await.unwrap();
1460
1461            // Read response
1462            let mut response_line = String::new();
1463            reader.read_line(&mut response_line).await.unwrap();
1464
1465            let response: PluginMessage = serde_json::from_str(&response_line).unwrap();
1466            match response {
1467                PluginMessage::ApiResponse { request_id, .. } => {
1468                    assert_eq!(request_id, format!("req-{i}"));
1469                }
1470                _ => panic!("Expected ApiResponse"),
1471            }
1472        }
1473
1474        drop(reader);
1475        drop(w);
1476        service.shutdown().await;
1477    }
1478
1479    #[tokio::test]
1480    async fn test_shutdown_signal() {
1481        let temp_dir = tempdir().unwrap();
1482        let socket_path = temp_dir.path().join("shared_shutdown_signal.sock");
1483
1484        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1485        let state = create_mock_app_state(None, None, None, None, None, None).await;
1486
1487        service
1488            .clone()
1489            .start(Arc::new(web::ThinData(state)))
1490            .await
1491            .unwrap();
1492
1493        tokio::time::sleep(Duration::from_millis(50)).await;
1494
1495        // Verify socket file exists
1496        assert!(std::path::Path::new(socket_path.to_str().unwrap()).exists());
1497
1498        // Shutdown the service
1499        service.shutdown().await;
1500
1501        // Give time for cleanup
1502        tokio::time::sleep(Duration::from_millis(100)).await;
1503
1504        // Socket file should be removed
1505        assert!(!std::path::Path::new(socket_path.to_str().unwrap()).exists());
1506    }
1507
1508    #[tokio::test]
1509    async fn test_malformed_json_handling() {
1510        let temp_dir = tempdir().unwrap();
1511        let socket_path = temp_dir.path().join("shared_malformed.sock");
1512
1513        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1514        let state = create_mock_app_state(None, None, None, None, None, None).await;
1515
1516        service
1517            .clone()
1518            .start(Arc::new(web::ThinData(state)))
1519            .await
1520            .unwrap();
1521
1522        let execution_id = "test-exec-malformed".to_string();
1523        let _guard = service.register_execution(execution_id.clone(), true).await;
1524
1525        tokio::time::sleep(Duration::from_millis(50)).await;
1526
1527        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1528            .await
1529            .unwrap();
1530
1531        // Register first
1532        let register_msg = PluginMessage::Register {
1533            execution_id: execution_id.clone(),
1534        };
1535        client
1536            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
1537            .await
1538            .unwrap();
1539
1540        // Send malformed JSON
1541        client
1542            .write_all(b"{ this is not valid json }\n")
1543            .await
1544            .unwrap();
1545        client.flush().await.unwrap();
1546
1547        // Connection should remain open (malformed messages are logged and skipped)
1548        tokio::time::sleep(Duration::from_millis(100)).await;
1549
1550        // Send valid shutdown message to verify connection is still up
1551        let shutdown_msg = PluginMessage::Shutdown;
1552        let write_result = client
1553            .write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
1554            .await;
1555
1556        assert!(
1557            write_result.is_ok(),
1558            "Connection should still be alive after malformed JSON"
1559        );
1560
1561        drop(client);
1562        service.shutdown().await;
1563    }
1564
1565    #[tokio::test]
1566    async fn test_invalid_message_direction() {
1567        let temp_dir = tempdir().unwrap();
1568        let socket_path = temp_dir.path().join("shared_invalid_direction.sock");
1569
1570        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1571        let state = create_mock_app_state(None, None, None, None, None, None).await;
1572
1573        service
1574            .clone()
1575            .start(Arc::new(web::ThinData(state)))
1576            .await
1577            .unwrap();
1578
1579        let execution_id = "test-exec-invalid-dir".to_string();
1580        let _guard = service.register_execution(execution_id.clone(), true).await;
1581
1582        tokio::time::sleep(Duration::from_millis(50)).await;
1583
1584        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1585            .await
1586            .unwrap();
1587
1588        // Register
1589        let register_msg = PluginMessage::Register {
1590            execution_id: execution_id.clone(),
1591        };
1592        client
1593            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
1594            .await
1595            .unwrap();
1596
1597        // Plugin tries to send ApiResponse (invalid direction - only Host sends ApiResponse)
1598        let invalid_msg = PluginMessage::ApiResponse {
1599            request_id: "invalid".to_string(),
1600            result: Some(serde_json::json!({})),
1601            error: None,
1602        };
1603        client
1604            .write_all((serde_json::to_string(&invalid_msg).unwrap() + "\n").as_bytes())
1605            .await
1606            .unwrap();
1607        client.flush().await.unwrap();
1608
1609        // Connection should remain open (invalid messages are logged and skipped)
1610        tokio::time::sleep(Duration::from_millis(100)).await;
1611
1612        // Verify connection is still alive
1613        let shutdown_msg = PluginMessage::Shutdown;
1614        let write_result = client
1615            .write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
1616            .await;
1617
1618        assert!(write_result.is_ok());
1619
1620        drop(client);
1621        service.shutdown().await;
1622    }
1623
1624    #[tokio::test]
1625    async fn test_stale_execution_cleanup() {
1626        let temp_dir = tempdir().unwrap();
1627        let socket_path = temp_dir.path().join("shared_stale_cleanup.sock");
1628
1629        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1630
1631        // Register an execution manually with old timestamp
1632        let execution_id = "stale-exec".to_string();
1633        let (tx, _rx) = mpsc::channel(1);
1634        // scc::HashMap insert
1635        let _ = service.executions.insert(
1636            execution_id.clone(),
1637            ExecutionContext {
1638                traces_tx: Some(tx),
1639                created_at: Instant::now() - Duration::from_secs(400), // 6+ minutes old
1640                bound_execution_id: execution_id.clone(),
1641            },
1642        );
1643
1644        // Verify it's registered using scc's contains()
1645        assert!(service.executions.contains(&execution_id));
1646
1647        // Wait for cleanup task to run (it runs every 60 seconds, but we can't wait that long)
1648        // Instead, we verify the cleanup logic by checking the code in new()
1649        // The actual cleanup test would require mocking time or waiting 60+ seconds
1650
1651        // For this test, we just verify the logic exists and doesn't panic
1652        drop(service);
1653    }
1654
1655    #[tokio::test]
1656    async fn test_socket_path_getter() {
1657        let temp_dir = tempdir().unwrap();
1658        let socket_path = temp_dir.path().join("shared_path.sock");
1659
1660        let service = SharedSocketService::new(socket_path.to_str().unwrap()).unwrap();
1661
1662        assert_eq!(service.socket_path(), socket_path.to_str().unwrap());
1663    }
1664
1665    #[tokio::test]
1666    async fn test_trace_send_timeout() {
1667        let temp_dir = tempdir().unwrap();
1668        let socket_path = temp_dir.path().join("shared_trace_timeout.sock");
1669
1670        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1671        let state = create_mock_app_state(None, None, None, None, None, None).await;
1672
1673        service
1674            .clone()
1675            .start(Arc::new(web::ThinData(state)))
1676            .await
1677            .unwrap();
1678
1679        let execution_id = "test-exec-trace-timeout".to_string();
1680        let guard = service.register_execution(execution_id.clone(), true).await;
1681
1682        // Don't consume the receiver - this will cause the channel to fill up
1683        drop(guard);
1684
1685        tokio::time::sleep(Duration::from_millis(50)).await;
1686
1687        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1688            .await
1689            .unwrap();
1690
1691        // Register
1692        let register_msg = PluginMessage::Register {
1693            execution_id: execution_id.clone(),
1694        };
1695        client
1696            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
1697            .await
1698            .unwrap();
1699
1700        // Send trace
1701        let trace = PluginMessage::Trace {
1702            trace: serde_json::json!({"event": "test"}),
1703        };
1704        client
1705            .write_all((serde_json::to_string(&trace).unwrap() + "\n").as_bytes())
1706            .await
1707            .unwrap();
1708
1709        // Shutdown
1710        let shutdown_msg = PluginMessage::Shutdown;
1711        client
1712            .write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
1713            .await
1714            .unwrap();
1715        client.flush().await.unwrap();
1716
1717        drop(client);
1718
1719        // Wait for connection to close - should handle timeout gracefully
1720        tokio::time::sleep(Duration::from_millis(200)).await;
1721
1722        service.shutdown().await;
1723    }
1724
1725    #[tokio::test]
1726    async fn test_get_shared_socket_service() {
1727        // Test the global singleton
1728        let service1 = get_shared_socket_service();
1729        assert!(service1.is_ok());
1730
1731        let service2 = get_shared_socket_service();
1732        assert!(service2.is_ok());
1733
1734        // Should return the same instance
1735        let svc1 = service1.unwrap();
1736        let svc2 = service2.unwrap();
1737        let path1 = svc1.socket_path();
1738        let path2 = svc2.socket_path();
1739        assert_eq!(path1, path2);
1740    }
1741
1742    #[tokio::test]
1743    async fn test_available_connection_slots_and_active_count() {
1744        let temp_dir = tempdir().unwrap();
1745        let socket_path = temp_dir.path().join("shared_slots.sock");
1746
1747        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1748        let state = create_mock_app_state(None, None, None, None, None, None).await;
1749        let max_connections = get_config().socket_max_connections;
1750
1751        // Before any connections
1752        assert_eq!(service.available_connection_slots(), max_connections);
1753        assert_eq!(service.active_connection_count(), 0);
1754
1755        service
1756            .clone()
1757            .start(Arc::new(web::ThinData(state)))
1758            .await
1759            .unwrap();
1760
1761        tokio::time::sleep(Duration::from_millis(50)).await;
1762
1763        // Connect a client
1764        let _client = UnixStream::connect(socket_path.to_str().unwrap())
1765            .await
1766            .unwrap();
1767
1768        tokio::time::sleep(Duration::from_millis(50)).await;
1769
1770        assert!(service.available_connection_slots() < max_connections);
1771        assert!(service.active_connection_count() > 0);
1772
1773        drop(_client);
1774        service.shutdown().await;
1775    }
1776
1777    #[tokio::test]
1778    async fn test_start_called_twice_is_idempotent() {
1779        let temp_dir = tempdir().unwrap();
1780        let socket_path = temp_dir.path().join("shared_double_start.sock");
1781
1782        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1783        let state = create_mock_app_state(None, None, None, None, None, None).await;
1784        let thin = Arc::new(web::ThinData(state));
1785
1786        // First start should succeed
1787        service.clone().start(thin.clone()).await.unwrap();
1788
1789        // Second start should also succeed (early return)
1790        service.clone().start(thin).await.unwrap();
1791
1792        service.shutdown().await;
1793    }
1794
1795    #[tokio::test]
1796    async fn test_traces_discarded_when_emit_traces_false() {
1797        let temp_dir = tempdir().unwrap();
1798        let socket_path = temp_dir.path().join("shared_no_traces.sock");
1799
1800        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1801        let state = create_mock_app_state(None, None, None, None, None, None).await;
1802
1803        service
1804            .clone()
1805            .start(Arc::new(web::ThinData(state)))
1806            .await
1807            .unwrap();
1808
1809        let execution_id = "test-exec-no-trace".to_string();
1810        // Register with emit_traces=false
1811        let guard = service
1812            .register_execution(execution_id.clone(), false)
1813            .await;
1814        assert_eq!(service.registered_executions_count().await, 1);
1815
1816        tokio::time::sleep(Duration::from_millis(50)).await;
1817
1818        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1819            .await
1820            .unwrap();
1821
1822        // Register
1823        let register_msg = PluginMessage::Register {
1824            execution_id: execution_id.clone(),
1825        };
1826        client
1827            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
1828            .await
1829            .unwrap();
1830
1831        // Send trace (should be silently discarded)
1832        let trace = PluginMessage::Trace {
1833            trace: serde_json::json!({"event": "should_be_discarded"}),
1834        };
1835        client
1836            .write_all((serde_json::to_string(&trace).unwrap() + "\n").as_bytes())
1837            .await
1838            .unwrap();
1839
1840        // Shutdown
1841        let shutdown_msg = PluginMessage::Shutdown;
1842        client
1843            .write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
1844            .await
1845            .unwrap();
1846        client.flush().await.unwrap();
1847        drop(client);
1848
1849        // Connection should be fully drained (permit returned).
1850        tokio::time::timeout(Duration::from_secs(1), async {
1851            loop {
1852                if service.active_connection_count() == 0 {
1853                    break;
1854                }
1855                tokio::time::sleep(Duration::from_millis(20)).await;
1856            }
1857        })
1858        .await
1859        .expect("Connection should close promptly when traces are disabled");
1860
1861        // Guard with emit_traces=false should return None
1862        assert!(guard.into_receiver().is_none());
1863        assert_eq!(service.registered_executions_count().await, 0);
1864
1865        service.shutdown().await;
1866    }
1867
1868    #[tokio::test]
1869    async fn test_legacy_protocol_without_http_request_id() {
1870        let temp_dir = tempdir().unwrap();
1871        let socket_path = temp_dir.path().join("shared_legacy_no_http_id.sock");
1872
1873        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1874        let state = create_mock_app_state(None, None, None, None, None, None).await;
1875
1876        service
1877            .clone()
1878            .start(Arc::new(web::ThinData(state)))
1879            .await
1880            .unwrap();
1881
1882        // Use request_id as the execution_id (fallback path)
1883        let request_id = "legacy-fallback-id".to_string();
1884        let _guard = service.register_execution(request_id.clone(), true).await;
1885
1886        tokio::time::sleep(Duration::from_millis(50)).await;
1887
1888        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1889            .await
1890            .unwrap();
1891
1892        // Send legacy Request with http_request_id = None
1893        // The handler falls back to request_id for execution binding
1894        let legacy_request = crate::services::plugins::relayer_api::Request {
1895            request_id: request_id.clone(),
1896            relayer_id: "relayer-1".to_string(),
1897            method: crate::services::plugins::relayer_api::PluginMethod::GetRelayerStatus,
1898            payload: serde_json::json!({}),
1899            http_request_id: None,
1900        };
1901        let legacy_json = serde_json::to_string(&legacy_request).unwrap() + "\n";
1902        client.write_all(legacy_json.as_bytes()).await.unwrap();
1903        client.flush().await.unwrap();
1904
1905        // Read legacy Response
1906        let (r, _w) = client.into_split();
1907        let mut reader = BufReader::new(r);
1908        let mut response_line = String::new();
1909        reader.read_line(&mut response_line).await.unwrap();
1910
1911        let response: crate::services::plugins::relayer_api::Response =
1912            serde_json::from_str(&response_line).unwrap();
1913        assert_eq!(response.request_id, request_id);
1914        assert!(response.result.is_some() || response.error.is_some());
1915
1916        drop(reader);
1917        drop(_w);
1918        service.shutdown().await;
1919    }
1920
1921    #[tokio::test]
1922    async fn test_legacy_protocol_with_unknown_execution_id() {
1923        let temp_dir = tempdir().unwrap();
1924        let socket_path = temp_dir.path().join("shared_legacy_unknown.sock");
1925
1926        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1927        let state = create_mock_app_state(None, None, None, None, None, None).await;
1928
1929        service
1930            .clone()
1931            .start(Arc::new(web::ThinData(state)))
1932            .await
1933            .unwrap();
1934
1935        // Do NOT register any execution — the legacy handler should still process
1936        // the request but log a debug warning about unknown execution_id
1937
1938        tokio::time::sleep(Duration::from_millis(50)).await;
1939
1940        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1941            .await
1942            .unwrap();
1943
1944        // Send legacy Request with unknown execution_id
1945        let legacy_request = crate::services::plugins::relayer_api::Request {
1946            request_id: "unknown-req".to_string(),
1947            relayer_id: "relayer-1".to_string(),
1948            method: crate::services::plugins::relayer_api::PluginMethod::GetRelayerStatus,
1949            payload: serde_json::json!({}),
1950            http_request_id: Some("nonexistent-exec-id".to_string()),
1951        };
1952        let legacy_json = serde_json::to_string(&legacy_request).unwrap() + "\n";
1953        client.write_all(legacy_json.as_bytes()).await.unwrap();
1954        client.flush().await.unwrap();
1955
1956        // Should still get a response (legacy path processes even without binding)
1957        let (r, _w) = client.into_split();
1958        let mut reader = BufReader::new(r);
1959        let mut response_line = String::new();
1960        reader.read_line(&mut response_line).await.unwrap();
1961
1962        let response: crate::services::plugins::relayer_api::Response =
1963            serde_json::from_str(&response_line).unwrap();
1964        assert_eq!(response.request_id, "unknown-req");
1965
1966        drop(reader);
1967        drop(_w);
1968        service.shutdown().await;
1969    }
1970
1971    #[tokio::test]
1972    async fn test_legacy_unparsable_message() {
1973        let temp_dir = tempdir().unwrap();
1974        let socket_path = temp_dir.path().join("shared_legacy_unparse.sock");
1975
1976        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
1977        let state = create_mock_app_state(None, None, None, None, None, None).await;
1978
1979        service
1980            .clone()
1981            .start(Arc::new(web::ThinData(state)))
1982            .await
1983            .unwrap();
1984
1985        let execution_id = "test-exec-legacy-unparse".to_string();
1986        let _guard = service.register_execution(execution_id.clone(), true).await;
1987
1988        tokio::time::sleep(Duration::from_millis(50)).await;
1989
1990        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
1991            .await
1992            .unwrap();
1993
1994        // Register first
1995        let register_msg = PluginMessage::Register {
1996            execution_id: execution_id.clone(),
1997        };
1998        client
1999            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
2000            .await
2001            .unwrap();
2002
2003        // Send JSON without "type" field AND not a valid legacy Request
2004        // This hits the fallback warn! at line 732
2005        client.write_all(b"{\"foo\": \"bar\"}\n").await.unwrap();
2006        client.flush().await.unwrap();
2007
2008        tokio::time::sleep(Duration::from_millis(100)).await;
2009
2010        // Connection should still be alive
2011        let shutdown_msg = PluginMessage::Shutdown;
2012        let write_result = client
2013            .write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
2014            .await;
2015
2016        assert!(
2017            write_result.is_ok(),
2018            "Connection should still be alive after unparsable legacy message"
2019        );
2020
2021        drop(client);
2022        service.shutdown().await;
2023    }
2024
2025    #[tokio::test]
2026    async fn test_invalid_plugin_message_type() {
2027        let temp_dir = tempdir().unwrap();
2028        let socket_path = temp_dir.path().join("shared_invalid_type.sock");
2029
2030        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
2031        let state = create_mock_app_state(None, None, None, None, None, None).await;
2032
2033        service
2034            .clone()
2035            .start(Arc::new(web::ThinData(state)))
2036            .await
2037            .unwrap();
2038
2039        let execution_id = "test-exec-invalid-type".to_string();
2040        let _guard = service.register_execution(execution_id.clone(), true).await;
2041
2042        tokio::time::sleep(Duration::from_millis(50)).await;
2043
2044        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
2045            .await
2046            .unwrap();
2047
2048        // Register first
2049        let register_msg = PluginMessage::Register {
2050            execution_id: execution_id.clone(),
2051        };
2052        client
2053            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
2054            .await
2055            .unwrap();
2056
2057        // Send JSON with "type" field but not a valid PluginMessage variant
2058        // This hits the `Err(e)` branch at line 584-586
2059        client
2060            .write_all(b"{\"type\": \"nonexistent_type\", \"data\": \"foo\"}\n")
2061            .await
2062            .unwrap();
2063        client.flush().await.unwrap();
2064
2065        // Send a valid request after invalid message.
2066        // If we get ApiResponse, the connection stayed alive and parsing recovered.
2067        let api_request = PluginMessage::ApiRequest {
2068            request_id: "req-after-invalid".to_string(),
2069            relayer_id: "relayer-1".to_string(),
2070            method: crate::services::plugins::relayer_api::PluginMethod::GetRelayerStatus,
2071            payload: serde_json::json!({}),
2072        };
2073        client
2074            .write_all((serde_json::to_string(&api_request).unwrap() + "\n").as_bytes())
2075            .await
2076            .unwrap();
2077        client.flush().await.unwrap();
2078
2079        let (r, mut w) = client.into_split();
2080        let mut reader = BufReader::new(r);
2081        let mut response_line = String::new();
2082        tokio::time::timeout(Duration::from_secs(1), reader.read_line(&mut response_line))
2083            .await
2084            .expect("Timed out waiting for ApiResponse")
2085            .unwrap();
2086
2087        let response: PluginMessage = serde_json::from_str(&response_line).unwrap();
2088        match response {
2089            PluginMessage::ApiResponse { request_id, .. } => {
2090                assert_eq!(request_id, "req-after-invalid");
2091            }
2092            _ => panic!("Expected ApiResponse after invalid plugin message"),
2093        }
2094
2095        let shutdown_msg = PluginMessage::Shutdown;
2096        w.write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
2097            .await
2098            .unwrap();
2099        w.flush().await.unwrap();
2100
2101        drop(reader);
2102        drop(w);
2103        service.shutdown().await;
2104    }
2105
2106    #[tokio::test]
2107    async fn test_service_drop_sends_shutdown() {
2108        let temp_dir = tempdir().unwrap();
2109        let socket_path = temp_dir.path().join("shared_drop.sock");
2110
2111        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
2112        let state = create_mock_app_state(None, None, None, None, None, None).await;
2113
2114        service
2115            .clone()
2116            .start(Arc::new(web::ThinData(state)))
2117            .await
2118            .unwrap();
2119
2120        // Subscribe before dropping so we can observe Drop-triggered shutdown signal.
2121        let mut shutdown_rx = service.shutdown_tx.subscribe();
2122
2123        // Drop should send shutdown signal.
2124        drop(service);
2125
2126        tokio::time::timeout(Duration::from_secs(1), shutdown_rx.changed())
2127            .await
2128            .expect("Timed out waiting for shutdown signal from Drop")
2129            .unwrap();
2130        assert!(
2131            *shutdown_rx.borrow(),
2132            "Drop should broadcast shutdown=true to listeners"
2133        );
2134    }
2135
2136    #[tokio::test]
2137    async fn test_trace_channel_closed_before_send() {
2138        let temp_dir = tempdir().unwrap();
2139        let socket_path = temp_dir.path().join("shared_trace_closed.sock");
2140
2141        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
2142        let state = create_mock_app_state(None, None, None, None, None, None).await;
2143
2144        service
2145            .clone()
2146            .start(Arc::new(web::ThinData(state)))
2147            .await
2148            .unwrap();
2149
2150        let execution_id = "test-exec-trace-closed".to_string();
2151        let guard = service.register_execution(execution_id.clone(), true).await;
2152
2153        // Consume and drop the receiver immediately — channel is now closed
2154        let rx = guard.into_receiver();
2155        assert!(rx.is_some());
2156        drop(rx);
2157
2158        tokio::time::sleep(Duration::from_millis(50)).await;
2159
2160        let mut client = UnixStream::connect(socket_path.to_str().unwrap())
2161            .await
2162            .unwrap();
2163
2164        // Register
2165        let register_msg = PluginMessage::Register {
2166            execution_id: execution_id.clone(),
2167        };
2168        client
2169            .write_all((serde_json::to_string(&register_msg).unwrap() + "\n").as_bytes())
2170            .await
2171            .unwrap();
2172
2173        // Send trace
2174        let trace = PluginMessage::Trace {
2175            trace: serde_json::json!({"event": "will_be_lost"}),
2176        };
2177        client
2178            .write_all((serde_json::to_string(&trace).unwrap() + "\n").as_bytes())
2179            .await
2180            .unwrap();
2181
2182        // Shutdown
2183        let shutdown_msg = PluginMessage::Shutdown;
2184        client
2185            .write_all((serde_json::to_string(&shutdown_msg).unwrap() + "\n").as_bytes())
2186            .await
2187            .unwrap();
2188        client.flush().await.unwrap();
2189        drop(client);
2190
2191        // Handler should not get stuck trying to send traces to a closed channel.
2192        tokio::time::timeout(Duration::from_secs(1), async {
2193            loop {
2194                if service.active_connection_count() == 0 {
2195                    break;
2196                }
2197                tokio::time::sleep(Duration::from_millis(20)).await;
2198            }
2199        })
2200        .await
2201        .expect("Connection should close even when trace receiver is dropped");
2202
2203        service.shutdown().await;
2204    }
2205
2206    #[tokio::test]
2207    async fn test_duplicate_execution_id_does_not_corrupt_counter() {
2208        let temp_dir = tempdir().unwrap();
2209        let socket_path = temp_dir.path().join("shared_duplicate_exec.sock");
2210
2211        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
2212        let execution_id = "duplicate-exec-id".to_string();
2213
2214        // Initial count should be 0
2215        assert_eq!(service.registered_executions_count().await, 0);
2216
2217        // Register first execution
2218        let guard1 = service.register_execution(execution_id.clone(), true).await;
2219        assert_eq!(service.registered_executions_count().await, 1);
2220
2221        // Try to register with same execution_id (duplicate)
2222        // This should NOT increment the counter (insertion will fail)
2223        let guard2 = service.register_execution(execution_id.clone(), true).await;
2224        // Counter should still be 1 (not 2)
2225        assert_eq!(service.registered_executions_count().await, 1);
2226
2227        // Drop the duplicate guard first - should NOT decrement counter
2228        // (because it was never successfully registered)
2229        drop(guard2);
2230        assert_eq!(service.registered_executions_count().await, 1);
2231
2232        // Drop the original guard - should decrement counter
2233        drop(guard1);
2234        assert_eq!(service.registered_executions_count().await, 0);
2235    }
2236
2237    #[tokio::test]
2238    async fn test_execution_guard_registered_field() {
2239        let temp_dir = tempdir().unwrap();
2240        let socket_path = temp_dir.path().join("shared_registered_field.sock");
2241
2242        let service = Arc::new(SharedSocketService::new(socket_path.to_str().unwrap()).unwrap());
2243
2244        // Register a unique execution_id
2245        let execution_id_1 = "unique-exec-1".to_string();
2246        let guard1 = service
2247            .register_execution(execution_id_1.clone(), true)
2248            .await;
2249        assert_eq!(service.registered_executions_count().await, 1);
2250
2251        // Register another unique execution_id
2252        let execution_id_2 = "unique-exec-2".to_string();
2253        let guard2 = service
2254            .register_execution(execution_id_2.clone(), false)
2255            .await;
2256        assert_eq!(service.registered_executions_count().await, 2);
2257
2258        // into_receiver should work regardless of registered status
2259        let rx = guard1.into_receiver();
2260        assert!(rx.is_some()); // emit_traces=true
2261
2262        // guard2 had emit_traces=false
2263        let rx2 = guard2.into_receiver();
2264        assert!(rx2.is_none()); // emit_traces=false
2265
2266        // After guards are consumed via into_receiver, counter should be decremented
2267        assert_eq!(service.registered_executions_count().await, 0);
2268    }
2269
2270    // =========================================================================
2271    // log_socket_write_error tests
2272    // =========================================================================
2273
2274    #[test]
2275    fn test_log_socket_write_error_broken_pipe_does_not_panic() {
2276        // BrokenPipe is expected during timeout teardown → should log at DEBUG, not WARN
2277        let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "Broken pipe");
2278        log_socket_write_error("API response", &err);
2279    }
2280
2281    #[test]
2282    fn test_log_socket_write_error_connection_reset_does_not_panic() {
2283        // ConnectionReset is expected during timeout teardown → should log at DEBUG, not WARN
2284        let err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "Connection reset");
2285        log_socket_write_error("API response flush", &err);
2286    }
2287
2288    #[test]
2289    fn test_log_socket_write_error_other_errors_do_not_panic() {
2290        // Other IO errors (e.g., PermissionDenied) → should log at WARN
2291        let err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Permission denied");
2292        log_socket_write_error("response", &err);
2293    }
2294
2295    #[test]
2296    fn test_log_socket_write_error_unexpected_eof() {
2297        let err = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected eof");
2298        log_socket_write_error("response flush", &err);
2299    }
2300
2301    #[test]
2302    fn test_log_socket_write_error_context_strings() {
2303        // Verify all 4 context strings used in production don't cause issues
2304        let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "os error 32");
2305        log_socket_write_error("API response", &err);
2306        log_socket_write_error("API response flush", &err);
2307        log_socket_write_error("response", &err);
2308        log_socket_write_error("response flush", &err);
2309    }
2310}