Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion crates/moon-core/src/feed/live/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,9 @@ fn rebuild_sync(
/// `SetMarket` queue entries are wake/order markers; their payloads can be stale behind an action
/// backlog, so the shared authoritative snapshot is adopted before and after the batch. The return
/// value tells the live loop whether it disconnected, emptied the queue, or must poll again without
/// blocking. `core_config_events` collects any shared-config edit lifecycle events a queue-drain
/// blocking. `problems_relist` is raised by an operator-requested diagnostics re-read and consumed
/// by the caller's own publish block, so a burst of presses costs one snapshot read rather than
/// one per press. `core_config_events` collects any shared-config edit lifecycle events a queue-drain
/// send produced; the caller sends them as `FeedMsg::CoreConfigEdit` and stamps their clock, the
/// same as the events an event-batch-driven `SharedConfigSequence::drive` produces.
pub(super) fn drain_commands(
Expand All @@ -706,6 +708,7 @@ pub(super) fn drain_commands(
market_role: &mut MarketRoleState,
force_market_sample: &mut bool,
orders_mutated: &mut bool,
problems_relist: &mut bool,
local_strat_edits: &mut LocalStratEdits,
strategy_placements: &mut StrategyPlacementGuard,
client_settings_sequence: &mut ClientSettingsSequence,
Expand Down Expand Up @@ -1491,6 +1494,16 @@ pub(super) fn drain_commands(
),
}
}
Ok(CoreCmd::RefreshProblems) => {
// Deliberately sends NOTHING: see `CoreCmd::RefreshProblems` for why no request
// exists. The flag makes the live loop republish from the retained snapshot once
// this batch is drained.
*problems_relist = true;
log::debug!(
"core {} problems re-read requested",
crate::feed::core_label(server.id)
);
}
Ok(CoreCmd::SetAutoDetect(on)) => {
// Passive mode off/on; the new value reaches the store via RuntimeStateUpdated,
// the same command that carries `is_started`.
Expand Down
30 changes: 24 additions & 6 deletions crates/moon-core/src/feed/live/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,30 @@ pub(super) fn settings_event_snapshot<T>(
matched: impl Fn(&Event) -> bool,
extract: impl FnOnce(Arc<moonproto::MoonStateSnapshot>) -> Option<T>,
) -> Option<T> {
events
.iter()
.any(matched)
.then(|| client.snapshot())
.flatten()
.and_then(extract)
snapshot_when(events.iter().any(matched), client, extract)
}

/// Read the retained snapshot when `gate` says to, and project it.
///
/// The half of [`settings_event_snapshot`] that is not about events, split out for the one caller
/// whose gate is not an event at all: an operator asking for a republish of something the wire will
/// never announce again. Written as a helper rather than copied inline so the snapshot/flatten/
/// project chain has one spelling — nine sibling publish blocks in the live loop reach it through
/// [`settings_event_snapshot`], and a tenth on a hand-rolled copy is how the two drift.
///
/// Args:
/// gate: Whether there is any reason to read the snapshot at all.
/// client: Connected moonproto client holding the retained state.
/// extract: Projects the retained snapshot into the terminal's own type.
///
/// Returns:
/// The projection, or `None` when the gate is shut or no snapshot exists yet.
pub(super) fn snapshot_when<T>(
gate: bool,
client: &MoonClient,
extract: impl FnOnce(Arc<moonproto::MoonStateSnapshot>) -> Option<T>,
) -> Option<T> {
gate.then(|| client.snapshot()).flatten().and_then(extract)
}

/// Convert protocol-v4 `KernelHealth` into terminal telemetry and stamp its
Expand Down
27 changes: 17 additions & 10 deletions crates/moon-core/src/feed/live/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@ pub(super) fn run(
// `SetMarket` contains complete desired state; the other coordinator commands are deltas
// or actions. A closed channel means the coordinator has exited, so disconnect.
let mut orders_mutated = false;
let mut problems_relist = false;
let mut core_config_events = Vec::new();
let command_drain = drain_commands(
cmd_rx,
Expand All @@ -677,6 +678,7 @@ pub(super) fn run(
market_role,
&mut force_market_sample,
&mut orders_mutated,
&mut problems_relist,
&mut local_strat_edits,
&mut strategy_placements,
client_settings_sequence,
Expand Down Expand Up @@ -1542,22 +1544,27 @@ pub(super) fn run(
// Nothing is ever requested here: the core sends its list unprompted on connection, and a
// core too old for the extension simply never sends one. That is the whole compatibility
// story — see `CoreProblems::supported`.
let problems = settings_event_snapshot(
&events,
&client,
|ev| {
//
// An operator-requested re-read is a SECOND reason to read the same retained state, so it
// joins the event gate rather than branching beside it. There is no event to wait for: the
// wire cannot be asked for a list, and moonproto empties this state without one when a
// ServerToken changes — see `CoreCmd::RefreshProblems`. Either reason reads the same
// snapshot through the same projection, so one gate is all the difference between them.
let want_problems = problems_relist
|| events.iter().any(|ev| {
matches!(
ev,
&Event::Settings(
SettingsEvent::ProblemsUpdated | SettingsEvent::ProblemConfirmed { .. }
)
)
},
// Wrapped in `Some` because an EMPTY list is a real answer — the core looked and found
// nothing — and the helper would otherwise drop it as "nothing to report", which is the
// one reading this feature must never produce.
|state| Some(convert::problems_from_proto(&state.settings().problems)),
);
});
// Wrapped in `Some` because an EMPTY list is a real answer — the core looked and found
// nothing — and the helper would otherwise drop it as "nothing to report", which is the one
// reading this feature must never produce.
let problems = convert::snapshot_when(want_problems, &client, |state| {
Some(convert::problems_from_proto(&state.settings().problems))
});
if let Some(problems) = problems {
if tx.send(FeedMsg::Problems(problems)).is_err() {
break;
Expand Down
25 changes: 25 additions & 0 deletions crates/moon-core/src/feed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,31 @@ pub enum CoreCmd {
/// The UI must therefore confirm before sending, and must NOT clear its local rows
/// optimistically: the answer is the core's next full list.
ClearProblems,
/// Re-publish one core's diagnostics from the library's retained snapshot, without an event.
///
/// NOT a wire request, because there is no such thing. The protocol has no way to ask a core
/// for its diagnostics list: the core pushes one on connection and again whenever its own
/// detectors change it, moonproto's `docs/problems.md` states outright that "there is no
/// periodic diagnostic-list refresh", and the only client command that produces a fresh list is
/// the destructive [`CoreCmd::ClearProblems`].
///
/// It is nonetheless a real repair rather than a decorative button, and for one specific reason.
/// moonproto mutates its retained problems through exactly three paths, and only two of them
/// raise an event this feed can follow. The third is the hard-session reset: on a ServerToken or
/// peer-app-token CHANGE — a hard reconnect, or the MoonBot process restarting behind the same
/// endpoint — `events/active.rs` assigns `settings.problems = Default::default()` and publishes
/// NOTHING. Our own store is not rebuilt then either, because the terminal never respawned the
/// feed thread, so it keeps serving the findings of a core instance that no longer exists, with
/// `supported` still true. The restarted core normally pushes a list of its own and repairs
/// that within seconds; a core too old for the extension never does, and the stale rows then
/// stand forever.
///
/// So this re-reads what moonproto holds NOW and publishes it. Usually that equals what the
/// store already has and the store drops it. After a silent reset it does not, and the honest
/// answer — "this core has not told this connection anything" — replaces the previous
/// instance's findings. That replacement is the point, not a hazard: it is the same rule
/// `CoreData::begin_connection_attempt` applies when the terminal itself reconnects a core.
RefreshProblems,
}

/// Complete market-role assignment published independently of the bounded command backlog.
Expand Down
45 changes: 45 additions & 0 deletions crates/moon-core/src/session/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,51 @@ impl SessionManager {
self.send_core_cmd(core, CoreCmd::ClearProblems, "clear problems")
}

/// Clear the diagnostics of a whole scope — every core a panel currently covers.
///
/// One command per core, like [`Self::set_auto_detect_many`], because that is what the protocol
/// offers. Far more destructive than the single-core form by exactly the number of cores given,
/// so a caller must state that number to the operator before asking.
///
/// Args:
/// cores: Cores whose diagnostics are dropped.
///
/// Returns:
/// The cores whose command channel accepted the intent, in the order given.
pub fn clear_core_problems_many(&self, cores: &[CoreId]) -> Vec<CoreId> {
self.send_many(cores, "clear problems", |core| {
self.clear_core_problems(core)
})
}

/// Re-read one core's diagnostics from the library's retained snapshot.
///
/// Nothing is sent to the core and nothing on the core changes: the protocol has no request for
/// a diagnostics list at all, so this only re-publishes what already arrived. See
/// [`CoreCmd::RefreshProblems`] for the whole reasoning.
///
/// Args:
/// core: Core whose retained list is re-published.
///
/// Returns:
/// Whether the command reached the core's channel.
pub fn refresh_core_problems(&self, core: CoreId) -> Result<()> {
self.send_core_cmd(core, CoreCmd::RefreshProblems, "refresh problems")
}

/// Re-read the diagnostics of a whole scope. See [`Self::refresh_core_problems`].
///
/// Args:
/// cores: Cores whose retained lists are re-published.
///
/// Returns:
/// The cores whose command channel accepted the intent, in the order given.
pub fn refresh_core_problems_many(&self, cores: &[CoreId]) -> Vec<CoreId> {
self.send_many(cores, "refresh problems", |core| {
self.refresh_core_problems(core)
})
}

/// Turn one core's AutoDetect on or off — Moonbot's passive mode, inverted.
///
/// The command is an intent: the core answers with a new runtime state, which is what any
Expand Down
Loading