wip(#1838): extract deploy_applied_target shared deploy tail + dedup approve dispatch
This commit is contained in:
parent
449e3fcb7b
commit
5f5626456a
1 changed files with 139 additions and 101 deletions
|
|
@ -46,20 +46,12 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await
|
run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await
|
||||||
}
|
}
|
||||||
ApprovalKind::ApplyCommit => {
|
ApprovalKind::ApplyCommit => {
|
||||||
coord
|
enqueue_approval_rebuild(
|
||||||
.rebuild_queue
|
&coord,
|
||||||
.enqueue_full(crate::rebuild_queue::FullEnqueue {
|
&approval.agent,
|
||||||
kind: crate::rebuild_queue::QueueKind::Rebuild,
|
id,
|
||||||
agent: approval.agent.clone(),
|
format!("approval #{id} apply commit"),
|
||||||
source: crate::rebuild_queue::QueueSource::Approval,
|
);
|
||||||
reason: format!("approval #{id} apply commit"),
|
|
||||||
parent_id: None,
|
|
||||||
inputs: Vec::new(),
|
|
||||||
approval_id: Some(id),
|
|
||||||
perm_payload: None,
|
|
||||||
depends_on: Vec::new(),
|
|
||||||
});
|
|
||||||
coord.emit_rebuild_queue_snapshot();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
ApprovalKind::UpdateMetaInputs => {
|
ApprovalKind::UpdateMetaInputs => {
|
||||||
|
|
@ -134,25 +126,43 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
// dispatches MergeConfigPr approvals to `run_merge_config_pr`
|
// dispatches MergeConfigPr approvals to `run_merge_config_pr`
|
||||||
// (verify the reviewed PR head, ff the forge config repo's
|
// (verify the reviewed PR head, ff the forge config repo's
|
||||||
// main to it, mark merged, then the shared deploy tail).
|
// main to it, mark merged, then the shared deploy tail).
|
||||||
coord
|
enqueue_approval_rebuild(
|
||||||
.rebuild_queue
|
&coord,
|
||||||
.enqueue_full(crate::rebuild_queue::FullEnqueue {
|
&approval.agent,
|
||||||
kind: crate::rebuild_queue::QueueKind::Rebuild,
|
id,
|
||||||
agent: approval.agent.clone(),
|
format!("approval #{id} merge config pr"),
|
||||||
source: crate::rebuild_queue::QueueSource::Approval,
|
);
|
||||||
reason: format!("approval #{id} merge config pr"),
|
|
||||||
parent_id: None,
|
|
||||||
inputs: Vec::new(),
|
|
||||||
approval_id: Some(id),
|
|
||||||
perm_payload: None,
|
|
||||||
depends_on: Vec::new(),
|
|
||||||
});
|
|
||||||
coord.emit_rebuild_queue_snapshot();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enqueue a `Rebuild` queue entry tied to an approval id. Shared by the
|
||||||
|
/// `ApplyCommit` and `MergeConfigPr` dispatch arms — both end in a container
|
||||||
|
/// rebuild routed through the queue, differing only in the queue `reason`.
|
||||||
|
/// The queue worker branches on the approval's kind to pick the right handler.
|
||||||
|
fn enqueue_approval_rebuild(
|
||||||
|
coord: &Arc<Coordinator>,
|
||||||
|
agent: &str,
|
||||||
|
approval_id: i64,
|
||||||
|
reason: String,
|
||||||
|
) {
|
||||||
|
coord
|
||||||
|
.rebuild_queue
|
||||||
|
.enqueue_full(crate::rebuild_queue::FullEnqueue {
|
||||||
|
kind: crate::rebuild_queue::QueueKind::Rebuild,
|
||||||
|
agent: agent.to_owned(),
|
||||||
|
source: crate::rebuild_queue::QueueSource::Approval,
|
||||||
|
reason,
|
||||||
|
parent_id: None,
|
||||||
|
inputs: Vec::new(),
|
||||||
|
approval_id: Some(approval_id),
|
||||||
|
perm_payload: None,
|
||||||
|
depends_on: Vec::new(),
|
||||||
|
});
|
||||||
|
coord.emit_rebuild_queue_snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
/// Worker entry point for `ApprovalKind::ApplyCommit` queue entries.
|
/// Worker entry point for `ApprovalKind::ApplyCommit` queue entries.
|
||||||
/// Re-fetches the approval row, runs the commit pipeline, and fires
|
/// Re-fetches the approval row, runs the commit pipeline, and fires
|
||||||
/// `ApprovalResolved` + the lifecycle event (`Rebuilt` / `Spawned`
|
/// `ApprovalResolved` + the lifecycle event (`Rebuilt` / `Spawned`
|
||||||
|
|
@ -446,22 +456,19 @@ fn finish_approval(
|
||||||
sha: approval.fetched_sha.clone(),
|
sha: approval.fetched_sha.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ApprovalKind::ApplyCommit => coord.notify_manager(&HelperEvent::Rebuilt {
|
// MergeConfigPr ends in a container rebuild just like a
|
||||||
agent: approval.agent.clone(),
|
// non-first-spawn ApplyCommit, so both surface the same Rebuilt
|
||||||
ok,
|
// lifecycle event. (MergeConfigPr is never a first spawn — the
|
||||||
note,
|
// agent already exists — so it never hits the Spawned arm above.)
|
||||||
sha: approval.fetched_sha.clone(),
|
ApprovalKind::ApplyCommit | ApprovalKind::MergeConfigPr => {
|
||||||
tag: terminal_tag,
|
coord.notify_manager(&HelperEvent::Rebuilt {
|
||||||
}),
|
agent: approval.agent.clone(),
|
||||||
// MergeConfigPr ends in a container rebuild like ApplyCommit, so
|
ok,
|
||||||
// surface the same Rebuilt lifecycle event.
|
note,
|
||||||
ApprovalKind::MergeConfigPr => coord.notify_manager(&HelperEvent::Rebuilt {
|
sha: approval.fetched_sha.clone(),
|
||||||
agent: approval.agent.clone(),
|
tag: terminal_tag,
|
||||||
ok,
|
});
|
||||||
note,
|
}
|
||||||
sha: approval.fetched_sha.clone(),
|
|
||||||
tag: terminal_tag,
|
|
||||||
}),
|
|
||||||
// UpdateMetaInputs / SchedulePrompt: ApprovalResolved already
|
// UpdateMetaInputs / SchedulePrompt: ApprovalResolved already
|
||||||
// carries the result. No separate lifecycle event needed.
|
// carries the result. No separate lifecycle event needed.
|
||||||
ApprovalKind::UpdateMetaInputs | ApprovalKind::SchedulePrompt => {}
|
ApprovalKind::UpdateMetaInputs | ApprovalKind::SchedulePrompt => {}
|
||||||
|
|
@ -478,12 +485,8 @@ fn finish_approval(
|
||||||
/// (`deployed/<id>`) or annotate `failed/<id>` with the build error
|
/// (`deployed/<id>`) or annotate `failed/<id>` with the build error
|
||||||
/// and reset the working tree back to the last known-good main. main
|
/// and reset the working tree back to the last known-good main. main
|
||||||
/// never advances on a failed build, so a crash-and-recover doesn't
|
/// never advances on a failed build, so a crash-and-recover doesn't
|
||||||
/// leave the agent pointing at a tree it can't evaluate.
|
/// leave the agent pointing at a tree it can't evaluate. The shared
|
||||||
#[allow(
|
/// ff/deploy/rebuild/finalize tail lives in `deploy_applied_target`.
|
||||||
clippy::too_many_lines,
|
|
||||||
reason = "one sequential build/tag/notify pipeline; splitting the steps \
|
|
||||||
across helpers would obscure the linear flow without shrinking it"
|
|
||||||
)]
|
|
||||||
async fn run_apply_commit(
|
async fn run_apply_commit(
|
||||||
coord: &Arc<Coordinator>,
|
coord: &Arc<Coordinator>,
|
||||||
approval: &hive_sh4re::Approval,
|
approval: &hive_sh4re::Approval,
|
||||||
|
|
@ -574,28 +577,76 @@ async fn run_apply_commit(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fast-forward applied/main to the proposal, run the meta deploy +
|
||||||
|
// container rebuild, and finalize/roll-back — the tail shared with the
|
||||||
|
// PR-merge flow. ApplyCommit's target == finalize sha source is
|
||||||
|
// `fetched_sha` (or the proposal ref when unset), matching the prior
|
||||||
|
// inline behavior exactly.
|
||||||
|
let (result, tag) = deploy_applied_target(
|
||||||
|
coord,
|
||||||
|
&approval.agent,
|
||||||
|
agent_dir,
|
||||||
|
applied_dir,
|
||||||
|
&proposal_ref,
|
||||||
|
approval.fetched_sha.as_deref().unwrap_or(&proposal_ref),
|
||||||
|
id,
|
||||||
|
&prev_main_sha,
|
||||||
|
is_first_spawn,
|
||||||
|
queue_entry_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
(result, tag, is_first_spawn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared deploy tail for config-applying approvals (`ApplyCommit` + the
|
||||||
|
/// PR-merge flow). Fast-forwards `applied/main` to `target_ref`, syncs the
|
||||||
|
/// working tree, runs the meta two-phase deploy + container rebuild, and
|
||||||
|
/// plants the `deployed/<tag_base>` / `failed/<tag_base>` bookkeeping tags.
|
||||||
|
/// On build failure it rolls `applied/main` back to `prev_main_sha` and aborts
|
||||||
|
/// the staged meta lock so the agent stays on its last-good tree. Returns the
|
||||||
|
/// build result + the terminal tag name.
|
||||||
|
///
|
||||||
|
/// Caller-specific bits stay OUT of here: the source fetch (proposal tag vs
|
||||||
|
/// forge fetch), the `approved/building` tags, `verify_commit`, and any forge
|
||||||
|
/// ff-push / mark-merged. `is_first_spawn` gates the one-time meta
|
||||||
|
/// `sync_agents` step (only `ApplyCommit`'s first spawn passes `true`;
|
||||||
|
/// the PR-merge flow always passes `false` — the agent already exists).
|
||||||
|
/// `finalize_sha` is the sha recorded by `meta::finalize_deploy`; `target_ref`
|
||||||
|
/// is what `applied/main` fast-forwards to (a proposal ref or a commit sha).
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "one sequential ff/deploy/rebuild/finalize pipeline shared by both \
|
||||||
|
config-apply callers; splitting it would obscure the linear flow"
|
||||||
|
)]
|
||||||
|
async fn deploy_applied_target(
|
||||||
|
coord: &Arc<Coordinator>,
|
||||||
|
agent: &str,
|
||||||
|
agent_dir: &std::path::Path,
|
||||||
|
applied_dir: &std::path::Path,
|
||||||
|
target_ref: &str,
|
||||||
|
finalize_sha: &str,
|
||||||
|
tag_base: i64,
|
||||||
|
prev_main_sha: &str,
|
||||||
|
is_first_spawn: bool,
|
||||||
|
queue_entry_id: Option<u64>,
|
||||||
|
) -> (Result<()>, Option<String>) {
|
||||||
|
let id = tag_base;
|
||||||
|
|
||||||
coord.set_queue_step(queue_entry_id, "fast-forward applied/main");
|
coord.set_queue_step(queue_entry_id, "fast-forward applied/main");
|
||||||
// Fast-forward applied/main to proposal/<id> + sync the working
|
// Fast-forward applied/main to target_ref + sync the working tree.
|
||||||
// tree. Meta input pins `?ref=main`, so this is what makes nix
|
// Meta input pins `?ref=main`, so this is what makes nix re-lock to
|
||||||
// re-lock to the proposal commit on the prepare_deploy step
|
// the target commit on the prepare_deploy step below. On build
|
||||||
// below. On build failure we roll main back to prev_main_sha so
|
// failure we roll main back to prev_main_sha so a crash leaves the
|
||||||
// a crash leaves the agent on its last-good tree.
|
// agent on its last-good tree.
|
||||||
if let Err(e) = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &proposal_ref).await {
|
if let Err(e) = lifecycle::git_update_ref(applied_dir, "refs/heads/main", target_ref).await {
|
||||||
return (
|
return (Err(anyhow::anyhow!("ff main to {target_ref}: {e:#}")), None);
|
||||||
Err(anyhow::anyhow!("ff main to {proposal_ref}: {e:#}")),
|
|
||||||
None,
|
|
||||||
is_first_spawn,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if let Err(e) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await {
|
if let Err(e) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await {
|
||||||
// main is ahead; working tree didn't sync. Roll main back to
|
// main is ahead; working tree didn't sync. Roll main back to
|
||||||
// keep the two consistent before bailing.
|
// keep the two consistent before bailing.
|
||||||
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
|
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await;
|
||||||
return (
|
return (Err(anyhow::anyhow!("read-tree to main: {e:#}")), None);
|
||||||
Err(anyhow::anyhow!("read-tree to main: {e:#}")),
|
|
||||||
None,
|
|
||||||
is_first_spawn,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// First spawn: sync_agents must add this agent to the meta flake
|
// First spawn: sync_agents must add this agent to the meta flake
|
||||||
|
|
@ -603,40 +654,34 @@ async fn run_apply_commit(
|
||||||
// exist yet if this is the agent's first deploy).
|
// exist yet if this is the agent's first deploy).
|
||||||
if is_first_spawn {
|
if is_first_spawn {
|
||||||
coord.set_queue_step(queue_entry_id, "meta sync_agents (first spawn)");
|
coord.set_queue_step(queue_entry_id, "meta sync_agents (first spawn)");
|
||||||
let agents = match lifecycle::agents_for_meta_listing_with(&approval.agent).await {
|
let agents = match lifecycle::agents_for_meta_listing_with(agent).await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ =
|
let _ =
|
||||||
lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
|
lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await;
|
||||||
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
||||||
return (
|
return (
|
||||||
Err(anyhow::anyhow!("agents_for_meta_listing_with: {e:#}")),
|
Err(anyhow::anyhow!("agents_for_meta_listing_with: {e:#}")),
|
||||||
None,
|
None,
|
||||||
is_first_spawn,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Err(e) = crate::meta::sync_agents(&coord.hive_env(), &agents).await {
|
if let Err(e) = crate::meta::sync_agents(&coord.hive_env(), &agents).await {
|
||||||
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
|
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await;
|
||||||
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
||||||
return (
|
return (
|
||||||
Err(anyhow::anyhow!("meta sync_agents for first spawn: {e:#}")),
|
Err(anyhow::anyhow!("meta sync_agents for first spawn: {e:#}")),
|
||||||
None,
|
None,
|
||||||
is_first_spawn,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coord.set_queue_step(queue_entry_id, "meta prepare_deploy");
|
coord.set_queue_step(queue_entry_id, "meta prepare_deploy");
|
||||||
// Phase 1 of the meta two-phase deploy: relock without committing.
|
// Phase 1 of the meta two-phase deploy: relock without committing.
|
||||||
if let Err(e) = crate::meta::prepare_deploy(&approval.agent).await {
|
if let Err(e) = crate::meta::prepare_deploy(agent).await {
|
||||||
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await;
|
let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await;
|
||||||
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await;
|
||||||
return (
|
return (Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")), None);
|
||||||
Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")),
|
|
||||||
None,
|
|
||||||
is_first_spawn,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container-level rebuild (or first-time create) against meta#<name>.
|
// Container-level rebuild (or first-time create) against meta#<name>.
|
||||||
|
|
@ -644,9 +689,9 @@ async fn run_apply_commit(
|
||||||
// the dashboard reflects actual phase progress rather than a static
|
// the dashboard reflects actual phase progress rather than a static
|
||||||
// "nixos-container update" label for the whole multi-minute window.
|
// "nixos-container update" label for the whole multi-minute window.
|
||||||
let hive = coord.hive_env();
|
let hive = coord.hive_env();
|
||||||
let paths = Coordinator::agent_paths(&approval.agent, agent_dir.to_path_buf());
|
let paths = Coordinator::agent_paths(agent, agent_dir.to_path_buf());
|
||||||
let build_result = lifecycle::rebuild_no_meta(
|
let build_result = lifecycle::rebuild_no_meta(
|
||||||
&approval.agent,
|
agent,
|
||||||
&hive,
|
&hive,
|
||||||
&paths,
|
&paths,
|
||||||
&|step| coord.set_queue_step(queue_entry_id, step),
|
&|step| coord.set_queue_step(queue_entry_id, step),
|
||||||
|
|
@ -664,54 +709,47 @@ async fn run_apply_commit(
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
coord.set_queue_step(queue_entry_id, "finalize deploy");
|
coord.set_queue_step(queue_entry_id, "finalize deploy");
|
||||||
let tag = format!("deployed/{id}");
|
let tag = format!("deployed/{id}");
|
||||||
if let Err(e) = lifecycle::git_tag(applied_dir, &tag, &proposal_ref).await {
|
if let Err(e) = lifecycle::git_tag(applied_dir, &tag, target_ref).await {
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?e, "plant deployed tag failed");
|
tracing::warn!(%agent, %id, error = ?e, "plant deployed tag failed");
|
||||||
}
|
}
|
||||||
if let Err(e) = crate::meta::finalize_deploy(
|
if let Err(e) = crate::meta::finalize_deploy(agent, finalize_sha, &tag).await {
|
||||||
&approval.agent,
|
|
||||||
approval.fetched_sha.as_deref().unwrap_or(&proposal_ref),
|
|
||||||
&tag,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
// The build itself succeeded — meta lock landed but
|
// The build itself succeeded — meta lock landed but
|
||||||
// couldn't be committed. Surface as a soft warn so the
|
// couldn't be committed. Surface as a soft warn so the
|
||||||
// operator can git-commit by hand if they care.
|
// operator can git-commit by hand if they care.
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?e, "meta finalize_deploy failed");
|
tracing::warn!(%agent, %id, error = ?e, "meta finalize_deploy failed");
|
||||||
}
|
}
|
||||||
// Wake the agent on its next turn so claude sees the
|
// Wake the agent on its next turn so claude sees the
|
||||||
// config change took effect. Same hint pattern as
|
// config change took effect. Same hint pattern as
|
||||||
// auto_update::rebuild_agent — manager approved a
|
// auto_update::rebuild_agent — manager approved a
|
||||||
// proposal, agent picks up where it left off with the
|
// proposal, agent picks up where it left off with the
|
||||||
// new env / packages.
|
// new env / packages.
|
||||||
coord.kick_agent(&approval.agent, "config update applied");
|
coord.kick_agent(agent, "config update applied");
|
||||||
(Ok(()), Some(tag), is_first_spawn)
|
(Ok(()), Some(tag))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let tag = format!("failed/{id}");
|
let tag = format!("failed/{id}");
|
||||||
let body = format!("{e:#}");
|
let body = format!("{e:#}");
|
||||||
if let Err(te) =
|
if let Err(te) =
|
||||||
lifecycle::git_tag_annotated(applied_dir, &tag, &proposal_ref, &body).await
|
lifecycle::git_tag_annotated(applied_dir, &tag, target_ref, &body).await
|
||||||
{
|
{
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?te, "annotate failed tag failed");
|
tracing::warn!(%agent, %id, error = ?te, "annotate failed tag failed");
|
||||||
}
|
}
|
||||||
// Roll main back to last known-good so the on-disk state
|
// Roll main back to last known-good so the on-disk state
|
||||||
// matches what nixos-container last successfully built.
|
// matches what nixos-container last successfully built.
|
||||||
if let Err(re) =
|
if let Err(re) =
|
||||||
lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await
|
lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await
|
||||||
{
|
{
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?re, "main rollback failed");
|
tracing::warn!(%agent, %id, error = ?re, "main rollback failed");
|
||||||
}
|
}
|
||||||
if let Err(re) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await {
|
if let Err(re) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await {
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?re, "rollback read-tree failed");
|
tracing::warn!(%agent, %id, error = ?re, "rollback read-tree failed");
|
||||||
}
|
}
|
||||||
// Drop the staged meta lock change so the deploy log
|
// Drop the staged meta lock change so the deploy log
|
||||||
// only ever shows successes.
|
// only ever shows successes.
|
||||||
if let Err(ae) = crate::meta::abort_deploy().await {
|
if let Err(ae) = crate::meta::abort_deploy().await {
|
||||||
tracing::warn!(agent = %approval.agent, %id, error = ?ae, "meta abort_deploy failed");
|
tracing::warn!(%agent, %id, error = ?ae, "meta abort_deploy failed");
|
||||||
}
|
}
|
||||||
let _ = coord;
|
(Err(e), Some(tag))
|
||||||
(Err(e), Some(tag), is_first_spawn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue