1pub mod audit_log;
79pub mod backpressure;
80pub mod checkpointing;
81pub mod circuit_breaker;
82pub mod cluster;
83pub mod compaction;
84pub mod connection_pool;
85pub mod consensus;
86pub mod coordinator;
87pub mod discovery;
88pub mod distributed_enhancements;
89pub mod fault_tolerance;
90pub mod geo_placement;
91pub mod heartbeat;
92pub mod job_dag;
93pub mod job_preemption;
94pub mod job_tracker;
95pub mod leader_election;
96pub mod lease;
97pub mod load_balancer;
98pub mod membership;
99pub mod message_bus;
100pub mod message_queue;
101pub mod metrics_aggregator;
102pub mod node_health;
103pub mod node_registry;
104pub mod node_topology;
105pub mod notifications;
106pub mod partition;
107pub mod pb;
108pub mod raft_primitives;
109pub mod replication;
110pub mod resource_quota;
111pub mod scheduler;
112pub mod segment;
113pub mod segment_merge;
114pub mod shard;
115pub mod shard_map;
116pub mod snapshot_store;
117pub mod task_distribution;
118pub mod task_priority_queue;
119pub mod task_queue;
120pub mod task_retry;
121pub mod twopc;
122pub mod weighted_round_robin;
123pub mod work_stealing;
124pub mod worker;
125pub mod worker_draining;
126
127#[cfg(feature = "s3")]
128pub mod s3_integration;
129
130#[cfg(feature = "k8s")]
131pub mod k8s_autoscale;
132
133use std::collections::HashMap;
134use std::sync::Arc;
135use std::time::{Duration, Instant};
136use thiserror::Error;
137use tokio::sync::RwLock;
138use uuid::Uuid;
139
140pub type Result<T> = std::result::Result<T, DistributedError>;
142
143#[derive(Debug, Error)]
145pub enum DistributedError {
146 #[error("Worker error: {0}")]
147 Worker(String),
148
149 #[error("Coordinator error: {0}")]
150 Coordinator(String),
151
152 #[error("Job error: {0}")]
153 Job(String),
154
155 #[error("Network error: {0}")]
156 Network(#[from] tonic::transport::Error),
157
158 #[error("gRPC status error: {0}")]
159 Status(#[from] tonic::Status),
160
161 #[error("Serialization error: {0}")]
162 Serialization(#[from] serde_json::Error),
163
164 #[error("IO error: {0}")]
165 Io(#[from] std::io::Error),
166
167 #[error("Discovery error: {0}")]
168 Discovery(String),
169
170 #[error("Scheduling error: {0}")]
171 Scheduling(String),
172
173 #[error("Segmentation error: {0}")]
174 Segmentation(String),
175
176 #[error("Timeout error")]
177 Timeout,
178
179 #[error("Invalid configuration: {0}")]
180 InvalidConfig(String),
181
182 #[error("Resource exhausted: {0}")]
183 ResourceExhausted(String),
184
185 #[error("Error: {0}")]
186 Other(Box<dyn std::error::Error + Send + Sync>),
187}
188
189impl From<Box<dyn std::error::Error + Send + Sync>> for DistributedError {
190 fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
191 DistributedError::Other(err)
192 }
193}
194
195#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
197pub struct DistributedConfig {
198 pub coordinator_addr: String,
200
201 pub max_retries: u32,
203
204 pub heartbeat_interval: Duration,
206
207 pub job_timeout: Duration,
209
210 pub max_concurrent_jobs: u32,
212
213 pub fault_tolerance: bool,
215
216 pub discovery_method: DiscoveryMethod,
218}
219
220impl Default for DistributedConfig {
221 fn default() -> Self {
222 Self {
223 coordinator_addr: "127.0.0.1:50051".to_string(),
224 max_retries: 3,
225 heartbeat_interval: Duration::from_secs(30),
226 job_timeout: Duration::from_secs(3600),
227 max_concurrent_jobs: 4,
228 fault_tolerance: true,
229 discovery_method: DiscoveryMethod::Static,
230 }
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
236#[allow(dead_code)]
237pub enum DiscoveryMethod {
238 Static,
240 #[allow(clippy::upper_case_acronyms)]
242 MDNS,
243 Etcd,
245 Consul,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
251pub enum SplitStrategy {
252 SegmentBased,
254 TileBased,
256 GopBased,
258}
259
260impl From<SplitStrategy> for i32 {
261 fn from(strategy: SplitStrategy) -> Self {
262 match strategy {
263 SplitStrategy::SegmentBased => 0,
264 SplitStrategy::TileBased => 1,
265 SplitStrategy::GopBased => 2,
266 }
267 }
268}
269
270#[derive(
272 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
273)]
274pub enum JobPriority {
275 Low = 0,
276 Normal = 1,
277 High = 2,
278 Critical = 3,
279}
280
281impl From<JobPriority> for u32 {
282 fn from(priority: JobPriority) -> Self {
283 priority as u32
284 }
285}
286
287#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
289pub struct DistributedJob {
290 pub id: Uuid,
292
293 pub task_id: Uuid,
295
296 pub source_url: String,
298
299 pub codec: String,
301
302 pub strategy: SplitStrategy,
304
305 pub priority: JobPriority,
307
308 pub params: EncodingParams,
310
311 pub output_url: String,
313
314 pub deadline: Option<i64>,
316}
317
318#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
320pub struct EncodingParams {
321 pub bitrate: Option<u32>,
322 pub width: Option<u32>,
323 pub height: Option<u32>,
324 pub preset: Option<String>,
325 pub profile: Option<String>,
326 pub crf: Option<u32>,
327 pub extra_params: std::collections::HashMap<String, String>,
328}
329
330impl Default for EncodingParams {
331 fn default() -> Self {
332 Self {
333 bitrate: None,
334 width: None,
335 height: None,
336 preset: Some("medium".to_string()),
337 profile: None,
338 crf: Some(23),
339 extra_params: std::collections::HashMap::new(),
340 }
341 }
342}
343
344#[derive(Debug, Clone)]
346struct JobRecord {
347 #[allow(dead_code)]
349 job: DistributedJob,
350 status: JobStatus,
352 submitted_at: Instant,
354 retries: u32,
356}
357
358pub struct DistributedEncoder {
368 config: DistributedConfig,
369 jobs: Arc<RwLock<HashMap<Uuid, JobRecord>>>,
371 server_handle: Option<tokio::task::JoinHandle<()>>,
374}
375
376impl DistributedEncoder {
377 #[must_use]
385 pub fn new(config: DistributedConfig) -> Self {
386 let server_handle = if !config.coordinator_addr.is_empty() {
387 match config.coordinator_addr.parse::<std::net::SocketAddr>() {
388 Ok(addr) => {
389 let coord = crate::coordinator::Coordinator::new(
390 crate::coordinator::CoordinatorConfig::default(),
391 );
392 let handle = tokio::spawn(async move {
393 if let Err(e) = coord.serve(addr).await {
394 tracing::warn!("Coordinator server stopped on {}: {}", addr, e);
395 }
396 });
397 Some(handle)
398 }
399 Err(e) => {
400 tracing::warn!(
401 "Invalid coordinator_addr '{}': {}",
402 config.coordinator_addr,
403 e
404 );
405 None
406 }
407 }
408 } else {
409 None
410 };
411
412 Self {
413 config,
414 jobs: Arc::new(RwLock::new(HashMap::new())),
415 server_handle,
416 }
417 }
418
419 #[must_use]
421 pub fn with_defaults() -> Self {
422 Self::new(DistributedConfig::default())
423 }
424
425 #[must_use]
427 pub fn config(&self) -> &DistributedConfig {
428 &self.config
429 }
430
431 pub async fn job_count(&self) -> usize {
433 self.jobs.read().await.len()
434 }
435
436 pub async fn active_job_count(&self) -> usize {
438 self.jobs
439 .read()
440 .await
441 .values()
442 .filter(|r| {
443 matches!(
444 r.status,
445 JobStatus::Pending | JobStatus::Assigned | JobStatus::InProgress
446 )
447 })
448 .count()
449 }
450
451 pub async fn submit_job(&self, job: DistributedJob) -> Result<Uuid> {
470 if job.source_url.is_empty() {
472 return Err(DistributedError::InvalidConfig(
473 "source_url must not be empty".to_string(),
474 ));
475 }
476 if job.output_url.is_empty() {
477 return Err(DistributedError::InvalidConfig(
478 "output_url must not be empty".to_string(),
479 ));
480 }
481 if job.codec.is_empty() {
482 return Err(DistributedError::InvalidConfig(
483 "codec must not be empty".to_string(),
484 ));
485 }
486
487 if let Some(deadline) = job.deadline {
489 let now = std::time::SystemTime::now()
490 .duration_since(std::time::UNIX_EPOCH)
491 .map_err(|e| DistributedError::Job(format!("System time error: {e}")))?;
492 if deadline < now.as_secs() as i64 {
493 return Err(DistributedError::Job(
494 "Job deadline is already in the past".to_string(),
495 ));
496 }
497 }
498
499 let mut jobs = self.jobs.write().await;
500
501 if jobs.contains_key(&job.id) {
503 return Err(DistributedError::Job(format!(
504 "Job with ID {} already exists",
505 job.id
506 )));
507 }
508
509 let active_count = jobs
511 .values()
512 .filter(|r| {
513 matches!(
514 r.status,
515 JobStatus::Pending | JobStatus::Assigned | JobStatus::InProgress
516 )
517 })
518 .count();
519
520 if active_count >= self.config.max_concurrent_jobs as usize {
521 return Err(DistributedError::ResourceExhausted(format!(
522 "Maximum concurrent jobs ({}) reached",
523 self.config.max_concurrent_jobs
524 )));
525 }
526
527 let job_id = job.id;
528
529 tracing::info!(
530 "Submitting job {} (codec={}, strategy={:?}, priority={:?}) to coordinator at {}",
531 job_id,
532 job.codec,
533 job.strategy,
534 job.priority,
535 self.config.coordinator_addr
536 );
537
538 jobs.insert(
539 job_id,
540 JobRecord {
541 job,
542 status: JobStatus::Pending,
543 submitted_at: Instant::now(),
544 retries: 0,
545 },
546 );
547
548 Ok(job_id)
549 }
550
551 pub async fn job_status(&self, job_id: Uuid) -> Result<JobStatus> {
561 tracing::debug!("Querying status for job {}", job_id);
562
563 let mut jobs = self.jobs.write().await;
564 let record = jobs
565 .get_mut(&job_id)
566 .ok_or_else(|| DistributedError::Job(format!("Job {job_id} not found")))?;
567
568 if matches!(
570 record.status,
571 JobStatus::Pending | JobStatus::Assigned | JobStatus::InProgress
572 ) && record.submitted_at.elapsed() > self.config.job_timeout
573 {
574 tracing::warn!(
575 "Job {} has timed out after {:?}",
576 job_id,
577 self.config.job_timeout
578 );
579 record.status = JobStatus::Failed;
580 }
581
582 Ok(record.status)
583 }
584
585 pub async fn cancel_job(&self, job_id: Uuid) -> Result<()> {
595 tracing::info!("Cancelling job {}", job_id);
596
597 let mut jobs = self.jobs.write().await;
598 let record = jobs
599 .get_mut(&job_id)
600 .ok_or_else(|| DistributedError::Job(format!("Job {job_id} not found")))?;
601
602 match record.status {
603 JobStatus::Completed => {
604 return Err(DistributedError::Job(format!(
605 "Job {job_id} is already completed and cannot be cancelled"
606 )));
607 }
608 JobStatus::Failed => {
609 return Err(DistributedError::Job(format!(
610 "Job {job_id} has already failed and cannot be cancelled"
611 )));
612 }
613 JobStatus::Cancelled => {
614 return Err(DistributedError::Job(format!(
615 "Job {job_id} is already cancelled"
616 )));
617 }
618 _ => {}
619 }
620
621 record.status = JobStatus::Cancelled;
622 Ok(())
623 }
624
625 pub async fn advance_job(&self, job_id: Uuid) -> Result<JobStatus> {
633 let mut jobs = self.jobs.write().await;
634 let record = jobs
635 .get_mut(&job_id)
636 .ok_or_else(|| DistributedError::Job(format!("Job {job_id} not found")))?;
637
638 record.status = match record.status {
639 JobStatus::Pending => JobStatus::Assigned,
640 JobStatus::Assigned => JobStatus::InProgress,
641 JobStatus::InProgress => JobStatus::Completed,
642 other => {
643 return Err(DistributedError::Job(format!(
644 "Cannot advance job in terminal state: {other:?}"
645 )));
646 }
647 };
648
649 Ok(record.status)
650 }
651
652 pub async fn fail_job(&self, job_id: Uuid) -> Result<()> {
658 let mut jobs = self.jobs.write().await;
659 let record = jobs
660 .get_mut(&job_id)
661 .ok_or_else(|| DistributedError::Job(format!("Job {job_id} not found")))?;
662
663 if matches!(record.status, JobStatus::Completed | JobStatus::Cancelled) {
664 return Err(DistributedError::Job(format!(
665 "Cannot fail job {job_id} in terminal state: {:?}",
666 record.status
667 )));
668 }
669
670 if self.config.fault_tolerance && record.retries < self.config.max_retries {
672 record.retries += 1;
673 record.status = JobStatus::Pending;
674 tracing::info!(
675 "Retrying job {} (attempt {}/{})",
676 job_id,
677 record.retries,
678 self.config.max_retries
679 );
680 } else {
681 record.status = JobStatus::Failed;
682 }
683
684 Ok(())
685 }
686
687 pub async fn job_retries(&self, job_id: Uuid) -> Result<u32> {
693 let jobs = self.jobs.read().await;
694 let record = jobs
695 .get(&job_id)
696 .ok_or_else(|| DistributedError::Job(format!("Job {job_id} not found")))?;
697 Ok(record.retries)
698 }
699
700 pub async fn list_jobs(&self) -> Vec<(Uuid, JobStatus)> {
702 self.jobs
703 .read()
704 .await
705 .iter()
706 .map(|(id, record)| (*id, record.status))
707 .collect()
708 }
709
710 pub async fn submit_jobs_batch(&self, jobs: Vec<DistributedJob>) -> Vec<Result<Uuid>> {
720 let mut results = Vec::with_capacity(jobs.len());
721 for job in jobs {
722 results.push(self.submit_job(job).await);
723 }
724 results
725 }
726}
727
728impl Drop for DistributedEncoder {
729 fn drop(&mut self) {
730 if let Some(handle) = self.server_handle.take() {
731 handle.abort();
732 }
733 }
734}
735
736#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
738pub enum JobStatus {
739 Pending,
740 Assigned,
741 InProgress,
742 Completed,
743 Failed,
744 Cancelled,
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 fn make_job() -> DistributedJob {
752 DistributedJob {
753 id: Uuid::new_v4(),
754 task_id: Uuid::new_v4(),
755 source_url: "s3://bucket/input.mp4".to_string(),
756 codec: "av1".to_string(),
757 strategy: SplitStrategy::SegmentBased,
758 priority: JobPriority::Normal,
759 params: EncodingParams::default(),
760 output_url: "s3://bucket/output.mp4".to_string(),
761 deadline: None,
762 }
763 }
764
765 #[test]
766 fn test_default_config() {
767 let config = DistributedConfig::default();
768 assert_eq!(config.coordinator_addr, "127.0.0.1:50051");
769 assert_eq!(config.max_retries, 3);
770 assert_eq!(config.max_concurrent_jobs, 4);
771 }
772
773 #[tokio::test]
774 async fn test_encoder_creation() {
775 let encoder = DistributedEncoder::with_defaults();
776 assert_eq!(encoder.config().coordinator_addr, "127.0.0.1:50051");
777 }
778
779 #[test]
780 fn test_job_priority_ordering() {
781 assert!(JobPriority::Critical > JobPriority::High);
782 assert!(JobPriority::High > JobPriority::Normal);
783 assert!(JobPriority::Normal > JobPriority::Low);
784 }
785
786 #[tokio::test]
787 async fn test_submit_and_query_job() {
788 let encoder = DistributedEncoder::with_defaults();
789 let job = make_job();
790 let job_id = job.id;
791
792 let returned_id = encoder
793 .submit_job(job)
794 .await
795 .expect("submit should succeed");
796 assert_eq!(returned_id, job_id);
797
798 let status = encoder
799 .job_status(job_id)
800 .await
801 .expect("status should succeed");
802 assert_eq!(status, JobStatus::Pending);
803 }
804
805 #[tokio::test]
806 async fn test_submit_rejects_empty_source_url() {
807 let encoder = DistributedEncoder::with_defaults();
808 let mut job = make_job();
809 job.source_url = String::new();
810
811 let result = encoder.submit_job(job).await;
812 assert!(result.is_err());
813 }
814
815 #[tokio::test]
816 async fn test_submit_rejects_empty_codec() {
817 let encoder = DistributedEncoder::with_defaults();
818 let mut job = make_job();
819 job.codec = String::new();
820
821 let result = encoder.submit_job(job).await;
822 assert!(result.is_err());
823 }
824
825 #[tokio::test]
826 async fn test_submit_rejects_empty_output_url() {
827 let encoder = DistributedEncoder::with_defaults();
828 let mut job = make_job();
829 job.output_url = String::new();
830
831 let result = encoder.submit_job(job).await;
832 assert!(result.is_err());
833 }
834
835 #[tokio::test]
836 async fn test_submit_rejects_duplicate_id() {
837 let encoder = DistributedEncoder::with_defaults();
838 let job = make_job();
839 let dup = job.clone();
840
841 encoder
842 .submit_job(job)
843 .await
844 .expect("first submit should succeed");
845 let result = encoder.submit_job(dup).await;
846 assert!(result.is_err());
847 }
848
849 #[tokio::test]
850 async fn test_cancel_job() {
851 let encoder = DistributedEncoder::with_defaults();
852 let job = make_job();
853 let job_id = job.id;
854
855 encoder
856 .submit_job(job)
857 .await
858 .expect("submit should succeed");
859 encoder
860 .cancel_job(job_id)
861 .await
862 .expect("cancel should succeed");
863
864 let status = encoder
865 .job_status(job_id)
866 .await
867 .expect("status should succeed");
868 assert_eq!(status, JobStatus::Cancelled);
869 }
870
871 #[tokio::test]
872 async fn test_cancel_nonexistent_job_fails() {
873 let encoder = DistributedEncoder::with_defaults();
874 let result = encoder.cancel_job(Uuid::new_v4()).await;
875 assert!(result.is_err());
876 }
877
878 #[tokio::test]
879 async fn test_cancel_completed_job_fails() {
880 let encoder = DistributedEncoder::with_defaults();
881 let job = make_job();
882 let job_id = job.id;
883
884 encoder
885 .submit_job(job)
886 .await
887 .expect("submit should succeed");
888 encoder
890 .advance_job(job_id)
891 .await
892 .expect("advance should succeed"); encoder
894 .advance_job(job_id)
895 .await
896 .expect("advance should succeed"); encoder
898 .advance_job(job_id)
899 .await
900 .expect("advance should succeed"); let result = encoder.cancel_job(job_id).await;
903 assert!(result.is_err());
904 }
905
906 #[tokio::test]
907 async fn test_advance_job_lifecycle() {
908 let encoder = DistributedEncoder::with_defaults();
909 let job = make_job();
910 let job_id = job.id;
911
912 encoder
913 .submit_job(job)
914 .await
915 .expect("submit should succeed");
916
917 let s1 = encoder
918 .advance_job(job_id)
919 .await
920 .expect("advance should succeed");
921 assert_eq!(s1, JobStatus::Assigned);
922
923 let s2 = encoder
924 .advance_job(job_id)
925 .await
926 .expect("advance should succeed");
927 assert_eq!(s2, JobStatus::InProgress);
928
929 let s3 = encoder
930 .advance_job(job_id)
931 .await
932 .expect("advance should succeed");
933 assert_eq!(s3, JobStatus::Completed);
934
935 let result = encoder.advance_job(job_id).await;
937 assert!(result.is_err());
938 }
939
940 #[tokio::test]
941 async fn test_fail_job_with_retry() {
942 let config = DistributedConfig {
943 max_retries: 2,
944 fault_tolerance: true,
945 ..DistributedConfig::default()
946 };
947 let encoder = DistributedEncoder::new(config);
948 let job = make_job();
949 let job_id = job.id;
950
951 encoder
952 .submit_job(job)
953 .await
954 .expect("submit should succeed");
955
956 encoder.fail_job(job_id).await.expect("fail should succeed");
958 let status = encoder
959 .job_status(job_id)
960 .await
961 .expect("status should succeed");
962 assert_eq!(status, JobStatus::Pending);
963 let retries = encoder
964 .job_retries(job_id)
965 .await
966 .expect("retries should succeed");
967 assert_eq!(retries, 1);
968
969 encoder.fail_job(job_id).await.expect("fail should succeed");
971 let retries = encoder
972 .job_retries(job_id)
973 .await
974 .expect("retries should succeed");
975 assert_eq!(retries, 2);
976
977 encoder.fail_job(job_id).await.expect("fail should succeed");
979 let status = encoder
980 .job_status(job_id)
981 .await
982 .expect("status should succeed");
983 assert_eq!(status, JobStatus::Failed);
984 }
985
986 #[tokio::test]
987 async fn test_fail_without_fault_tolerance() {
988 let config = DistributedConfig {
989 fault_tolerance: false,
990 ..DistributedConfig::default()
991 };
992 let encoder = DistributedEncoder::new(config);
993 let job = make_job();
994 let job_id = job.id;
995
996 encoder
997 .submit_job(job)
998 .await
999 .expect("submit should succeed");
1000 encoder.fail_job(job_id).await.expect("fail should succeed");
1001
1002 let status = encoder
1003 .job_status(job_id)
1004 .await
1005 .expect("status should succeed");
1006 assert_eq!(status, JobStatus::Failed);
1007 }
1008
1009 #[tokio::test]
1010 async fn test_concurrency_limit() {
1011 let config = DistributedConfig {
1012 max_concurrent_jobs: 2,
1013 ..DistributedConfig::default()
1014 };
1015 let encoder = DistributedEncoder::new(config);
1016
1017 encoder
1018 .submit_job(make_job())
1019 .await
1020 .expect("first should succeed");
1021 encoder
1022 .submit_job(make_job())
1023 .await
1024 .expect("second should succeed");
1025
1026 let result = encoder.submit_job(make_job()).await;
1028 assert!(result.is_err());
1029 }
1030
1031 #[tokio::test]
1032 async fn test_concurrency_freed_after_cancel() {
1033 let config = DistributedConfig {
1034 max_concurrent_jobs: 1,
1035 ..DistributedConfig::default()
1036 };
1037 let encoder = DistributedEncoder::new(config);
1038
1039 let job = make_job();
1040 let job_id = job.id;
1041 encoder.submit_job(job).await.expect("first should succeed");
1042
1043 assert!(encoder.submit_job(make_job()).await.is_err());
1045
1046 encoder
1048 .cancel_job(job_id)
1049 .await
1050 .expect("cancel should succeed");
1051
1052 encoder
1054 .submit_job(make_job())
1055 .await
1056 .expect("after cancel should succeed");
1057 }
1058
1059 #[tokio::test]
1060 async fn test_list_jobs() {
1061 let encoder = DistributedEncoder::with_defaults();
1062 let j1 = make_job();
1063 let j2 = make_job();
1064 let id1 = j1.id;
1065 let id2 = j2.id;
1066
1067 encoder.submit_job(j1).await.expect("submit should succeed");
1068 encoder.submit_job(j2).await.expect("submit should succeed");
1069
1070 let jobs = encoder.list_jobs().await;
1071 assert_eq!(jobs.len(), 2);
1072
1073 let ids: Vec<Uuid> = jobs.iter().map(|(id, _)| *id).collect();
1074 assert!(ids.contains(&id1));
1075 assert!(ids.contains(&id2));
1076 }
1077
1078 #[tokio::test]
1079 async fn test_job_count() {
1080 let encoder = DistributedEncoder::with_defaults();
1081 assert_eq!(encoder.job_count().await, 0);
1082
1083 encoder
1084 .submit_job(make_job())
1085 .await
1086 .expect("submit should succeed");
1087 assert_eq!(encoder.job_count().await, 1);
1088 assert_eq!(encoder.active_job_count().await, 1);
1089 }
1090
1091 #[tokio::test]
1092 async fn test_status_nonexistent_job_fails() {
1093 let encoder = DistributedEncoder::with_defaults();
1094 let result = encoder.job_status(Uuid::new_v4()).await;
1095 assert!(result.is_err());
1096 }
1097
1098 #[tokio::test]
1099 async fn test_job_timeout_detection() {
1100 let config = DistributedConfig {
1101 job_timeout: Duration::from_millis(1),
1102 ..DistributedConfig::default()
1103 };
1104 let encoder = DistributedEncoder::new(config);
1105 let job = make_job();
1106 let job_id = job.id;
1107
1108 encoder
1109 .submit_job(job)
1110 .await
1111 .expect("submit should succeed");
1112
1113 tokio::time::sleep(Duration::from_millis(10)).await;
1115
1116 let status = encoder
1117 .job_status(job_id)
1118 .await
1119 .expect("status should succeed");
1120 assert_eq!(status, JobStatus::Failed);
1121 }
1122
1123 #[tokio::test]
1124 async fn test_submit_past_deadline_rejected() {
1125 let encoder = DistributedEncoder::with_defaults();
1126 let mut job = make_job();
1127 job.deadline = Some(0); let result = encoder.submit_job(job).await;
1130 assert!(result.is_err());
1131 }
1132
1133 #[tokio::test]
1134 async fn test_coordinator_server_starts() {
1135 let port = {
1138 let l = std::net::TcpListener::bind("127.0.0.1:0")
1139 .expect("ephemeral port allocation must succeed");
1140 l.local_addr().expect("local_addr must work").port()
1141 };
1142 let addr = format!("127.0.0.1:{port}");
1143 let config = DistributedConfig {
1144 coordinator_addr: addr.clone(),
1145 ..DistributedConfig::default()
1146 };
1147 let encoder = DistributedEncoder::new(config);
1148 tokio::time::sleep(std::time::Duration::from_millis(80)).await;
1150 assert!(
1152 encoder.server_handle.is_some(),
1153 "server_handle should be set for a valid coordinator_addr"
1154 );
1155 let connect_result = tokio::net::TcpStream::connect(addr.as_str()).await;
1157 assert!(
1158 connect_result.is_ok(),
1159 "coordinator TCP control server should be accepting connections on {addr}"
1160 );
1161 }
1163
1164 #[tokio::test]
1165 async fn test_distributed_encoder_local_fallback_still_works() {
1166 let config = DistributedConfig {
1170 coordinator_addr: "127.0.0.1:1".to_string(),
1171 ..DistributedConfig::default()
1172 };
1173 let encoder = DistributedEncoder::new(config);
1174 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1176
1177 let job = make_job();
1179 let result = encoder.submit_job(job).await;
1180 assert!(
1181 result.is_ok(),
1182 "local job store must work even when gRPC server bind may fail"
1183 );
1184 }
1185}