ironflow_store/store.rs
1//! The [`RunStore`] trait — async storage abstraction for runs and steps.
2//!
3//! Implement this trait to plug in any backing store. Built-in implementations:
4//!
5//! - [`InMemoryStore`](crate::memory::InMemoryStore) — development and testing.
6//! - `PostgresStore` — production (behind the `store-postgres` feature).
7
8use std::future::Future;
9use std::pin::Pin;
10
11use chrono::{DateTime, Utc};
12use uuid::Uuid;
13
14use crate::api_key_store::ApiKeyStore;
15use crate::approval_delegation_store::ApprovalDelegationStore;
16use crate::artifact_store::ArtifactStore;
17use crate::audit_log_store::AuditLogStore;
18use crate::entities::{
19 ConcurrencyGroupBacklog, LeaseRequest, NewRun, NewStep, NewStepDependency, Page, PurgePolicy,
20 PurgeableRun, ReapedRun, Run, RunCreation, RunFilter, RunStats, RunStatus, RunUpdate,
21 StatsHistoryBucket, StatsHistoryFilter, Step, StepApproval, StepDependency, StepUpdate,
22 WorkerCapabilities,
23};
24use crate::error::StoreError;
25use crate::log_store::LogStore;
26use crate::provider_account_store::ProviderAccountStore;
27use crate::schedule_store::ScheduleStore;
28use crate::secret_store::SecretStore;
29use crate::signal_store::SignalStore;
30use crate::user_store::UserStore;
31
32/// Boxed future for [`RunStore`] methods — ensures object safety for `dyn RunStore`.
33pub type StoreFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, StoreError>> + Send + 'a>>;
34
35/// Error recorded on a run that exhausted its retries through lease expiries.
36///
37/// Set by [`RunStore::reap_expired_leases`] when a run has been recovered more
38/// than `max_retries` times.
39pub const LEASE_EXPIRED_ERROR: &str = "worker lease expired";
40
41/// Error recorded on a step that was running when its worker lost the lease.
42///
43/// Set by the reaper on the `Running` steps of a run that
44/// [`RunStore::reap_expired_leases`] requeued. The engine executes such a step
45/// again at the same position when the run is picked up, keeping the
46/// interrupted record in the step history.
47///
48/// # Examples
49///
50/// ```
51/// use ironflow_store::store::{LEASE_EXPIRED_ERROR, STEP_INTERRUPTED_ERROR};
52///
53/// assert_eq!(STEP_INTERRUPTED_ERROR, "interrupted: worker lease lost");
54/// assert_ne!(STEP_INTERRUPTED_ERROR, LEASE_EXPIRED_ERROR);
55/// ```
56pub const STEP_INTERRUPTED_ERROR: &str = "interrupted: worker lease lost";
57
58/// Async storage abstraction for workflow runs and steps.
59///
60/// All methods return a [`StoreFuture`] (boxed future) to maintain object safety,
61/// allowing the store to be used as `Arc<dyn RunStore>`.
62///
63/// # Examples
64///
65/// ```no_run
66/// use std::collections::HashMap;
67/// use ironflow_store::prelude::*;
68/// use serde_json::json;
69/// use uuid::Uuid;
70///
71/// # async fn example() -> Result<(), ironflow_store::error::StoreError> {
72/// let store = InMemoryStore::new();
73///
74/// let run = store.create_run(NewRun {
75/// workflow_name: "deploy".to_string(),
76/// trigger: TriggerKind::Manual,
77/// payload: json!({}),
78/// max_retries: 3,
79/// handler_version: None,
80/// labels: HashMap::new(),
81/// scheduled_at: None,
82/// created_by: None,
83/// idempotency_key: None,
84/// concurrency_key: None,
85/// priority: 0,
86/// concurrency_limits: Vec::new(),
87/// max_cost_usd: None,
88/// worker_tags: Vec::new(),
89/// }).await?.into_run();
90///
91/// let fetched = store.get_run(run.id).await?;
92/// assert!(fetched.is_some());
93/// # Ok(())
94/// # }
95/// ```
96pub trait RunStore: Send + Sync {
97 /// Create a new run in `Pending` status.
98 ///
99 /// When [`NewRun::idempotency_key`] is set and already bound to a run created
100 /// within [`IDEMPOTENCY_WINDOW`](crate::entities::IDEMPOTENCY_WINDOW), nothing is
101 /// inserted and that run is returned as [`RunCreation::Existing`]. A key bound to
102 /// an older run is released and reused for the new one.
103 ///
104 /// Concurrent calls sharing the same key resolve to a single run: exactly one
105 /// receives [`RunCreation::Created`], the others [`RunCreation::Existing`].
106 ///
107 /// When [`NewRun::concurrency_key`] is set, the idempotency lookup runs first,
108 /// then the key is checked: concurrent calls sharing it are serialized, and
109 /// at most one non-terminal run holds it at a time.
110 ///
111 /// [`NewRun::concurrency_limits`] is validated before anything is written.
112 ///
113 /// # Errors
114 ///
115 /// Returns [`StoreError::ConcurrencyConflict`](crate::error::StoreError::ConcurrencyConflict)
116 /// when a run that is not Completed, Failed, Warning or Cancelled already
117 /// holds [`NewRun::concurrency_key`],
118 /// [`StoreError::InvalidConcurrencyLimit`](crate::error::StoreError::InvalidConcurrencyLimit)
119 /// when [`NewRun::concurrency_limits`] holds an empty or too long group, a
120 /// zero limit or a duplicated group, and a database error when the backing
121 /// store fails.
122 fn create_run(&self, req: NewRun) -> StoreFuture<'_, RunCreation>;
123
124 /// Look up the run bound to an idempotency key.
125 ///
126 /// Returns `None` when the key is unknown, or when the run holding it is older
127 /// than [`IDEMPOTENCY_WINDOW`](crate::entities::IDEMPOTENCY_WINDOW).
128 fn find_run_by_idempotency_key(&self, key: &str) -> StoreFuture<'_, Option<Run>>;
129
130 /// Get a run by ID. Returns `None` if not found.
131 fn get_run(&self, id: Uuid) -> StoreFuture<'_, Option<Run>>;
132
133 /// List runs matching the given filter, with pagination.
134 ///
135 /// Results are ordered by `created_at` descending (newest first).
136 fn list_runs(&self, filter: RunFilter, page: u32, per_page: u32) -> StoreFuture<'_, Page<Run>>;
137
138 /// Update a run's status with FSM validation.
139 ///
140 /// # Errors
141 ///
142 /// Returns [`StoreError::InvalidTransition`] if the transition is not allowed.
143 /// Returns [`StoreError::RunNotFound`] if the run does not exist.
144 fn update_run_status(&self, id: Uuid, new_status: RunStatus) -> StoreFuture<'_, ()>;
145
146 /// Apply a partial update to a run.
147 ///
148 /// [`RunUpdate::lease`] is applied in the same transaction as the status
149 /// transition, after it: `status: Running` with
150 /// [`LeaseUpdate::Set`](crate::entities::LeaseUpdate::Set) leaves the run
151 /// `Running` and owned by that worker, so it is never `Running` without a
152 /// lease in between. [`LeaseUpdate::Release`](crate::entities::LeaseUpdate::Release)
153 /// drops the lease without touching the status. An explicit lease change
154 /// wins over the clearing that a transition out of `Running` does.
155 ///
156 /// # Errors
157 ///
158 /// Returns [`StoreError::RunNotFound`] if the run does not exist.
159 fn update_run(&self, id: Uuid, update: RunUpdate) -> StoreFuture<'_, ()>;
160
161 /// List the non-terminal descendants of a run, oldest first.
162 ///
163 /// A descendant is a sub-workflow run
164 /// ([`TriggerKind::Workflow`](crate::entities::TriggerKind::Workflow))
165 /// reached from `run_id` through
166 /// [`PARENT_RUN_ID_LABEL`](crate::entities::PARENT_RUN_ID_LABEL), at any
167 /// depth. Terminal runs are not returned, but their own descendants are:
168 /// a child left running under a finished parent is still found. Labels are
169 /// data, so a chain that loops back on itself is followed once and never
170 /// returns `run_id` itself.
171 ///
172 /// Returns an empty list for an unknown run or a run without children.
173 ///
174 /// # Errors
175 ///
176 /// Returns a database error when the backing store fails.
177 fn list_active_descendants(&self, run_id: Uuid) -> StoreFuture<'_, Vec<Run>>;
178
179 /// Atomically pick the oldest pending run and transition it to `Running`.
180 ///
181 /// In PostgreSQL, this uses `SELECT FOR UPDATE SKIP LOCKED` for safe
182 /// multi-worker concurrency. The in-memory implementation uses a write lock.
183 ///
184 /// When `lease` is `Some`, the worker lease is attached in the same
185 /// transaction as the status change, so a run is never `Running` without an
186 /// owner. Pass `None` for callers that execute runs in-process and cannot
187 /// refresh a lease (inline execution, API-side resume): those runs are never
188 /// recovered by [`reap_expired_leases`](Self::reap_expired_leases).
189 ///
190 /// Concurrency groups gate the pick: a run carrying
191 /// [`Run::concurrency_limits`] is skipped while, for any of its groups, the
192 /// number of root runs in state `Running` carrying that group is already at
193 /// or above the run's own limit for it. Sleeping, awaiting approval,
194 /// retrying and pending runs do not count, and sub-workflow runs
195 /// ([`TriggerKind::Workflow`](crate::entities::TriggerKind::Workflow)) are
196 /// never counted. A held-back run does not block the queue: the oldest
197 /// eligible run wins. The check is atomic across concurrent callers, so a
198 /// group never exceeds its limit.
199 ///
200 /// Returns `None` if no pending runs are available.
201 ///
202 /// Equivalent to [`pick_next_pending_for`](Self::pick_next_pending_for)
203 /// with no worker capabilities: every run is eligible.
204 fn pick_next_pending(&self, lease: Option<LeaseRequest>) -> StoreFuture<'_, Option<Run>> {
205 self.pick_next_pending_for(lease, None)
206 }
207
208 /// Atomically pick the oldest pending run the worker can take and
209 /// transition it to `Running`.
210 ///
211 /// Same contract as [`pick_next_pending`](Self::pick_next_pending), with
212 /// worker routing on top: when `capabilities` is `Some`, a run is only
213 /// eligible when [`WorkerCapabilities::can_take`] accepts its workflow name
214 /// and its [`Run::worker_tags`]. An ineligible run is skipped and never
215 /// blocks younger runs. `None` keeps the legacy behavior of a worker that
216 /// sends no capabilities: every run is eligible.
217 ///
218 /// Returns `None` if no eligible pending run is available.
219 fn pick_next_pending_for(
220 &self,
221 lease: Option<LeaseRequest>,
222 capabilities: Option<WorkerCapabilities>,
223 ) -> StoreFuture<'_, Option<Run>>;
224
225 /// Extend the worker lease on a run and return the new expiry.
226 ///
227 /// # Errors
228 ///
229 /// Returns [`StoreError::RunNotFound`] if the run does not exist.
230 /// Returns [`StoreError::LeaseLost`] if the run is no longer `Running` or if
231 /// the lease belongs to another worker — the caller must stop executing it.
232 fn renew_lease(&self, id: Uuid, lease: LeaseRequest) -> StoreFuture<'_, DateTime<Utc>>;
233
234 /// Count, for each concurrency group, the due runs it currently holds back.
235 ///
236 /// A run is counted when it is pending or retrying, due (no
237 /// `scheduled_at` in the future) and not pickable because the group is
238 /// saturated for its own limit (see [`pick_next_pending`](Self::pick_next_pending)).
239 /// A run held back by two groups counts in both. Groups holding back no
240 /// run are omitted. Results are sorted by group name.
241 ///
242 /// # Errors
243 ///
244 /// Returns a database error when the backing store fails.
245 ///
246 /// # Examples
247 ///
248 /// ```no_run
249 /// use ironflow_store::store::RunStore;
250 ///
251 /// # async fn example(store: &dyn RunStore) -> Result<(), ironflow_store::error::StoreError> {
252 /// for backlog in store.count_blocked_runs_by_group().await? {
253 /// println!("{}: {} runs held back", backlog.group, backlog.blocked_runs);
254 /// }
255 /// # Ok(())
256 /// # }
257 /// ```
258 fn count_blocked_runs_by_group(&self) -> StoreFuture<'_, Vec<ConcurrencyGroupBacklog>>;
259
260 /// Recover runs whose worker lease expired, at most `limit` per call.
261 ///
262 /// Each recovered run has [`Run::lease_recoveries`] incremented and its lease
263 /// cleared, then goes back to `Pending` — or to `Failed` with
264 /// [`LEASE_EXPIRED_ERROR`] once more than `max_retries` recoveries happened.
265 /// Runs without a lease are never touched.
266 /// A root run resumed through its sub-workflow child carries the lease the
267 /// child held (see [`RunUpdate::lease`]), so it is recovered like any run.
268 ///
269 /// [`Run::retry_count`], and so the attempt number of the steps created
270 /// afterwards, is left unchanged: a requeued run resumes in the same attempt
271 /// and replays the steps it already finished.
272 ///
273 /// The whole batch is atomic per run (`FOR UPDATE SKIP LOCKED` in
274 /// PostgreSQL), so concurrent reapers never recover the same run twice.
275 ///
276 /// Callers are responsible for the side effects that follow a recovery:
277 /// failing orphaned steps and publishing status-change events.
278 fn reap_expired_leases(&self, limit: u32) -> StoreFuture<'_, Vec<ReapedRun>>;
279
280 /// Atomically claim approval steps whose SLA deadline has passed.
281 ///
282 /// Returns the claimed steps with their *pre-claim* `approval_deadline_at`
283 /// still populated, so the caller can report which deadline fired. The
284 /// timer is cleared in the same transaction, so a deadline fires at most
285 /// once even with several API instances running the escalator (the
286 /// PostgreSQL implementation uses `FOR UPDATE SKIP LOCKED`).
287 ///
288 /// Only steps still in [`StepStatus::AwaitingApproval`](crate::entities::StepStatus::AwaitingApproval)
289 /// are returned.
290 ///
291 /// Delivery is at most once: a caller that crashes between the claim and
292 /// the escalation leaves the gate open with no timer, the same trade-off
293 /// [`reap_expired_leases`](Self::reap_expired_leases) accepts.
294 fn claim_due_approval_deadlines(&self, limit: u32) -> StoreFuture<'_, Vec<Step>>;
295
296 /// Atomically wake the `Sleeping` runs whose `scheduled_at` has passed, at
297 /// most `limit` per call.
298 ///
299 /// Each claimed run goes `Sleeping -> Pending` (`delay_elapsed`) and has
300 /// its `scheduled_at` cleared in the same transaction, so a run is woken
301 /// exactly once even with several API instances running the waker (the
302 /// PostgreSQL implementation uses `FOR UPDATE SKIP LOCKED`). Runs are
303 /// claimed oldest `scheduled_at` first.
304 ///
305 /// Returns the runs as they are after the transition. Callers decide how
306 /// the requeued runs resume: a worker picks them up, or the API resumes
307 /// them in-process when it has no worker.
308 ///
309 /// # Errors
310 ///
311 /// Returns [`StoreError`] on storage failure.
312 fn claim_due_sleeping_runs(&self, limit: u32) -> StoreFuture<'_, Vec<Run>>;
313
314 /// Create a new step for a run.
315 ///
316 /// # Errors
317 ///
318 /// Returns [`StoreError::RunNotFound`] if the parent run does not exist.
319 fn create_step(&self, step: NewStep) -> StoreFuture<'_, Step>;
320
321 /// Apply a partial update to a step after execution.
322 ///
323 /// # Errors
324 ///
325 /// Returns [`StoreError::StepNotFound`] if the step does not exist.
326 fn update_step(&self, id: Uuid, update: StepUpdate) -> StoreFuture<'_, ()>;
327
328 /// Get a single step by ID. Returns `None` if not found.
329 fn get_step(&self, id: Uuid) -> StoreFuture<'_, Option<Step>>;
330
331 /// List all steps for a run, ordered by position ascending.
332 fn list_steps(&self, run_id: Uuid) -> StoreFuture<'_, Vec<Step>>;
333
334 /// Record a vote on an approval gate and return the updated step.
335 ///
336 /// The vote is appended atomically to [`Step::approvals`] unless the same
337 /// [`StepApproval::user_id`] already voted, in which case the step is
338 /// returned unchanged. Recording a vote never resolves the gate: the
339 /// caller compares the vote count against the step's
340 /// [`approval_requirement`](Step::approval_requirement).
341 ///
342 /// # Errors
343 ///
344 /// Returns [`StoreError::StepNotFound`] if the step does not exist.
345 fn record_step_approval(&self, step_id: Uuid, approval: StepApproval) -> StoreFuture<'_, Step>;
346
347 /// Get aggregated statistics across runs matching the filter.
348 ///
349 /// Returns counts of runs by terminal state, counts of active runs
350 /// (`Pending`, `Running`, `Retrying`, `AwaitingApproval` or `Sleeping`),
351 /// the number of runs awaiting approval, and totals for cost and duration.
352 /// Computed efficiently by the store implementation (single SQL query in
353 /// PostgreSQL).
354 ///
355 /// Pass [`RunFilter::default()`] to get stats across all runs.
356 fn get_stats(&self, filter: RunFilter) -> StoreFuture<'_, RunStats>;
357
358 /// Get time-bucketed historical statistics for trend charts.
359 ///
360 /// Aggregates runs created during the filter's period into time buckets
361 /// based on its granularity, counting every run status and computing
362 /// duration percentiles. Applies the same run filters as
363 /// [`get_stats`](Self::get_stats) (workflow substring, status, labels,
364 /// steps, author). Bucket boundaries are UTC and weeks start on Monday
365 /// (see [`HistoryGranularity::bucket_start`](crate::entities::HistoryGranularity::bucket_start)).
366 /// Returns buckets ordered by time ascending; empty buckets are omitted.
367 ///
368 /// # Errors
369 ///
370 /// Returns [`StoreError::Database`] on underlying store failures.
371 ///
372 /// # Examples
373 ///
374 /// ```no_run
375 /// use ironflow_store::entities::{StatsHistoryFilter, HistoryPeriod, HistoryGranularity};
376 /// use ironflow_store::store::RunStore;
377 ///
378 /// # async fn example(store: &dyn RunStore) -> Result<(), ironflow_store::error::StoreError> {
379 /// let filter = StatsHistoryFilter {
380 /// period: HistoryPeriod::SevenDays,
381 /// granularity: HistoryGranularity::OneDay,
382 /// ..StatsHistoryFilter::default()
383 /// };
384 /// let buckets = store.get_stats_history(filter).await?;
385 /// for b in &buckets {
386 /// println!("{}: {} completed, {} failed", b.time, b.completed, b.failed);
387 /// }
388 /// # Ok(())
389 /// # }
390 /// ```
391 fn get_stats_history(
392 &self,
393 filter: StatsHistoryFilter,
394 ) -> StoreFuture<'_, Vec<StatsHistoryBucket>>;
395
396 /// Create step dependency edges in batch.
397 ///
398 /// Each entry records that `step_id` depends on `depends_on`.
399 /// Duplicate edges are silently ignored.
400 ///
401 /// # Errors
402 ///
403 /// Returns [`StoreError`] if a referenced step does not exist.
404 fn create_step_dependencies(&self, deps: Vec<NewStepDependency>) -> StoreFuture<'_, ()>;
405
406 /// List all step dependencies for a given run.
407 ///
408 /// Returns every edge where either `step_id` or `depends_on` belongs
409 /// to the run. Ordered by `created_at` ascending.
410 fn list_step_dependencies(&self, run_id: Uuid) -> StoreFuture<'_, Vec<StepDependency>>;
411
412 /// List runs eligible for purging according to the given policy.
413 ///
414 /// A run is eligible when it is in a terminal state ([`RunStatus::is_terminal`])
415 /// **and** either older than `policy.max_age_days` or exceeding
416 /// `policy.max_runs_per_workflow` for its workflow (oldest first).
417 ///
418 /// Runs in non-terminal states (`Pending`, `Running`, `Retrying`,
419 /// `AwaitingApproval`) are never returned.
420 ///
421 /// # Examples
422 ///
423 /// ```no_run
424 /// use ironflow_store::entities::PurgePolicy;
425 /// use ironflow_store::store::RunStore;
426 ///
427 /// # async fn example(store: &dyn RunStore) -> Result<(), ironflow_store::error::StoreError> {
428 /// let policy = PurgePolicy { max_age_days: 90, max_runs_per_workflow: 1000, dry_run: false };
429 /// let purgeable = store.list_purgeable_runs(&policy, 100).await?;
430 /// for p in &purgeable {
431 /// println!("purge {} ({}): {}", p.run_id, p.workflow_name, p.reason);
432 /// }
433 /// # Ok(())
434 /// # }
435 /// ```
436 fn list_purgeable_runs(
437 &self,
438 policy: &PurgePolicy,
439 batch_size: u32,
440 ) -> StoreFuture<'_, Vec<PurgeableRun>>;
441
442 /// Delete a run and all its associated data (steps, step dependencies).
443 ///
444 /// Returns the `storage_key` of every artifact that belonged to the run,
445 /// so the caller can delete the corresponding blobs from the blob store.
446 ///
447 /// # Errors
448 ///
449 /// Returns [`StoreError::RunNotFound`] if the run does not exist.
450 ///
451 /// # Examples
452 ///
453 /// ```no_run
454 /// use ironflow_store::store::RunStore;
455 /// use uuid::Uuid;
456 ///
457 /// # async fn example(store: &dyn RunStore, run_id: Uuid) -> Result<(), ironflow_store::error::StoreError> {
458 /// let storage_keys = store.delete_run(run_id).await?;
459 /// // Caller deletes blobs from the blob store using these keys.
460 /// # Ok(())
461 /// # }
462 /// ```
463 fn delete_run(&self, id: Uuid) -> StoreFuture<'_, Vec<String>>;
464
465 /// Apply a partial update to a run and return the updated run.
466 ///
467 /// Combines [`update_run`](Self::update_run) and [`get_run`](Self::get_run) in
468 /// a single operation to avoid an extra round-trip. Store implementations
469 /// may override this for efficiency (e.g. reading within the same transaction).
470 ///
471 /// The default implementation calls `update_run` followed by `get_run`.
472 ///
473 /// # Errors
474 ///
475 /// Returns [`StoreError::RunNotFound`] if the run does not exist.
476 /// Returns [`StoreError::InvalidTransition`] if the status transition is not allowed.
477 fn update_run_returning(&self, id: Uuid, update: RunUpdate) -> StoreFuture<'_, Run> {
478 Box::pin(async move {
479 self.update_run(id, update).await?;
480 self.get_run(id).await?.ok_or(StoreError::RunNotFound(id))
481 })
482 }
483}
484
485/// Unified storage abstraction combining all store capabilities.
486///
487/// Implementors provide runs, steps, users, API keys, and secrets
488/// through a single type. Pick one backend (in-memory or PostgreSQL)
489/// and it handles everything.
490///
491/// Both [`InMemoryStore`](crate::memory::InMemoryStore) and
492/// [`PostgresStore`](crate::postgres::PostgresStore) implement this trait.
493///
494/// # Examples
495///
496/// ```no_run
497/// use std::collections::HashMap;
498/// use std::sync::Arc;
499/// use ironflow_store::prelude::*;
500///
501/// # async fn example() -> Result<(), ironflow_store::error::StoreError> {
502/// let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
503///
504/// // All capabilities through one reference
505/// let _run = store.create_run(NewRun {
506/// workflow_name: "deploy".to_string(),
507/// trigger: TriggerKind::Manual,
508/// payload: serde_json::json!({}),
509/// max_retries: 3,
510/// handler_version: None,
511/// labels: HashMap::new(),
512/// scheduled_at: None,
513/// created_by: None,
514/// idempotency_key: None,
515/// concurrency_key: None,
516/// priority: 0,
517/// concurrency_limits: Vec::new(),
518/// max_cost_usd: None,
519/// worker_tags: Vec::new(),
520/// }).await?.into_run();
521/// let _users = store.count_users().await?;
522/// # Ok(())
523/// # }
524/// ```
525pub trait Store:
526 RunStore
527 + UserStore
528 + ApiKeyStore
529 + SecretStore
530 + AuditLogStore
531 + ArtifactStore
532 + LogStore
533 + ScheduleStore
534 + ApprovalDelegationStore
535 + ProviderAccountStore
536 + SignalStore
537{
538}
539
540impl<
541 T: RunStore
542 + UserStore
543 + ApiKeyStore
544 + SecretStore
545 + AuditLogStore
546 + ArtifactStore
547 + LogStore
548 + ScheduleStore
549 + ApprovalDelegationStore
550 + ProviderAccountStore
551 + SignalStore,
552> Store for T
553{
554}