Skip to content

fix(plugin): bound proposal queue deadlines - #1180

Open
i386 wants to merge 4 commits into
mainfrom
agent/fix-plugin-proposal-deadlines
Open

fix(plugin): bound proposal queue deadlines#1180
i386 wants to merge 4 commits into
mainfrom
agent/fix-plugin-proposal-deadlines

Conversation

@i386

@i386 i386 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • schedule native plugin proposals ahead of queued passive callbacks while preserving preceding lifecycle delivery
  • abstain before dispatch when the proposal wall-clock deadline has expired, avoiding stale plugin work
  • emit privacy-safe queue/callback timing and deadline outcome telemetry

Root cause

The native serving-plugin driver used a single FIFO for lifecycle callbacks, proposal queries, and passive proposal outcomes. A callback ahead of a proposal could consume its deadline before the plugin was reached.

Validation

  • cargo test -p mesh-native-serving-plugin-host --lib
  • cargo test -p skippy-server --lib
  • cargo test -p mesh-llm-host-runtime runtime::survey::tests --lib
  • cargo check -p mesh-llm
  • cargo clippy -p mesh-native-serving-plugin-host -p skippy-server --all-targets -- -D warnings
  • just build

cargo clippy -p mesh-llm --all-targets -- -D warnings remains blocked by pre-existing unfulfilled dead_code lint expectations in mesh-llm-host-runtime.

Closes #1178

Summary by CodeRabbit

  • New Features

    • Added privacy-safe telemetry for proposal queue wait time, callback duration, and source outcomes.
    • Exposed proposal telemetry through the public frontend interface.
    • Improved queue recovery, ordering, and isolation of background callbacks.
  • Bug Fixes

    • Enforced proposal deadlines before and after processing, preventing expired or late candidates from being forwarded.
  • Documentation

    • Documented the new proposal source telemetry attributes.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a8ace73d-5c50-4d40-9f49-bcf761090d90

📥 Commits

Reviewing files that changed from the base of the PR and between 2da0d53 and 35f2690.

📒 Files selected for processing (1)
  • crates/skippy-server/src/frontend/linear_proposal.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/skippy-server/src/frontend/linear_proposal.rs

📝 Walkthrough

Walkthrough

The PR replaces native proposal dispatch with deadline-aware active and passive workers. It adds structured source telemetry, propagates it through Skippy query outcomes, emits privacy-safe attributes, and tests deadline recovery and late candidates.

Changes

Proposal deadline and telemetry flow

Layer / File(s) Summary
Source telemetry contract
crates/skippy-server/src/frontend/linear_proposal.rs, crates/skippy-server/src/frontend.rs, crates/skippy-server/src/lib.rs, crates/skippy-server/src/serving_hooks.rs
LinearProposalIngress::propose now returns LinearProposalSourceResponse. Source outcomes and timing telemetry propagate through query results and public re-exports.
Deadline-aware native dispatch
crates/mesh-native-serving-plugin-host/src/lib.rs
Native dispatch uses bounded active and passive queues. Proposal submission and callback execution enforce deadlines and return queue, callback, and outcome telemetry.
Native callback behavior and tests
crates/mesh-native-serving-plugin-host/src/lib.rs
Fake plugins support delayed callbacks and candidates. Tests cover deadline recovery, passive callback isolation, late candidates, and stopped queues.
Telemetry emission and allowlist
crates/skippy-server/src/frontend/local_generation/linear_decode.rs, docs/plugins/telemetry.md, crates/mesh-llm-host-runtime/src/runtime/survey.rs
Linear decoding emits source telemetry. Documentation and survey tests include queue wait, callback duration, and bounded outcome attributes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LinearProposalIngress
  participant PluginDriver
  participant ActiveWorker
  participant PassiveWorker
  participant LinearProposalQuery
  LinearProposalIngress->>PluginDriver: submit proposal query with deadline
  PluginDriver->>ActiveWorker: enqueue proposal command
  ActiveWorker->>PluginDriver: return proposal and source telemetry
  PluginDriver->>PassiveWorker: enqueue report or discard callback
  PluginDriver->>LinearProposalIngress: return LinearProposalSourceResponse
  LinearProposalIngress->>LinearProposalQuery: preserve source telemetry
Loading

Possibly related PRs

Suggested reviewers: ndizazzo, michaelneale

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: enforcing proposal queue deadlines in the plugin.
Linked Issues check ✅ Passed The changes address [#1178] by enforcing deadlines, preserving causal ordering, isolating passive work, and adding required telemetry.
Out of Scope Changes check ✅ Passed The implementation, API updates, tests, and telemetry documentation directly support the linked issue objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/fix-plugin-proposal-deadlines

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

This pull request is currently a draft. Reviews will not take place until the PR is marked as ready for review.

@i386
i386 marked this pull request as ready for review August 5, 2026 03:39
@github-actions
github-actions Bot requested a review from ndizazzo August 5, 2026 03:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
crates/skippy-server/src/frontend/local_generation/linear_decode.rs (1)

187-199: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The source event is emitted on every decode step.

emit_linear_proposal_source_telemetry runs for every NoProposal, DeadlineExceeded, and Ready outcome. try_execute_linear_proposal runs once per decode step, so this emits one event per generated token per request. The comparable per-step proposal event at Lines 175-180 uses emit_debug and is gated by state.emit_token_debug. The existing stage.openai_linear_proposal_late event uses emit, but it fires only on the rare late path.

Consider gating the routine outcomes behind emit_debug, and keeping unconditional emit for the deadline and queue-pressure outcomes only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/skippy-server/src/frontend/local_generation/linear_decode.rs` around
lines 187 - 199, Update emit_linear_proposal_source_telemetry so routine
NoProposal and Ready outcomes use emit_debug gated by state.emit_token_debug,
while DeadlineExceeded and queue-pressure outcomes continue using unconditional
emit. Preserve the existing telemetry attributes and event name, and adjust
try_execute_linear_proposal callers as needed to distinguish these outcome
classes.
crates/mesh-native-serving-plugin-host/src/lib.rs (2)

604-617: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Passive commands can be deferred without bound.

pop_next returns a queued proposal ahead of every Report and Discard command. If proposals arrive continuously from several decode threads, the passive commands are never selected. They accumulate until the queue reaches PLUGIN_COMMAND_CAPACITY. After that, try_enqueue reports Full, so proposals return QueueFull and lifecycle delivery fails.

Bound the deferral. One option is to select the oldest passive command when its enqueued_at age exceeds a threshold. A second option is to select a passive command whenever the queue occupancy exceeds a watermark.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mesh-native-serving-plugin-host/src/lib.rs` around lines 604 - 617,
Update pop_next so passive commands (Report/Discard) cannot be deferred
indefinitely behind proposals: select an eligible passive command once its
enqueued_at age exceeds the chosen threshold or queue occupancy reaches a
defined watermark, while preserving lifecycle-command priority and normal
proposal ordering otherwise.

798-808: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

A plugin error is reported as Abstained.

The Err(_) arm maps a failed plugin callback to LinearProposalSourceOutcome::Abstained. Telemetry then cannot separate a deliberate abstention from a callback failure. LinearProposalSourceOutcome is #[non_exhaustive], so adding a dedicated variant is additive and does not break older consumers.

Note that this telemetry value is also unreachable today for the error case: NativeProposalIngress::propose returns Err at Line 489, and query_linear_proposal returns early without calling take_proposal_telemetry. The stored value then leaks into the next query. That leak is covered by the comment on the trait contract in crates/skippy-server/src/frontend/linear_proposal.rs.

Add a SourceError variant, or document that Abstained also covers callback failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mesh-native-serving-plugin-host/src/lib.rs` around lines 798 - 808,
Distinguish plugin callback failures from deliberate abstentions in the outcome
mapping around the result match: add the additive non-exhaustive
LinearProposalSourceOutcome::SourceError variant and map Err(_) to it. Ensure
the error path still records or consumes telemetry before
NativeProposalIngress::propose/query_linear_proposal returns, preventing the
failed result from leaking into the next query.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/mesh-native-serving-plugin-host/src/lib.rs`:
- Around line 559-586: Add a shared stopped-state flag to PluginCommandQueue,
have plugin_worker set it before normal return and via a Drop guard during
unwinding, and update try_enqueue to reject commands once the worker has
stopped. Preserve the existing Full and Poisoned error handling while ensuring
enqueue reports the stopped-worker failure so lifecycle delivery does not
succeed without a running callback worker.

In `@crates/skippy-server/src/frontend/linear_proposal.rs`:
- Around line 290-299: Remove the calling-thread assumption from
take_proposal_telemetry by carrying telemetry with the individual proposal
result through propose into query_linear_proposal. Ensure failed propose calls
also return or clear telemetry so stale values cannot be consumed by a later
successful query, and update the method documentation to match the new
request-scoped behavior.

---

Nitpick comments:
In `@crates/mesh-native-serving-plugin-host/src/lib.rs`:
- Around line 604-617: Update pop_next so passive commands (Report/Discard)
cannot be deferred indefinitely behind proposals: select an eligible passive
command once its enqueued_at age exceeds the chosen threshold or queue occupancy
reaches a defined watermark, while preserving lifecycle-command priority and
normal proposal ordering otherwise.
- Around line 798-808: Distinguish plugin callback failures from deliberate
abstentions in the outcome mapping around the result match: add the additive
non-exhaustive LinearProposalSourceOutcome::SourceError variant and map Err(_)
to it. Ensure the error path still records or consumes telemetry before
NativeProposalIngress::propose/query_linear_proposal returns, preventing the
failed result from leaking into the next query.

In `@crates/skippy-server/src/frontend/local_generation/linear_decode.rs`:
- Around line 187-199: Update emit_linear_proposal_source_telemetry so routine
NoProposal and Ready outcomes use emit_debug gated by state.emit_token_debug,
while DeadlineExceeded and queue-pressure outcomes continue using unconditional
emit. Preserve the existing telemetry attributes and event name, and adjust
try_execute_linear_proposal callers as needed to distinguish these outcome
classes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd439b5b-e53f-453c-a7f7-9aa92320fd41

📥 Commits

Reviewing files that changed from the base of the PR and between 8d72d95 and 79b605c.

📒 Files selected for processing (6)
  • crates/mesh-llm-host-runtime/src/runtime/survey.rs
  • crates/mesh-native-serving-plugin-host/src/lib.rs
  • crates/skippy-server/src/frontend.rs
  • crates/skippy-server/src/frontend/linear_proposal.rs
  • crates/skippy-server/src/frontend/local_generation/linear_decode.rs
  • docs/plugins/telemetry.md

Comment thread crates/mesh-native-serving-plugin-host/src/lib.rs
Comment thread crates/skippy-server/src/frontend/linear_proposal.rs Outdated
@i386
i386 force-pushed the agent/fix-plugin-proposal-deadlines branch from c67f73e to 1ebeea1 Compare August 5, 2026 04:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/mesh-native-serving-plugin-host/src/lib.rs (1)

840-867: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use one completion timestamp for late-candidate handling.

Lines 840 and 843 read Instant::now() independently. If the first read is before the deadline and the second is after it, telemetry reports CandidateReturnedTooLate, but candidate_was_late remains false and Lines 859-867 forward the candidate. Capture one callback completion timestamp and derive both the outcome and proposal suppression from it.

Proposed fix
-                let candidate_was_late =
-                    matches!(&result, Ok(Some(_))) && Instant::now() >= query.deadline;
+                let callback_finished_at = Instant::now();
+                let candidate_was_late =
+                    matches!(&result, Ok(Some(_))) && callback_finished_at >= query.deadline;
                 let outcome = match &result {
-                    Ok(Some(_)) if Instant::now() >= query.deadline => {
+                    Ok(Some(_)) if candidate_was_late => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mesh-native-serving-plugin-host/src/lib.rs` around lines 840 - 867,
Capture a single callback completion timestamp before the late-candidate checks,
then derive both candidate_was_late and the outcome match from that shared
timestamp. Use candidate_was_late to preserve suppression of late proposals in
ProposalResponse and the existing discard behavior, ensuring telemetry
classification and proposal forwarding cannot disagree.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/mesh-native-serving-plugin-host/src/lib.rs`:
- Around line 525-529: Extract the deadline dispatch subsystem spanning
PluginCommandQueue and its associated queue, driver, worker, and shutdown logic
into a private, responsibility-named plugin_dispatch module. Move all related
symbols and implementation together, update internal references and imports, and
keep the module private without adding a crate-root re-export.
- Around line 780-790: Update PluginDriver::stop_worker to close each command
queue atomically with its Shutdown command, retrying or waiting when enqueue
fails because the queue is full. After receiving an acknowledged shutdown or
observing queue.stopped, always take and join the worker handle without checking
JoinHandle::is_finished, ensuring the worker fully exits before cleanup
continues.
- Around line 853-858: Ensure the late-candidate discard in the candidate
handling path is never lost when passive_queue.enqueue rejects it. Update the
logic around candidate_was_late and PluginCommand::Discard to reserve capacity
or retain and retry the terminal command until the passive worker accepts it,
and handle the enqueue result explicitly rather than ignoring it.

---

Outside diff comments:
In `@crates/mesh-native-serving-plugin-host/src/lib.rs`:
- Around line 840-867: Capture a single callback completion timestamp before the
late-candidate checks, then derive both candidate_was_late and the outcome match
from that shared timestamp. Use candidate_was_late to preserve suppression of
late proposals in ProposalResponse and the existing discard behavior, ensuring
telemetry classification and proposal forwarding cannot disagree.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b769829c-f6e7-4eb9-a2f3-fd44c5bbc834

📥 Commits

Reviewing files that changed from the base of the PR and between 79b605c and c67f73e.

📒 Files selected for processing (7)
  • crates/mesh-native-serving-plugin-host/src/lib.rs
  • crates/skippy-server/src/frontend.rs
  • crates/skippy-server/src/frontend/linear_proposal.rs
  • crates/skippy-server/src/frontend/local_generation/linear_decode.rs
  • crates/skippy-server/src/lib.rs
  • crates/skippy-server/src/serving_hooks.rs
  • docs/plugins/telemetry.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/plugins/telemetry.md
  • crates/skippy-server/src/frontend.rs
  • crates/skippy-server/src/frontend/local_generation/linear_decode.rs

Comment on lines +525 to +529
struct PluginCommandQueue {
commands: Mutex<VecDeque<QueuedPluginCommand>>,
stopped: AtomicBool,
available: Condvar,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Extract the deadline dispatch subsystem.

Lines 525-925 add a separable queue, driver, worker, and shutdown subsystem to a file that now reaches line 1,613. Move this subsystem to a private, responsibility-named module such as plugin_dispatch. Do not add a crate-root re-export.

As per coding guidelines, “When modifying a Rust source file over 1,000 lines, extract any separable responsibility into a semantically named module.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mesh-native-serving-plugin-host/src/lib.rs` around lines 525 - 529,
Extract the deadline dispatch subsystem spanning PluginCommandQueue and its
associated queue, driver, worker, and shutdown logic into a private,
responsibility-named plugin_dispatch module. Move all related symbols and
implementation together, update internal references and imports, and keep the
module private without adding a crate-root re-export.

Source: Coding guidelines

Comment on lines +780 to 790
fn stop_worker(&self, queue: &Arc<PluginCommandQueue>, worker: &Mutex<Option<JoinHandle<()>>>) {
let (reply, response) = sync_channel(1);
let queued = self.sender.try_send(PluginCommand::Shutdown(reply)).is_ok();
if queued
&& response.recv_timeout(CLEAN_SHUTDOWN_TIMEOUT).is_ok()
&& let Ok(worker) = self.worker.get_mut()
let clean_shutdown = queue.enqueue(PluginCommand::Shutdown(reply)).is_ok()
&& response.recv_timeout(CLEAN_SHUTDOWN_TIMEOUT).is_ok();
if (clean_shutdown || queue.stopped.load(Ordering::Acquire))
&& let Ok(mut worker) = worker.lock()
&& worker.as_ref().is_some_and(JoinHandle::is_finished)
&& let Some(worker) = worker.take()
{
let _ = worker.join();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make worker shutdown and joining reliable.

A full queue makes queue.enqueue(Shutdown) fail, so that worker never receives a terminal command. After a successful shutdown reply, plugin_worker sends the reply before it breaks, so JoinHandle::is_finished() can still be false and this code also skips join. Either path leaves a worker holding Arc<ActivePlugin>, causes Arc::get_mut at Line 771 to fail, and skips the native plugin shutdown callback.

Close each queue atomically with its shutdown command, retry or wait when capacity is full, and join an acknowledged or stopped worker without an is_finished gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mesh-native-serving-plugin-host/src/lib.rs` around lines 780 - 790,
Update PluginDriver::stop_worker to close each command queue atomically with its
Shutdown command, retrying or waiting when enqueue fails because the queue is
full. After receiving an acknowledged shutdown or observing queue.stopped,
always take and join the worker handle without checking JoinHandle::is_finished,
ensuring the worker fully exits before cleanup continues.

Comment on lines +853 to +858
if candidate_was_late && let Ok(Some(proposal)) = &result {
let _ = passive_queue.enqueue(PluginCommand::Discard(
proposal.decision_id.as_bytes().to_vec(),
LinearProposalDiscardReason::DeadlineExceeded,
));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not drop the terminal discard for a late candidate.

If the passive queue is full or stopped, passive_queue.enqueue(...) returns an error that this code discards. The late candidate is withheld from decode, so downstream cannot send a receipt or discard. The plugin then receives no terminal disposition for that decision ID.

Reserve capacity for deadline discards, or retain and retry terminal commands until the passive worker accepts them. Do not ignore this enqueue failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mesh-native-serving-plugin-host/src/lib.rs` around lines 853 - 858,
Ensure the late-candidate discard in the candidate handling path is never lost
when passive_queue.enqueue rejects it. Update the logic around
candidate_was_late and PluginCommand::Discard to reserve capacity or retain and
retry the terminal command until the passive worker accepts it, and handle the
enqueue result explicitly rather than ignoring it.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@i386
i386 enabled auto-merge (squash) August 5, 2026 05:38
@i386
i386 disabled auto-merge August 5, 2026 05:39
let candidate_was_late =
matches!(&result, Ok(Some(_))) && Instant::now() >= query.deadline;
let outcome = match &result {
Ok(Some(_)) if Instant::now() >= query.deadline => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double Instant::now() in late-candidate path can disagree with telemetry.

Lines 891 and 893 each read Instant::now() independently. Between the two reads, time advances — it is possible for line 891 to see pre-deadline (so candidate_was_late = false) while line 893 sees post-deadline and classifies telemetry as CandidateReturnedTooLate. The proposal then gets forwarded (!candidate_was_late at line 908) but telemetry reports it was late.

Fix: Capture a single timestamp once:

let callback_finished_at = Instant::now();
let candidate_was_late = matches!(&result, Ok(Some(_))) && callback_finished_at >= query.deadline;
let outcome = match &result {
    Ok(Some(_)) if candidate_was_late => LinearProposalSourceOutcome::CandidateReturnedTooLate,

Also note line 897 has a third Instant::now() for the Ok(None) deadline case — same window.

Err(_) => LinearProposalSourceOutcome::SourceError,
};
if candidate_was_late && let Ok(Some(proposal)) = &result {
let _ = passive_queue.enqueue(PluginCommand::Discard(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'Passive Discard for late candidate is silently dropped when passive queue full.

The result of passive_queue.enqueue(...) on line 904 is let _ = ..., so if the passive queue hits capacity, the discard for a late candidate is silently lost. The proposal itself isn't forwarded (good), but its activation-frame resources are never cleaned up — no downstream report that this slot should be discarded. This leaks state in the decode pipeline until eviction happens naturally.

Consider either:

  • Making Discard terminal commands un-droppable (e.g., reserve one capacity slot, or retry with backoff)
  • At minimum logging a warning when it fails'

self.driver.propose(query)
fn propose(&self, query: LinearProposalQuery) -> Result<LinearProposalSourceResponse> {
let response = self.driver.propose(query)?;
let proposal = response.proposal.unwrap_or_default();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'Error from native plugin callback is swallowed by .unwrap_or_default()

The response.proposal field in NativeProposalIngress::propose is a Result<Option<LinearProposal>, String>. When the plugin callback returns an error, this becomes Err(String) — and .unwrap_or_default() replaces it with Ok(None) (i.e., "abstained"). The original error message from the native plugin is lost.

The ingress adapter then converts the outer Result to a different type before returning up through skippy-server into linear_decode.rs where errors become fatal bailouts at line 168 (expect("linear proposal failed")). So this silently turns "plugin crashed" into "abstained", which means decode continues without ever knowing something went wrong.

Suggestion: Propagate the error or map it to a distinct telemetry outcome rather than defaulting to Ok(None).'

fn next(&self) -> QueuedPluginCommand {
let mut commands = self
.commands
.lock()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'PluginCommandQueue::next() blocks indefinitely on unbounded condvar wait

queue.next() at line 634 uses an infinite loop with no timeout on the condvar wait. If the queue is stopped (mark_stopped) but a thread already entered wait(), it will block forever — there's no corresponding notify_all() in the stop path that would wake threads currently blocked inside next().

Looking at line 628, mark_stopped does call self.available.notify_all() after setting the flag. But if a thread is between checking pop_next (which returns None) and calling .wait(), it will enter an infinite loop:

  1. Thread checks stopped → false
  2. Pop next → None
  3. Calls wait(blocked here while stop happens + notifies)
  4. Wait wakes, but no new command was added...

Actually mark_stopped does notify_all so threads wake up — they'll see the queue is empty and loop back to wait again indefinitely unless a Shutdown or more commands arrive before shutdown completes.

Consider: Add a timeout-based wait (wait_timeout) that re-checks stopped.load() each iteration, breaking when stopped instead of blocking forever.'

@ndizazzo

ndizazzo commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Left inline comments on four items not covered by CodeRabbit. Summary below (each linked to the threaded discussion):

Functional correctness:

🟠 Double Instant::now() race in late-candidate path comment

Lines 891, 893 (and 897) each read Instant::now() independently. Between reads the deadline can cross over — candidate_was_late says "not late" while telemetry classifies it as CandidateReturnedTooLate. The proposal gets forwarded but telemetry reports otherwise. One shared timestamp fixes this (~2 line change).

🟡 Passive Discard silently dropped when queue full comment

Line 904: let _ = passive_queue.enqueue(...) — if the passive queue is at capacity, late-candidate discards are silently dropped. The proposal isn't forwarded (good), but activation-frame resources leak until natural eviction.

🟡 Plugin errors swallowed as "abstained" comment

Line 531: response.proposal.unwrap_or_default() turns any plugin callback error into a silent "abstained" (Ok(None)). The original native-plugin error message is lost and decode continues without knowing something failed.

Robustness:

🟡 Unbounded condvar wait in next() comment

Line 634: The infinite loop with no timeout means if the queue stops while a thread is between checking pop_next and calling .wait(), it loops forever waiting for commands that will never arrive. Add stopped.load() re-check after each wake or use wait_timeout.


Overall solid design — deadline enforcement, two-worker architecture, telemetry propagation are well thought out. The above items are edge cases in the new queue/worker model.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make native serving plugin proposal deadlines resilient to slow lifecycle callbacks

2 participants