Skip to main content

mj_controller/controller/checkpoint/
capture.rs

1use super::*;
2
3impl Controller {
4    pub(super) async fn checkpoint_session_controlled_with_manager(
5        &mut self,
6        session_id: &str,
7        executor: &(impl CommandExecutor + Sync),
8        manager: Option<&SessionManagerControl>,
9    ) -> Result<CheckpointMetadata> {
10        let previous = self
11            .state
12            .sessions
13            .get(session_id)
14            .with_context(|| format!("unknown session {session_id}"))?
15            .clone();
16        previous.validate_configuration(&self.config)?;
17        ensure!(
18            !matches!(
19                previous.state,
20                SessionState::Closing | SessionState::Destroying
21            ),
22            "session {session_id} is already closing; resume that close instead of starting an ordinary checkpoint"
23        );
24        let record = self.state.sessions.get_mut(session_id).unwrap();
25        record.state = SessionState::Checkpointing;
26        record.updated_at = now();
27        record.last_checkpoint_error = None;
28        self.persist_session_transition_or_restore(
29            session_id,
30            &previous,
31            "persist checkpointing state before creating a checkpoint",
32        )?;
33
34        match self
35            .checkpoint_session_latched(
36                session_id,
37                executor,
38                manager,
39                LatchExclusivity::ReleaseAfterLatch,
40                CheckpointExportPolicy::Always,
41            )
42            .await
43        {
44            Ok(latched) => {
45                let artifact = latched.artifact.clone();
46                if let Err(error) = mj_core::test_hooks::reach_test_hook(
47                    "checkpoint_archive_before_database_publication",
48                ) {
49                    latched.abandon(session_id).await;
50                    return Err(remove_uninstalled_checkpoint(
51                        &artifact.metadata.archive_path,
52                        error,
53                    ));
54                }
55                {
56                    let record = self.state.sessions.get_mut(session_id).unwrap();
57                    record.state = SessionState::Running;
58                    record.native_session_id = Some(artifact.native_session_id.clone());
59                    record.checkpoint = Some(artifact.metadata.clone());
60                    record.updated_at = now();
61                    record.last_error = None;
62                    record.last_checkpoint_error = None;
63                }
64                let persist_started = Instant::now();
65                if let Err(error) = self.persist_checkpoint_transition_or_restore(
66                    session_id,
67                    &previous,
68                    "persist verified checkpoint before releasing relay history",
69                ) {
70                    latched.abandon(session_id).await;
71                    return Err(error);
72                }
73                tracing::info!(
74                    session_id,
75                    persist_ms = persist_started.elapsed().as_millis() as u64,
76                    "checkpoint metadata persisted"
77                );
78                prune_replaced_checkpoint(previous.checkpoint.as_ref(), &artifact.metadata);
79                release_projection_behind_checkpoint(session_id, &artifact.metadata);
80                if let Err(error) = latched.complete().await {
81                    // Only journal retention is at stake. A barrier that is
82                    // still open cannot dangle: the actor retries a failed
83                    // submission over a fresh connection, and the worker
84                    // cancels barriers whose submitting connection dropped.
85                    // The next checkpoint moves the recovery floor again.
86                    tracing::warn!(
87                        session_id,
88                        "verified checkpoint was saved, but the relay could not be told to release the history it covers: {error:#}"
89                    );
90                }
91                Ok(artifact.metadata)
92            }
93            Err(error) => {
94                // A deferred checkpoint says the agent was working, not that
95                // anything failed. Recording it would leave a warning on the
96                // session row until the next successful copy, so the caller is
97                // told and the row is left alone.
98                let deferred = checkpoint_was_deferred(&error);
99                if let Some(record) = self.state.sessions.get_mut(session_id) {
100                    record.state = if previous.state == SessionState::Checkpointing {
101                        SessionState::Running
102                    } else {
103                        previous.state
104                    };
105                    record.updated_at = now();
106                    if !deferred {
107                        record.last_checkpoint_error = Some(format!("{error:#}"));
108                    }
109                }
110                Err(self.persist_failed_checkpoint_state_or_restore(session_id, &previous, error))
111            }
112        }
113    }
114
115    /// Create, checksum, and durably install a recovery archive before
116    /// allowing the relay to garbage-collect through its event frontier.
117    pub async fn create_recovery_checkpoint_managed_controlled(
118        &self,
119        session_id: &str,
120        manager: &SessionManagerControl,
121        executor: &(impl CommandExecutor + Sync),
122    ) -> Result<CheckpointArtifact> {
123        self.create_recovery_checkpoint_with_manager(session_id, Some(manager), executor)
124            .await
125    }
126
127    pub(super) async fn create_recovery_checkpoint_with_manager(
128        &self,
129        session_id: &str,
130        manager: Option<&SessionManagerControl>,
131        executor: &(impl CommandExecutor + Sync),
132    ) -> Result<CheckpointArtifact> {
133        let previous_checkpoint = self
134            .state
135            .sessions
136            .get(session_id)
137            .with_context(|| format!("unknown session {session_id}"))?
138            .checkpoint
139            .clone();
140        let latched = self
141            .checkpoint_session_latched_with_recovery_stage(
142                session_id,
143                executor,
144                manager,
145                LatchExclusivity::ReleaseAfterLatch,
146                CheckpointExportPolicy::Always,
147                true,
148            )
149            .await?;
150        let artifact = latched.artifact.clone();
151        let verification = {
152            let _verifying = ProvisionStageGuard::new(executor, ProvisionStage::Verifying);
153            verify_checkpoint_artifact(session_id, &artifact)
154        };
155        if let Err(error) = verification {
156            latched.abandon(session_id).await;
157            return Err(remove_uninstalled_checkpoint(
158                &artifact.metadata.archive_path,
159                error.context("final recovery checkpoint verification"),
160            ));
161        }
162        if let Err(error) =
163            mj_core::test_hooks::reach_test_hook("checkpoint_archive_before_database_publication")
164        {
165            latched.abandon(session_id).await;
166            return Err(remove_uninstalled_checkpoint(
167                &artifact.metadata.archive_path,
168                error,
169            ));
170        }
171        let persist_started = Instant::now();
172        if let Err(error) = crate::database::record_recovery_success(
173            session_id,
174            &artifact.native_session_id,
175            &artifact.metadata,
176        ) {
177            latched.abandon(session_id).await;
178            return Err(error
179                .context("persist verified recovery checkpoint before releasing relay history"));
180        }
181        tracing::info!(
182            session_id,
183            persist_ms = persist_started.elapsed().as_millis() as u64,
184            "recovery checkpoint metadata persisted"
185        );
186        if let Err(error) = latched.complete().await {
187            // Only journal retention is at stake. A barrier that is still open
188            // cannot dangle: the actor retries a failed submission over a fresh
189            // connection, and the worker cancels barriers whose submitting
190            // connection dropped. The next checkpoint moves the floor again.
191            tracing::warn!(
192                session_id,
193                "recovery checkpoint was saved, but the relay could not be told to release the history it covers: {error:#}"
194            );
195        }
196        prune_replaced_checkpoint(previous_checkpoint.as_ref(), &artifact.metadata);
197        release_projection_behind_checkpoint(session_id, &artifact.metadata);
198        Ok(artifact)
199    }
200}