Skip to main content

scheduler/
valkey_coordinated_store.rs

1mod codec;
2mod inflight_commands;
3mod keys;
4mod state_commands;
5
6use crate::coordinated_store::{
7    CoordinatedClaim, CoordinatedCompletion, CoordinatedLeaseConfig, CoordinatedPendingTrigger,
8    CoordinatedRuntimeState, CoordinatedStateStore,
9};
10use crate::error::{ExecutionGuardErrorKind, StoreErrorKind};
11use crate::execution_guard::{ExecutionGuardRenewal, ExecutionGuardScope, ExecutionLease};
12use crate::model::JobState;
13use crate::valkey_runtime::{ValkeyRecoveryConfig, ValkeyRuntime};
14use crate::valkey_store::ValkeyStoreError;
15use keys::{DEFAULT_EXECUTION_KEY_PREFIX, DEFAULT_STATE_KEY_PREFIX};
16use redis::Client;
17
18#[derive(Debug, Clone)]
19pub struct ValkeyCoordinatedStateStore {
20    pub(super) runtime: ValkeyRuntime,
21    pub(super) state_key_prefix: String,
22    pub(super) execution_key_prefix: String,
23}
24
25impl ValkeyCoordinatedStateStore {
26    pub async fn new(url: impl AsRef<str>) -> Result<Self, redis::RedisError> {
27        Self::with_prefixes(url, DEFAULT_STATE_KEY_PREFIX, DEFAULT_EXECUTION_KEY_PREFIX).await
28    }
29
30    pub async fn with_prefixes(
31        url: impl AsRef<str>,
32        state_key_prefix: impl Into<String>,
33        execution_key_prefix: impl Into<String>,
34    ) -> Result<Self, redis::RedisError> {
35        Self::with_prefixes_and_recovery_config(
36            url,
37            state_key_prefix,
38            execution_key_prefix,
39            ValkeyRecoveryConfig::default(),
40        )
41        .await
42    }
43
44    pub async fn with_recovery_config(
45        url: impl AsRef<str>,
46        config: ValkeyRecoveryConfig,
47    ) -> Result<Self, redis::RedisError> {
48        Self::with_prefixes_and_recovery_config(
49            url,
50            DEFAULT_STATE_KEY_PREFIX,
51            DEFAULT_EXECUTION_KEY_PREFIX,
52            config,
53        )
54        .await
55    }
56
57    pub async fn with_prefixes_and_recovery_config(
58        url: impl AsRef<str>,
59        state_key_prefix: impl Into<String>,
60        execution_key_prefix: impl Into<String>,
61        config: ValkeyRecoveryConfig,
62    ) -> Result<Self, redis::RedisError> {
63        let client = Client::open(url.as_ref())?;
64        let runtime = ValkeyRuntime::from_client(client, config).await?;
65        Ok(Self {
66            runtime,
67            state_key_prefix: state_key_prefix.into(),
68            execution_key_prefix: execution_key_prefix.into(),
69        })
70    }
71}
72
73impl CoordinatedStateStore for ValkeyCoordinatedStateStore {
74    type Error = ValkeyStoreError;
75
76    async fn load_or_initialize(
77        &self,
78        job_id: &str,
79        initial_state: JobState,
80    ) -> Result<CoordinatedRuntimeState, Self::Error> {
81        self.load_or_initialize_state(job_id, initial_state).await
82    }
83
84    async fn save_state(
85        &self,
86        job_id: &str,
87        revision: u64,
88        state: &JobState,
89    ) -> Result<bool, Self::Error> {
90        self.save_runtime_state(job_id, revision, state).await
91    }
92
93    async fn reclaim_inflight(
94        &self,
95        job_id: &str,
96        resource_id: &str,
97        lease_config: CoordinatedLeaseConfig,
98    ) -> Result<Option<CoordinatedClaim>, Self::Error> {
99        self.reclaim_inflight_claim(job_id, resource_id, lease_config)
100            .await
101    }
102
103    async fn claim_trigger(
104        &self,
105        job_id: &str,
106        resource_id: &str,
107        revision: u64,
108        trigger: CoordinatedPendingTrigger,
109        next_state: &JobState,
110        lease_config: CoordinatedLeaseConfig,
111        scope: ExecutionGuardScope,
112    ) -> Result<Option<CoordinatedClaim>, Self::Error> {
113        self.claim_trigger_for_scope(
114            job_id,
115            resource_id,
116            revision,
117            trigger,
118            next_state,
119            lease_config,
120            scope,
121        )
122        .await
123    }
124
125    async fn renew(
126        &self,
127        lease: &ExecutionLease,
128        lease_config: CoordinatedLeaseConfig,
129    ) -> Result<ExecutionGuardRenewal, Self::Error> {
130        self.renew_claim_lease(lease, lease_config).await
131    }
132
133    async fn complete(
134        &self,
135        job_id: &str,
136        lease: &ExecutionLease,
137        completion: CoordinatedCompletion,
138    ) -> Result<bool, Self::Error> {
139        self.complete_claim(job_id, lease, completion).await
140    }
141
142    async fn delete(&self, job_id: &str) -> Result<(), Self::Error> {
143        self.delete_state(job_id).await
144    }
145
146    async fn pause(&self, job_id: &str) -> Result<bool, Self::Error> {
147        self.pause_state(job_id).await
148    }
149
150    async fn resume(&self, job_id: &str) -> Result<bool, Self::Error> {
151        self.resume_state(job_id).await
152    }
153
154    fn classify_store_error(error: &Self::Error) -> StoreErrorKind
155    where
156        Self: Sized,
157    {
158        classify_store_error(error)
159    }
160
161    fn classify_guard_error(error: &Self::Error) -> ExecutionGuardErrorKind
162    where
163        Self: Sized,
164    {
165        classify_guard_error(error)
166    }
167}
168
169fn classify_store_error(error: &ValkeyStoreError) -> StoreErrorKind {
170    if matches!(error, ValkeyStoreError::Codec(_)) {
171        StoreErrorKind::Data
172    } else if error.is_connection_issue() {
173        StoreErrorKind::Connection
174    } else {
175        StoreErrorKind::Unknown
176    }
177}
178
179fn classify_guard_error(error: &ValkeyStoreError) -> ExecutionGuardErrorKind {
180    if matches!(error, ValkeyStoreError::Codec(_)) {
181        ExecutionGuardErrorKind::Data
182    } else if error.is_connection_issue() {
183        ExecutionGuardErrorKind::Connection
184    } else {
185        ExecutionGuardErrorKind::Unknown
186    }
187}