From efbc8e79854e7319c32d8155fa714b8501b23ee1 Mon Sep 17 00:00:00 2001 From: Snow Pettersen Date: Wed, 5 Aug 2026 11:04:18 -0400 Subject: [PATCH 1/2] add plan for new event buffer design --- .plans/event-buffer-plan.md | 543 ++++++++++++++++++++++++++++++++++++ 1 file changed, 543 insertions(+) create mode 100644 .plans/event-buffer-plan.md diff --git a/.plans/event-buffer-plan.md b/.plans/event-buffer-plan.md new file mode 100644 index 000000000..76eebd748 --- /dev/null +++ b/.plans/event-buffer-plan.md @@ -0,0 +1,543 @@ +# Logger Reliability Improvements: EventBuffer Plan + +This plan introduces a single, mutex-backed `EventBuffer` at the logger edge. It is delivered in +four milestones: first make session persistence generation-based and independently durable, +then build EventBuffer itself, then wire EventBuffer into logger ingress while retaining the +current `PreConfigBuffer`, and finally replace startup buffering with EventBuffer's delayed soft +replay gate. + +## Goals + +- Prefer important logs when bounded memory requires loss. +- Preserve workflow ordering during startup, including prior-session crash logs. +- Make every normal admission decision observable and priority-aware. +- Capture provider metadata close to the original `Logger.log` call. + +The ring buffer and upload pipeline are out of scope. + +## Architecture + +```text +LoggerHandle / state APIs + -> synchronous EventBuffer admission + -> Notify + -> AsyncLogBuffer task + -> PreConfigBuffer or workflow engine +``` + +`EventBuffer` replaces the log and state ingress channels and `OrderedReceiver`. It is shared by +synchronous producers and has one asynchronous consumer. The producer path uses +`parking_lot::Mutex` only for short, non-awaiting buffer operations; the consumer releases the +lock before awaiting work or invoking the pipeline. The existing logger continues to use its +current channels and `PreConfigBuffer` until milestone 3; milestone 4 moves startup responsibility +into EventBuffer. + +The buffer lock is the ordering point. No producer-visible sequence number is needed: events are +delivered in lock-admission order, and an internal monotonic insertion ID only breaks priority +ties and preserves oldest-retained behavior. + +EventBuffer lifecycle and replay gating are independent state machines. Its lifecycle is +`Accepting` until shutdown changes it to `Closed`; closed handles reject new work. Separately, +Milestone 4's drain gate is `Holding` or `Open`. A holding gate still accepts and accounts for +entries—it merely withholds consumer delivery. This avoids using "closed" to mean both normal +startup buffering and terminal shutdown. + +## Control-flow ownership + +EventBuffer is the ordered data-plane ingress, not a general control bus. The migration retains +the following ownership boundaries. + +| Flow | Owner and transport | Why it does or does not enter EventBuffer | +| --- | --- | --- | +| Logs, feature-flag exposure, post-startup memory pressure/entity-ID persistence, and `FlushState` | EventBuffer entries | These are ordered workflow, state-store, or barrier inputs. `FlushState` and `Block::Yes` logs are protected entries. | +| Logger `setField` / `removeField` | Synchronous EventBuffer-owned COW map | These mutate admission metadata rather than producing a replayable workflow input. | +| Session creation/rotation, durable persistence, and session flush | `bd_session` generation-based coalescing flusher | Session mutation remains under the strategy's existing mutex. A single flusher persists only the latest generation and has no per-operation FIFO command queue. EventBuffer receives only the resolved immutable session ID. | +| Config updates and configuration readiness | Existing config-update path directly to the consumer | Applying config can build/replace downstream pipeline state and has no producer admission order. In Milestone 4, readiness starts the runtime-configured replay-delay timer; it never consumes EventBuffer capacity. | +| Crash-report processing request | Existing report-processing request path directly to the consumer, then one EventBuffer batch admission | The request triggers potentially expensive report discovery and parsing. Only the resulting replayable crash logs enter EventBuffer, preserving their batch source order and taking one well-defined admission boundary relative to concurrent producers. | +| `CrashPending` | Direct drain-gate extension hint | It changes gate policy, not workflow state; it can extend a held window but cannot itself deliver, reorder, or consume buffer capacity. | +| Shutdown, `Notify`, timers, SDK lifecycle/status, sleep-mode watch, and tracing flag | Direct lifecycle/scheduling primitives | These change consumer scheduling or local observable state; they are not retained replayable work. Shutdown closes EventBuffer rather than queuing a terminal entry. | +| Downstream stats, upload, buffer-flush, and workflow side effects | Existing consumer-owned downstream channels | These are consequences of a processed ordered entry, not new logger ingress. Do not feed them back through EventBuffer. | + +The existing configuration and crash-request channels therefore remain purpose-specific control +paths. Session persistence adds only a coalesced wakeup/flush mechanism inside `bd_session`, not a +second general-purpose logger control queue: a flow belongs in EventBuffer when it needs ordering +with replayable inputs, and otherwise stays with the owner above. + +## Metadata and state handling + +- `LoggerHandle::log` captures the provider timestamp and provider fields inline, outside the + EventBuffer lock and while actually holding `with_thread_local_logger_guard`. The existing + admission-only guard is not sufficient once provider code runs on the caller thread. +- It then locks EventBuffer, snapshots its copy-on-write `setField` map, and admits a + `CapturedLog` containing both snapshots. A same-thread `setField(); log()` is therefore + reflected in the captured log; concurrent callers are ordered by buffer admission. +- `setField` and `removeField` update EventBuffer's copy-on-write field map synchronously. They + are not downstream workflow events. EventBuffer enforces a separate configured aggregate + logger-field byte/count limit: a mutation that would exceed it is rejected and leaves the + current map unchanged. Log calls only clone an `Arc` snapshot. +- Refactor `MetadataCollector` into provider-snapshot capture and background normalization. The + async consumer merges captured provider data, caller fields, and the captured logger field map, + then performs global-state tracking, state-store updates, and replay. It must not resolve a + mutable current session for an already-admitted log. +- Prior-run logs keep their existing special path: do not capture current-process provider data; + finalize them against prior global state on the background task. Preserve the existing + `PreviousRunSessionID` `_logged_at` behavior explicitly: it currently uses the timestamp + provider only, while `occurred_at` comes from the crash report. +- State/control operations that affect workflows or persistence remain protected EventBuffer + entries. `Block::Yes` logs are also protected: blocking is an explicit reliability request, not + merely a consumer-completion mechanism. Dedicated session persistence belongs to `bd_session`; + crash-pending and shutdown remain direct control signals, not buffer entries. +- `set_feature_flag_exposure` resolves its session ID through `bd_session`, then captures provider + and logger-field snapshots plus an admission timestamp before EventBuffer admission; provider + capture uses the same held thread-local guard as logs. The consumer uses those immutable inputs + for state-store insertion, global-state tracking, and workflow replay rather than querying the + provider or session strategy later. Any implicit-session callback is dispatched after this state + operation is admitted or terminally dropped, using the same rule as logs. + +Provider capture is intentionally outside the EventBuffer lock, so each operation has two defined +cut points: provider time/fields and session ID are captured at the original API call, while the +logger-field map and FIFO position are captured at EventBuffer lock admission. A concurrent +`setField` that wins the EventBuffer lock before a log is admitted is therefore reflected in that +log; one that loses is not. Session mutations are serialized by `bd_session`'s existing state +mutex; persistence is coalesced separately. +This is the linearization rule for concurrent callers, and is preferable to holding the EventBuffer +mutex across arbitrary platform provider code. + +Provider calls now become part of normal `Logger.log` latency. Before the migration, add duration +and failure telemetry to the current async calls; retain that telemetry after the move and measure +edge lock wait/hold time. A slow-provider result is the decision point for retaining the older +ingress-task design instead. This is also a provider-threading migration: today providers are +called serially on the async task; after the move they may be called concurrently on arbitrary +application threads. Certify the platform implementations' thread-affinity, synchronization, and +reentrant-logging behavior before enabling inline capture. + +## Session transitions + +Session durability remains outside EventBuffer. `bd_session` keeps ownership of its existing +mutex-protected in-memory state; EventBuffer never holds a session strategy handle, persistence +entry, callback, or session-persistence byte charge. + +Milestone 1 replaces snapshot-carrying `PreparedSessionOperation` persistence with a +generation-based coalescing flusher inside `bd_session`: + +- A session mutation applies under the existing strategy mutex, increments a monotonic dirty + generation, and returns the new/current session ID immediately. It records any deferred callback + against that generation, rather than handing a cloned `LoadedState` to a logger queue. +- A persistence request marks the strategy dirty and wakes one flusher. The flusher snapshots the + latest in-memory state and generation, persists it, then checks whether a newer generation was + created while the write was in flight. If so, it persists the newest snapshot again. Only one + flusher writes at a time, so an older asynchronous write cannot be the final durable state. +- There is no FIFO command queue and no persistence-before-return requirement for + `session_id`/`start_new_session`: a successfully applied mutation becomes current immediately; + persistence is best effort and measured. This intentionally adopts the non-blocking session + creation decision in this plan. +- A callback is dispatched only after the flusher has attempted persistence through its generation, + outside the strategy mutex, through a platform-provided `SessionCallbackDispatcher`. This is an + intentional change from the current originating/consumer-thread delivery and must be approved as + part of Milestone 1. After Milestone 3, an implicit log/feature-flag callback additionally waits + for its source admission outcome before dispatching. +- `FlushState(Block::Yes)` captures the current session generation after draining earlier + EventBuffer work and waits until persistence has attempted that generation before the existing + session/store/workflow flush completes. Shutdown remains best effort and does not turn pending + persistence into EventBuffer entries. + +Pure reads such as `previous_process_session_id` remain direct. In Milestone 3, current-process +logs and feature flags resolve their ID from the in-memory strategy before provider capture and +store that immutable ID in their EventBuffer payload. A later EventBuffer rejection never rolls +back the already-current session. + +The workflow engine still receives no new session-control event. Its existing session transition +behavior is driven by the captured session ID on the first log or session-bearing state operation +it processes. + +## EventBuffer behavior + +EventBuffer owns a `VecDeque` in admission order, byte accounting, the copy-on-write field +map, and a `Notify` for its consumer. The deque is both the FIFO delivery order and the source +scanned for priority eviction. + +### Queue representation and growth + +Each admission receives a monotonic insertion ID and is appended directly to `VecDeque`. +On pressure, EventBuffer scans the deque to select strictly lower-priority victims. It evicts +lowest-priority candidates first and, within a victim priority, newer entries first; this preserves +the oldest retained entry for an equal-priority admission. + +- Eviction physically removes selected entries with `VecDeque::remove`, in descending index order + so earlier selected indexes remain valid. There are no tombstones, stale index records, or + compaction passes in this version. +- `next_batch` drains with `pop_front`, so delivery remains ordinary FIFO over the retained + entries. +- Every entry is charged a conservative fixed bookkeeping overhead in addition to its payload. + This gives even zero-payload control entries a nonzero cost and bounds both live entry count and + queue metadata for a full buffer. +- The deque grows only on successful admission using fallible reservation before any existing + entry is evicted. Allocation failure rejects the incoming entry and leaves retained entries + untouched. Capacity is retained for amortized producer latency and is not synchronously shrunk + on the hot path. +- Completion callbacks are collected while the lock is held and invoked only after it is released. + Admission, eviction, and callback code therefore cannot re-enter one another while holding + EventBuffer state. + +EventBuffer has two configured byte limits, neither of which preallocates memory: + +- `total_limit`: a hard limit over every retained EventBuffer entry. Its initial value is 11 MiB, + preserving the current 1 MiB log-channel plus 10 MiB state-channel allowance. +- `log_limit`: a 1 MiB sub-limit over evictable log entries only. Protected state/control entries + and protected logs bypass this sub-limit but remain charged to `total_limit`. + +On ordinary-log admission, EventBuffer first makes room within `log_limit` by evicting lower +priority evictable logs. It drops the incoming log when it cannot displace a retained log; equal +priority retains the older entry. It then checks `total_limit`, using the same eviction policy if +additional evictable log bytes must be released. A protected entry bypasses `log_limit` and may +evict any evictable log to fit `total_limit`; it is rejected only if the total limit is occupied +entirely by protected entries. A normal log larger than `log_limit`, or any entry larger than +`total_limit`, is rejected and measured. There is no scheduler-dependent soft overflow. + +Priority policy: + +1. Prior-run logs, lifecycle logs, `Block::Yes` logs, and state/control events are protected and + replay-capable. +2. `NORMAL` and `DEVICE` logs form the higher evictable class. +3. `SPAN`, `REPLAY`, `INTERNAL_SDK`, and `RESOURCE` form the lower evictable class. +4. Within either log class, severity is error, warn, info, debug, then trace. + +### Shutdown and terminal outcomes + +Shutdown first changes EventBuffer's lifecycle to `Closed` under the lock, then wakes the consumer. +The consumer may finish an already detached batch, but it does not begin a new batch after its +shutdown branch wins. All still-retained entries are removed under the lock, recorded as shutdown +drops, and have any completion resolved after the lock is released. This preserves the current +best-effort shutdown behavior while preventing waiters from timing out solely because their sender +was stranded in the buffer. + +`Block::Yes` and blocking `FlushState` preserve their current public meaning: the caller waits for +a terminal outcome, not a guarantee that the log was delivered or that every downstream operation +succeeded. A blocking log bypasses `log_limit` and is never a priority-eviction victim, while it +still counts against `total_limit`; it can be explicitly rejected if the protected portion has +filled that hard limit. The completion payload remains unit; protected-budget rejection, +provider-capture failure, processing completion, and shutdown all resolve it exactly once. The +distinguishing information is emitted through outcome metrics and internal diagnostics, not a new +public API result. + +## ALB migration audit + +The existing async log buffer is more than a log receiver. Milestone 3 must replace the following +behaviors deliberately rather than moving only `LoggerHandle::log`. + +| Current ALB input or behavior | EventBuffer migration requirement | +| --- | --- | +| Normal logs and helper-produced `RESOURCE`, `REPLAY`, `LIFECYCLE`, and `INTERNAL_SDK` logs | Route every path through the concrete admission/consumer flow below; helpers only construct their existing type-specific fields and never bypass EventBuffer. | +| `AddLogField` / `RemoveLogField` | Update EventBuffer's copy-on-write map synchronously after the same custom-key validation the collector performs today. Do not emit a downstream workflow entry. | +| Feature-flag exposure | Keep it a protected ordered state operation. Capture both its session ID and the provider/field snapshots needed by `handle_state_insert` at admission; otherwise it would still call providers later and observe a different session/global context. While Milestone 3 still uses `PreConfigBuffer`, extend its pending feature-flag item to carry these captured inputs and replay that immutable payload after config. | +| Memory-pressure and opaque-entity updates | Preserve their ordered durable state-store writes. Before the state store is initialized, coalesce opaque-entity updates in an EventBuffer-owned pending slot rather than enqueueing individual state entries. Builder atomically takes that latest value, persists it, updates the public watch only on successful persistence, and marks the store ready; no stale startup entries remain to overwrite it later. After readiness, each update is a protected entry and updates the watch only after admission. Memory-pressure remains an ordered protected persistence entry and retains its existing prior-run initialization path. | +| `session_id` and `start_new_session` | In Milestone 1, remove ALB's `PersistPreparedSession` state entry and use `bd_session`'s generation-based coalesced persistence. A mutation becomes current immediately and wakes persistence without blocking the caller. In Milestone 3, EventBuffer receives only the immutable session ID already resolved from that in-memory state; it contains no session-persistence entry. | +| `FlushState` and `Block::Yes` logs | Classify every `FlushState` as protected, and classify every `Block::Yes` log as a protected log. They bypass `log_limit`, are never evicted for priority, and remain bounded by `total_limit`; an all-protected full buffer explicitly rejects the incoming operation and resolves its completion. `FlushState(Block::Yes)` and blocking logs are ordered barriers. A blocking flush drains all earlier admitted EventBuffer work, captures and waits for the current session persistence generation, then runs the existing stats, buffer, session, and workflow flushes. `FlushState(Block::No)` remains protected but does not change startup timing. Every blocking completion must resolve exactly once on processing, protected-budget rejection, admission failure, or shutdown so callers never wait forever. | +| Crash-report requests and the crash-monitor callback | Keep report scanning outside EventBuffer. Once a scan returns, take one EventBuffer lock and admit its reports as an ordered batch, applying normal priority eviction per report without producer interleaving. A batch is an atomic producer-order boundary, not all-or-nothing: every report gets its own admission result and metric, so a full protected budget can retain an ordered prefix and explicitly reject the remainder without soft overflow. Current-run reports capture current provider/field/session context at that admission. Previous-run reports are protected, use persisted prior global state and the prior-process session ID; if none exists, use the normal current-session preparation path and record the fallback. They do not capture current field-provider fields, but preserve the existing timestamp-provider use for `_logged_at`. The crash-monitor callback uses the same current-run admission helper. `CrashPending` remains an out-of-band gate-extension signal. | +| Config updates | Keep these control-plane messages outside EventBuffer. They have no producer admission order today and may perform expensive pipeline setup; configuration readiness is the explicit gate-release condition. | +| Workflow-injected logs | Keep them within the consumer's current processing transaction rather than re-admitting them at the edge. They must inherit immutable source context—at minimum the source session ID—so a later edge session transition cannot relabel generated logs. | +| Interceptors | Keep all interceptors on the single consumer and outside the EventBuffer lock. This includes internal-report counters, HTTP/battery aggregation, network-quality decoration, device matching fields, and the screenshot-ready side effect; moving them would change their serialized state and side effects. | + +### Normal and helper-produced log flow + +Every `LoggerHandle::log` path—including resource utilization, session replay, SDK start, app +update, and internal SDK helpers—calls one `EventBuffer::admit_log` API. Helpers construct their +existing message, fields, and `LogType`; priority follows from that type and level inside +EventBuffer rather than from a helper-specific queue path, except that `Block::Yes` promotes the +log to the protected class. + +1. For a current-process log, `bd_session` resolves the current session ID and schedules any + required persistence. A resolution failure is a terminal log drop; a successful implicit + rotation records a deferred callback for post-admission dispatch once persistence is attempted. + The caller then holds + `with_thread_local_logger_guard` and captures provider timestamp and fields outside the + EventBuffer lock. A provider failure is also a terminal drop; both outcomes record their + respective metrics and resolve any `Block::Yes` completion without entering EventBuffer. +2. `PreviousRunSessionID` logs skip current-process session, provider, and logger-field capture. + They retain their raw fields and override for the existing previous-global-state consumer path. + Normal logs and `OccurredAt` logs proceed with their captured provider data; the latter retains + its supplied occurrence timestamp. +3. EventBuffer acquires its lock and snapshots the COW logger-field map. The log entry retains the + original `LogLine` message, fields, matching fields, override, `CaptureSession`, provider + snapshot, field-map snapshot, session ID, and optional completion handle. A rejected log does + not roll back or otherwise alter an already-persisted session transition. +4. Admission applies the total/log limits and priority eviction policy. A `Block::Yes` log is + protected, so it bypasses `log_limit` and cannot be evicted; it is rejected only if it cannot + fit the remaining `total_limit` after evictable entries have been displaced. Rejection or + eviction resolves the entry's completion with a terminal drop outcome after releasing the lock. + Successful admission schedules `Notify`; it does not wait for the background consumer. In + either admission outcome, dispatch any deferred implicit-session callback only after the lock is + released, so callback-originated logging follows this source operation. +5. The consumer removes the entry in FIFO order, runs the existing interceptors, then normalizes + the original fields using the captured provider and logger-field snapshots. It uses the captured + session ID rather than querying mutable session state. For `OccurredAt`, it emits the supplied + timestamp and attaches captured provider time as `_logged_at`; the previous-run branch retains + its existing prior-global-state and `_logged_at` semantics. It then follows the existing replay, + buffer-writing, `CaptureSession`, and blocking-flush path. A successfully processed blocking log + resolves its completion exactly once after that path finishes. + +`CapturedLog` sizing includes provider snapshots, the session ID, and completion state. The +logger-field `Arc` is deliberately not charged once per retained log: EventBuffer instead enforces +the aggregate logger-field byte/count limit at `setField` time. Old COW snapshots retained by +queued logs are accepted as auxiliary memory, not a reason to evict or reject otherwise valid +events. Their worst case is bounded by the configured map limit times the bounded number of +retained map versions. Record live bytes, distinct-snapshot count, and rejected field mutations; +if that overhead proves material, replace the map representation with structural sharing rather +than coupling it to log-priority eviction. + +The consumer exposes `EventBuffer::next_batch(max_entries)` as one branch of the existing async +`select!`. That future first registers `Notify::notified()`, then checks and takes a bounded FIFO +batch under the lock; registering before the check prevents a missed wakeup. It releases the lock +before interceptors, normalization, persistence, and replay. If entries remain after a batch, the +next call is immediately ready; control returns to `select!` between batches so configuration, +crash-report processing, the pipeline, timers, resource utilization, replay recording, events, +and shutdown retain fair progress. No code awaits, runs callbacks, parses reports, updates config, +or flushes while holding the lock. + +## Milestone 1: generation-based session persistence + +- Refactor `bd_session` so a mutation increments an in-memory persistence generation and no + longer hands a cloned state snapshot to `PersistPreparedSession`. Remove that ALB state variant. +- Add one coalescing persistence flusher: snapshot the latest state/generation, persist, and loop + if a newer generation appeared. It has a single in-flight write, bounded state, and a wakeup—not + a FIFO queue of prepared operations or per-operation responses. +- Make `session_id` and `start_new_session` non-blocking with respect to persistence. They return + after their in-memory mutation has succeeded and record failure/latency telemetry from the later + attempt. Deferred callbacks use `SessionCallbackDispatcher` after that attempt rather than the + current initiating/consumer thread. Preserve pure previous-process lookup behavior. +- Preserve current automatic-rotation *detection* while ALB remains in place. The consumer still + resolves sessions at its current processing point; it merely wakes the coalescing flusher rather + than awaiting a persisted snapshot. `FlushState(Block::Yes)` captures a generation and waits + until it has been attempted. +- Add generation, coalescing, attempt/failure, and callback-outcome telemetry. Test concurrent + `session_id`/`start_new_session` mutations, a write completing after newer mutations, + last-activity persistence, automatic rotation, flush-through-generation, shutdown, and callback + dispatch. No EventBuffer, provider, log-admission, or startup-replay semantics change in this + milestone. + +## Milestone 2: EventBuffer state machine + +- Implement EventBuffer as an unused logger-internal component with the full entry model required + by this plan: captured logs (including protected `Block::Yes` logs), protected state/control + entries, completion handles, and + closed/shutdown state. +- Implement dual-limit admission, priority classification, FIFO delivery, bounded deque scans and + physical eviction, insertion-ID tie breaking, protected-entry handling, fallible container + growth, and terminal completion on rejection, eviction, or close. +- Implement the copy-on-write logger-field map with its independent aggregate byte/count limit, + field validation, and snapshot telemetry. +- Implement `next_batch(max_entries)` with the lost-wakeup-safe `Notify` protocol and bounded + batches. Test it independently from the async logger's `select!` loop. +- Add focused unit and concurrency tests for all capacity, priority, ordering, COW, completion, + close, and notification invariants. This remains an unused component milestone; logger ingress + behavior is unchanged. + +## Milestone 3: logger ingress migration + +- Construct EventBuffer with the logger and replace the ALB log/state channels and + `OrderedReceiver` with its synchronous handle and `next_batch` branch in the existing async + `select!` consumer. +- Move provider snapshot capture to `LoggerHandle`, move logger-managed fields into EventBuffer, + and split metadata normalization from provider capture. Enable this only after platform-provider + threading certification and its telemetry are in place. +- Move current log and feature-flag session resolution from the consumer to the logger edge using + the Milestone-1 in-memory `bd_session` API. Capture the returned session ID before provider capture and + EventBuffer admission. Implicit log/state rotations dispatch callbacks only after source + admission/drop as well as their persistence attempt. +- Migrate every ALB state and internal-ingress path in the audit table, including feature-flag + metadata/session capture, opaque-entity startup recovery, crash-report batches, interceptor + placement, generated-log context, and flush/blocking completion semantics. +- Preserve the current `PreConfigBuffer` and its immediate replay on initial configuration. + Consequently, Milestone 3 has up to the 11 MiB EventBuffer allowance plus the existing 1 MiB + startup buffer allowance while configuration is unavailable. Overflow in that startup buffer + retains the current FIFO behavior and metrics; priority-aware startup retention arrives in + milestone 4. +- Ship integration telemetry for EventBuffer admission, eviction, provider latency, lock latency, + consumer service time, session-persistence generation/coalescing/attempt time, and `select!` + fairness. Use + production measurements to validate the synchronous provider and locking cost before changing + startup semantics. + +## Milestone 4: soft startup replay gate + +After Milestone 3 is stable, replace `PreConfigBuffer` with EventBuffer startup buffering and add +the soft drain gate below. This delivers delayed replay and crash-log reordering without coupling +those startup semantics to the ingress migration. + +EventBuffer starts with its drain gate `Holding`. Once configuration has created the processing +pipeline, it reads the replay-delay runtime configuration and starts the base replay timer. The +gate opens only after that configuration-relative deadline has passed. A platform crash-pending +hint can extend the deadline while the gate is still holding, subject to the configured extension +limit. Holding continues to capture and prioritize events but does not deliver them. + +Before configuration is ready, `CrashPending` is retained as a pending extension hint and a +high-watermark crossing is retained as an early-release request; neither can deliver work without +a pipeline. At configuration readiness, apply the pending hint to the runtime-configured deadline. +If the buffer is already at the high watermark, release immediately with reason +`high_watermark`; otherwise arm the configured timer. + +Configuration construction, including restoration of already-persisted workflow actions, remains +outside the EventBuffer ordering domain and keeps its current startup behavior while the gate is +holding. `InitLifecycle::LogProcessingStarted` and the SDK "running" status move to the first +gate release, immediately before the first EventBuffer batch is delivered; creating the pipeline +alone is not reported as log processing. + +Removing `PreConfigBuffer` at this point means startup events are retained in their original +EventBuffer representation, so the same priority/eviction policy applies before and after +configuration is ready. + +### Previous-session replay ordering + +`CapturedLog` carries two independent classifications: its retention priority and its source +(`CurrentProcess` or `PreviousProcess`). A previous-process log is eligible for special ordering +only when EventBuffer admits it while the startup gate is holding. The eligibility bit is captured +on the entry; it is not inferred later from the source alone. + +When the gate releases, EventBuffer takes its deque under the lock, partitions the already-admitted +entries into reorderable previous-process logs and everything else, then restores the deque as: + +```text +previous-process logs (their original FIFO order) +-> all other retained entries (their original FIFO order) +``` + +The same lock transition changes the gate to `Open`. Entries admitted afterwards always append to +the back, including a late previous-process crash log. The gate never reopens, so a crash report +that arrives later in the session keeps its higher retention priority but is not reordered ahead of +current-session work. This avoids retroactively changing workflow order after current-session +events have started flowing. + +Only prior-process logs move during this partition. Current state/control entries, fields, +session-bearing entries, and flush barriers stay in their original FIFO relation. The one-time +partition moves entries without cloning payloads; it is an intentional startup-only lock hold and +is instrumented separately. + +The gate is soft: admission of a protected event at or above an 80%-of-`total_limit` high +watermark opens it early with reason `high_watermark`. Low-priority traffic alone does not shorten +the startup window. If the consumer still cannot catch up, the normal hard-cap eviction policy +applies; priority-event loss is measured rather than exceeding capacity. + +`CrashPending` may extend the deadline only while the gate is holding. A high-watermark release, +a current-process session change after the gate has observed its first current-process session ID, +a flush barrier, or normal timer release seals the ordering window; later hints and late +previous-process logs cannot reopen it. + +The first current-process session-bearing entry establishes the gate's current-session baseline. A +later current-process entry whose captured session ID differs from that baseline is a replay +barrier: if admitted while holding, the buffer drains through that entry after partitioning +eligible previous-process logs first. This prevents retained previous-session logs from being +finalized under the newer session. `start_new_session` itself remains an in-memory session mutation; with no +following EventBuffer entry it has no replay-ordering effect. + +An admitted `FlushState(Block::Yes)` or blocking log is also a gate barrier: it seals the gate, +partitions eligible previous-process logs first, then drains through its ordered position before +its completion resolves. It does not bypass older work. An admission-rejected or +provider-capture-rejected blocking operation resolves immediately as a terminal drop and cannot +act as a barrier. `FlushState(Block::No)` stays behind the gate, matching its existing +fire-and-forget behavior. Neither admitted blocking operation may remain pending solely because +the soft startup delay has not elapsed. + +## Observability and validation + +- Preserve the existing log enqueue success/full/closed metrics for continuity until the channel + path is removed; add equivalent state metrics during the transition. +- Record EventBuffer admission, eviction, incoming drop, protected rejection, oversized rejection, + queued bytes by total/log/protected category, high-watermark replay, scheduled replay, crash-hint replay, + and time spent behind the drain gate. Break these down by event kind, log type, level, and + completion outcome. Record aggregate logger-field bytes/count, COW snapshot bytes/version + count, and field-limit rejections separately from event-buffer eviction. Record startup-window + sealing reason, reordered previous-process count, late previous-process count, and partition + duration separately from ordinary dequeue latency. +- Record provider duration/failure and EventBuffer lock wait/hold histograms before and after the + inline-provider migration, including state-operation snapshots. Record consumer batch length, + time between notification and dequeue, and service time for each external `select!` branch. +- In milestone 2, test dual-limit admission, priority eviction, FIFO retention, equal-priority + oldest retention, protected-entry behavior, oversized input, COW field snapshots and limits, + lifecycle close, completion on rejection/eviction/close, and Notify wake/drain races. Add + repeated arbitrary eviction tests that verify descending-index removal, plus allocation-failure + and callback-reentrancy coverage. +- In milestone 3, add old-log / session-start / new-log attribution, automatic session rotation, + concurrent session-start/log admission, edge-time in-memory session resolution, explicit- and + implicit-rotation callback dispatch/order, feature-flag state replay with captured metadata, + opaque-entity pre-store coalescing/admission/recovery, memory-pressure persistence, flush + ordering, both crash-report paths, provider reentrancy/threading, generated-log session + inheritance, and bounded-batch fairness with a continuously non-empty EventBuffer. +- In milestone 4, add prior-run metadata behavior, gate timer and crash extension, + high-watermark early replay, barrier release, previous-process FIFO partitioning, late + previous-process no-reorder behavior, captured-session-change gate activation, + blocking-flush gate activation, nonblocking-flush gate retention, and + PreConfigBuffer-to-EventBuffer migration coverage. +- Add contention benchmarks covering concurrent logging, field updates, and deliberately slow + providers as part of milestone 3. Verify the crate with `cargo nextest run -p bd-logger`. + +## Decision record + +- The plan deliberately avoids an extra ingress task and the residual blind-drop path of a bounded + Tokio ingress channel. +- It deliberately accepts that provider execution can add synchronous caller latency, subject to + measured provider and lock-tail latency. +- Session persistence is deliberately outside EventBuffer. `bd_session` applies a session mutation + immediately under its own mutex and coalesces durable writes by generation; logs and state + operations carry the resulting immutable session ID. EventBuffer admission never changes, + persists, or rolls back session state. +- EventBuffer is one ordering domain for producer data and state, not for configuration. Existing + configuration control-plane timing remains out of band and is made explicit through the startup + gate. +- The initial limits are an 11 MiB total budget and a 1 MiB evictable-log budget, preserving the + current separate log and state allowances without requiring separate ingress queues. Reducing + either is a later product decision informed by telemetry. +- Milestones 1 through 3 preserve current `PreConfigBuffer` startup behavior; milestone 4 is the + only milestone that changes initialization replay and workflow ordering. +- The current 1 MiB log limit, 10 MiB state limit, and 80% high watermark are initial defaults. + Runtime configurability can be added after telemetry establishes safe bootstrap and live-resize + semantics. +- Provider-capture failures preserve today's best-effort public behavior: the affected log is + dropped, its blocking completion becomes terminal, and diagnostics remain metrics/logging rather + than a new caller-visible error. Reentrant logging during provider capture remains rejected by + the thread-local guard. +- Current-process crash reports use admission-time provider, logger-field, and session snapshots; + prior-process reports use prior global state and the prior-process session ID when available. + This is the attribution rule rather than an unresolved choice. +- An all-protected full EventBuffer rejects the incoming protected entry. It never exceeds + `total_limit`, parks a pending admission, or relies on Tokio scheduling for room; rejection is + explicit and measured. +- The initial COW implementation accepts old map snapshots as bounded auxiliary memory. It will be + replaced with structural sharing only if the specified telemetry shows that retained versions are + material; it is not a prerequisite for the EventBuffer migration. + +### Rejected alternative: fork the previous-process workflow engine + +We considered taking a copy of the persisted workflow-engine state at startup and replaying +previous-process logs against an isolated, in-memory historical engine while current-process logs +continued immediately through the live engine. This would prevent a late previous-process log +from directly resetting or advancing the live engine's state. + +We are not selecting this as a replacement for the soft replay gate. A process boundary is not +necessarily a session boundary: a prior-process log and a current-process log may belong to the +same session. In that case the historical and live engines would advance independently, and there +is no general correct merge for workflow runs, extraction state, tracing, generated logs, or +triggered actions. The prior log must still be processed before the current work in the live +workflow ordering domain. + +The alternative also requires a fork of both the persisted workflow state and the exact workflow +configuration that produced it; the current state snapshot does not contain workflow definitions. +It would require a new action policy as well: workflow processing currently emits metrics, +flush/streaming intents, Sankey work, screenshots, and injected logs while advancing state. A +historical engine would either suppress those effects—changing crash-log workflow behavior—or need +per-effect idempotence and cross-process ownership rules. Those costs are disproportionate to the +cases where process and session boundaries happen to differ. + +An isolated historical engine remains a possible future policy for *late* previous-process logs +after the startup window has sealed, if product decides they must not affect current workflows. It +does not remove the need for the bounded replay gate that establishes the initial ordering. + +## Open questions for team review + +| Topic | Assumption in this plan | Decision needed | +| --- | --- | --- | +| Provider execution contract | Providers run inline on arbitrary application threads and may run concurrently. Provider time, fields, and failure are observed at `LoggerHandle` admission rather than later on the async task. | Are platform providers thread-safe, thread-affinity-free, and fast enough for this to be an SDK contract? If not, retain the ingress-task design or define a provider execution boundary. | +| `setField` contract | Field changes take effect at synchronous EventBuffer admission. The aggregate logger-field map has a configured byte/count limit, and an over-limit mutation is rejected without changing current fields. | Is immediate same-thread `setField(); log()` behavior desired on every platform, and how should callers observe a rejected field mutation? | +| Session callback contract | `bd_session` records an implicit log/state rotation against its persistence generation. Callbacks use `SessionCallbackDispatcher` after the generation has been attempted; after Milestone 3 implicit callbacks also wait for source admission/drop. | Is moving callbacks away from the current originating/consumer thread acceptable, which platform dispatcher provides the required affinity, and is source-admission-before-callback ordering acceptable for activity-session integrations? | +| Startup reordering | Only previous-process logs admitted before gate sealing are reordered ahead of current-process entries. Late previous-process logs retain high eviction priority but are never reordered. | What base delay, maximum crash-hint extension, and high-watermark threshold provide the desired crash coverage without delaying normal startup too much? | +| Startup capacity transition | In Milestone 3, EventBuffer has an 11 MiB hard total while the retained 1 MiB `PreConfigBuffer` may also hold work. Milestone 4 currently removes that second stage and keeps an 11 MiB EventBuffer. | Should Milestone 4 raise `total_limit` to 12 MiB to preserve the current worst-case startup retention budget, or is the intentional 1 MiB reduction acceptable once the duplicate staging buffer is gone? | +| Priority policy | Previous-process, lifecycle, `Block::Yes`, and state/control entries are protected; other log types and levels follow the proposed eviction ranking. | Confirm the taxonomy with product/workflow owners, including whether any customer log classes need to be promoted or demoted. | From 105deb0f62d2d13ff60b3a4ef5d857bc36d7b2a2 Mon Sep 17 00:00:00 2001 From: Snow Pettersen Date: Wed, 5 Aug 2026 12:09:04 -0400 Subject: [PATCH 2/2] implement std::mutex::Mutex unwrap clippy lint --- .plans/event-buffer-plan.md | 543 -------- Cargo.toml | 1 + Makefile | 7 + ci/license_header.py | 6 +- dylint.toml | 2 + tools/dylints/Cargo.lock | 1158 +++++++++++++++++ tools/dylints/Cargo.toml | 3 + .../mutex_lock_must_unwrap/.cargo/config.toml | 6 + .../dylints/mutex_lock_must_unwrap/.gitignore | 1 + .../dylints/mutex_lock_must_unwrap/Cargo.toml | 21 + .../dylints/mutex_lock_must_unwrap/README.md | 22 + .../mutex_lock_must_unwrap/rust-toolchain | 3 + .../mutex_lock_must_unwrap/rustfmt.toml | 1 + .../dylints/mutex_lock_must_unwrap/src/lib.rs | 105 ++ .../ui/lock_with_expect.rs | 13 + .../ui/lock_with_expect.stderr | 10 + .../ui/lock_without_unwrap.rs | 13 + .../ui/lock_without_unwrap.stderr | 10 + .../dylints/mutex_lock_must_unwrap/ui/main.rs | 13 + .../ui/non_std_mutex.rs | 16 + .../ui/ufcs_lock_without_unwrap.rs | 13 + .../ui/ufcs_lock_without_unwrap.stderr | 10 + 22 files changed, 1433 insertions(+), 544 deletions(-) delete mode 100644 .plans/event-buffer-plan.md create mode 100644 dylint.toml create mode 100644 tools/dylints/Cargo.lock create mode 100644 tools/dylints/Cargo.toml create mode 100644 tools/dylints/mutex_lock_must_unwrap/.cargo/config.toml create mode 100644 tools/dylints/mutex_lock_must_unwrap/.gitignore create mode 100644 tools/dylints/mutex_lock_must_unwrap/Cargo.toml create mode 100644 tools/dylints/mutex_lock_must_unwrap/README.md create mode 100644 tools/dylints/mutex_lock_must_unwrap/rust-toolchain create mode 100644 tools/dylints/mutex_lock_must_unwrap/rustfmt.toml create mode 100644 tools/dylints/mutex_lock_must_unwrap/src/lib.rs create mode 100644 tools/dylints/mutex_lock_must_unwrap/ui/lock_with_expect.rs create mode 100644 tools/dylints/mutex_lock_must_unwrap/ui/lock_with_expect.stderr create mode 100644 tools/dylints/mutex_lock_must_unwrap/ui/lock_without_unwrap.rs create mode 100644 tools/dylints/mutex_lock_must_unwrap/ui/lock_without_unwrap.stderr create mode 100644 tools/dylints/mutex_lock_must_unwrap/ui/main.rs create mode 100644 tools/dylints/mutex_lock_must_unwrap/ui/non_std_mutex.rs create mode 100644 tools/dylints/mutex_lock_must_unwrap/ui/ufcs_lock_without_unwrap.rs create mode 100644 tools/dylints/mutex_lock_must_unwrap/ui/ufcs_lock_without_unwrap.stderr diff --git a/.plans/event-buffer-plan.md b/.plans/event-buffer-plan.md deleted file mode 100644 index 76eebd748..000000000 --- a/.plans/event-buffer-plan.md +++ /dev/null @@ -1,543 +0,0 @@ -# Logger Reliability Improvements: EventBuffer Plan - -This plan introduces a single, mutex-backed `EventBuffer` at the logger edge. It is delivered in -four milestones: first make session persistence generation-based and independently durable, -then build EventBuffer itself, then wire EventBuffer into logger ingress while retaining the -current `PreConfigBuffer`, and finally replace startup buffering with EventBuffer's delayed soft -replay gate. - -## Goals - -- Prefer important logs when bounded memory requires loss. -- Preserve workflow ordering during startup, including prior-session crash logs. -- Make every normal admission decision observable and priority-aware. -- Capture provider metadata close to the original `Logger.log` call. - -The ring buffer and upload pipeline are out of scope. - -## Architecture - -```text -LoggerHandle / state APIs - -> synchronous EventBuffer admission - -> Notify - -> AsyncLogBuffer task - -> PreConfigBuffer or workflow engine -``` - -`EventBuffer` replaces the log and state ingress channels and `OrderedReceiver`. It is shared by -synchronous producers and has one asynchronous consumer. The producer path uses -`parking_lot::Mutex` only for short, non-awaiting buffer operations; the consumer releases the -lock before awaiting work or invoking the pipeline. The existing logger continues to use its -current channels and `PreConfigBuffer` until milestone 3; milestone 4 moves startup responsibility -into EventBuffer. - -The buffer lock is the ordering point. No producer-visible sequence number is needed: events are -delivered in lock-admission order, and an internal monotonic insertion ID only breaks priority -ties and preserves oldest-retained behavior. - -EventBuffer lifecycle and replay gating are independent state machines. Its lifecycle is -`Accepting` until shutdown changes it to `Closed`; closed handles reject new work. Separately, -Milestone 4's drain gate is `Holding` or `Open`. A holding gate still accepts and accounts for -entries—it merely withholds consumer delivery. This avoids using "closed" to mean both normal -startup buffering and terminal shutdown. - -## Control-flow ownership - -EventBuffer is the ordered data-plane ingress, not a general control bus. The migration retains -the following ownership boundaries. - -| Flow | Owner and transport | Why it does or does not enter EventBuffer | -| --- | --- | --- | -| Logs, feature-flag exposure, post-startup memory pressure/entity-ID persistence, and `FlushState` | EventBuffer entries | These are ordered workflow, state-store, or barrier inputs. `FlushState` and `Block::Yes` logs are protected entries. | -| Logger `setField` / `removeField` | Synchronous EventBuffer-owned COW map | These mutate admission metadata rather than producing a replayable workflow input. | -| Session creation/rotation, durable persistence, and session flush | `bd_session` generation-based coalescing flusher | Session mutation remains under the strategy's existing mutex. A single flusher persists only the latest generation and has no per-operation FIFO command queue. EventBuffer receives only the resolved immutable session ID. | -| Config updates and configuration readiness | Existing config-update path directly to the consumer | Applying config can build/replace downstream pipeline state and has no producer admission order. In Milestone 4, readiness starts the runtime-configured replay-delay timer; it never consumes EventBuffer capacity. | -| Crash-report processing request | Existing report-processing request path directly to the consumer, then one EventBuffer batch admission | The request triggers potentially expensive report discovery and parsing. Only the resulting replayable crash logs enter EventBuffer, preserving their batch source order and taking one well-defined admission boundary relative to concurrent producers. | -| `CrashPending` | Direct drain-gate extension hint | It changes gate policy, not workflow state; it can extend a held window but cannot itself deliver, reorder, or consume buffer capacity. | -| Shutdown, `Notify`, timers, SDK lifecycle/status, sleep-mode watch, and tracing flag | Direct lifecycle/scheduling primitives | These change consumer scheduling or local observable state; they are not retained replayable work. Shutdown closes EventBuffer rather than queuing a terminal entry. | -| Downstream stats, upload, buffer-flush, and workflow side effects | Existing consumer-owned downstream channels | These are consequences of a processed ordered entry, not new logger ingress. Do not feed them back through EventBuffer. | - -The existing configuration and crash-request channels therefore remain purpose-specific control -paths. Session persistence adds only a coalesced wakeup/flush mechanism inside `bd_session`, not a -second general-purpose logger control queue: a flow belongs in EventBuffer when it needs ordering -with replayable inputs, and otherwise stays with the owner above. - -## Metadata and state handling - -- `LoggerHandle::log` captures the provider timestamp and provider fields inline, outside the - EventBuffer lock and while actually holding `with_thread_local_logger_guard`. The existing - admission-only guard is not sufficient once provider code runs on the caller thread. -- It then locks EventBuffer, snapshots its copy-on-write `setField` map, and admits a - `CapturedLog` containing both snapshots. A same-thread `setField(); log()` is therefore - reflected in the captured log; concurrent callers are ordered by buffer admission. -- `setField` and `removeField` update EventBuffer's copy-on-write field map synchronously. They - are not downstream workflow events. EventBuffer enforces a separate configured aggregate - logger-field byte/count limit: a mutation that would exceed it is rejected and leaves the - current map unchanged. Log calls only clone an `Arc` snapshot. -- Refactor `MetadataCollector` into provider-snapshot capture and background normalization. The - async consumer merges captured provider data, caller fields, and the captured logger field map, - then performs global-state tracking, state-store updates, and replay. It must not resolve a - mutable current session for an already-admitted log. -- Prior-run logs keep their existing special path: do not capture current-process provider data; - finalize them against prior global state on the background task. Preserve the existing - `PreviousRunSessionID` `_logged_at` behavior explicitly: it currently uses the timestamp - provider only, while `occurred_at` comes from the crash report. -- State/control operations that affect workflows or persistence remain protected EventBuffer - entries. `Block::Yes` logs are also protected: blocking is an explicit reliability request, not - merely a consumer-completion mechanism. Dedicated session persistence belongs to `bd_session`; - crash-pending and shutdown remain direct control signals, not buffer entries. -- `set_feature_flag_exposure` resolves its session ID through `bd_session`, then captures provider - and logger-field snapshots plus an admission timestamp before EventBuffer admission; provider - capture uses the same held thread-local guard as logs. The consumer uses those immutable inputs - for state-store insertion, global-state tracking, and workflow replay rather than querying the - provider or session strategy later. Any implicit-session callback is dispatched after this state - operation is admitted or terminally dropped, using the same rule as logs. - -Provider capture is intentionally outside the EventBuffer lock, so each operation has two defined -cut points: provider time/fields and session ID are captured at the original API call, while the -logger-field map and FIFO position are captured at EventBuffer lock admission. A concurrent -`setField` that wins the EventBuffer lock before a log is admitted is therefore reflected in that -log; one that loses is not. Session mutations are serialized by `bd_session`'s existing state -mutex; persistence is coalesced separately. -This is the linearization rule for concurrent callers, and is preferable to holding the EventBuffer -mutex across arbitrary platform provider code. - -Provider calls now become part of normal `Logger.log` latency. Before the migration, add duration -and failure telemetry to the current async calls; retain that telemetry after the move and measure -edge lock wait/hold time. A slow-provider result is the decision point for retaining the older -ingress-task design instead. This is also a provider-threading migration: today providers are -called serially on the async task; after the move they may be called concurrently on arbitrary -application threads. Certify the platform implementations' thread-affinity, synchronization, and -reentrant-logging behavior before enabling inline capture. - -## Session transitions - -Session durability remains outside EventBuffer. `bd_session` keeps ownership of its existing -mutex-protected in-memory state; EventBuffer never holds a session strategy handle, persistence -entry, callback, or session-persistence byte charge. - -Milestone 1 replaces snapshot-carrying `PreparedSessionOperation` persistence with a -generation-based coalescing flusher inside `bd_session`: - -- A session mutation applies under the existing strategy mutex, increments a monotonic dirty - generation, and returns the new/current session ID immediately. It records any deferred callback - against that generation, rather than handing a cloned `LoadedState` to a logger queue. -- A persistence request marks the strategy dirty and wakes one flusher. The flusher snapshots the - latest in-memory state and generation, persists it, then checks whether a newer generation was - created while the write was in flight. If so, it persists the newest snapshot again. Only one - flusher writes at a time, so an older asynchronous write cannot be the final durable state. -- There is no FIFO command queue and no persistence-before-return requirement for - `session_id`/`start_new_session`: a successfully applied mutation becomes current immediately; - persistence is best effort and measured. This intentionally adopts the non-blocking session - creation decision in this plan. -- A callback is dispatched only after the flusher has attempted persistence through its generation, - outside the strategy mutex, through a platform-provided `SessionCallbackDispatcher`. This is an - intentional change from the current originating/consumer-thread delivery and must be approved as - part of Milestone 1. After Milestone 3, an implicit log/feature-flag callback additionally waits - for its source admission outcome before dispatching. -- `FlushState(Block::Yes)` captures the current session generation after draining earlier - EventBuffer work and waits until persistence has attempted that generation before the existing - session/store/workflow flush completes. Shutdown remains best effort and does not turn pending - persistence into EventBuffer entries. - -Pure reads such as `previous_process_session_id` remain direct. In Milestone 3, current-process -logs and feature flags resolve their ID from the in-memory strategy before provider capture and -store that immutable ID in their EventBuffer payload. A later EventBuffer rejection never rolls -back the already-current session. - -The workflow engine still receives no new session-control event. Its existing session transition -behavior is driven by the captured session ID on the first log or session-bearing state operation -it processes. - -## EventBuffer behavior - -EventBuffer owns a `VecDeque` in admission order, byte accounting, the copy-on-write field -map, and a `Notify` for its consumer. The deque is both the FIFO delivery order and the source -scanned for priority eviction. - -### Queue representation and growth - -Each admission receives a monotonic insertion ID and is appended directly to `VecDeque`. -On pressure, EventBuffer scans the deque to select strictly lower-priority victims. It evicts -lowest-priority candidates first and, within a victim priority, newer entries first; this preserves -the oldest retained entry for an equal-priority admission. - -- Eviction physically removes selected entries with `VecDeque::remove`, in descending index order - so earlier selected indexes remain valid. There are no tombstones, stale index records, or - compaction passes in this version. -- `next_batch` drains with `pop_front`, so delivery remains ordinary FIFO over the retained - entries. -- Every entry is charged a conservative fixed bookkeeping overhead in addition to its payload. - This gives even zero-payload control entries a nonzero cost and bounds both live entry count and - queue metadata for a full buffer. -- The deque grows only on successful admission using fallible reservation before any existing - entry is evicted. Allocation failure rejects the incoming entry and leaves retained entries - untouched. Capacity is retained for amortized producer latency and is not synchronously shrunk - on the hot path. -- Completion callbacks are collected while the lock is held and invoked only after it is released. - Admission, eviction, and callback code therefore cannot re-enter one another while holding - EventBuffer state. - -EventBuffer has two configured byte limits, neither of which preallocates memory: - -- `total_limit`: a hard limit over every retained EventBuffer entry. Its initial value is 11 MiB, - preserving the current 1 MiB log-channel plus 10 MiB state-channel allowance. -- `log_limit`: a 1 MiB sub-limit over evictable log entries only. Protected state/control entries - and protected logs bypass this sub-limit but remain charged to `total_limit`. - -On ordinary-log admission, EventBuffer first makes room within `log_limit` by evicting lower -priority evictable logs. It drops the incoming log when it cannot displace a retained log; equal -priority retains the older entry. It then checks `total_limit`, using the same eviction policy if -additional evictable log bytes must be released. A protected entry bypasses `log_limit` and may -evict any evictable log to fit `total_limit`; it is rejected only if the total limit is occupied -entirely by protected entries. A normal log larger than `log_limit`, or any entry larger than -`total_limit`, is rejected and measured. There is no scheduler-dependent soft overflow. - -Priority policy: - -1. Prior-run logs, lifecycle logs, `Block::Yes` logs, and state/control events are protected and - replay-capable. -2. `NORMAL` and `DEVICE` logs form the higher evictable class. -3. `SPAN`, `REPLAY`, `INTERNAL_SDK`, and `RESOURCE` form the lower evictable class. -4. Within either log class, severity is error, warn, info, debug, then trace. - -### Shutdown and terminal outcomes - -Shutdown first changes EventBuffer's lifecycle to `Closed` under the lock, then wakes the consumer. -The consumer may finish an already detached batch, but it does not begin a new batch after its -shutdown branch wins. All still-retained entries are removed under the lock, recorded as shutdown -drops, and have any completion resolved after the lock is released. This preserves the current -best-effort shutdown behavior while preventing waiters from timing out solely because their sender -was stranded in the buffer. - -`Block::Yes` and blocking `FlushState` preserve their current public meaning: the caller waits for -a terminal outcome, not a guarantee that the log was delivered or that every downstream operation -succeeded. A blocking log bypasses `log_limit` and is never a priority-eviction victim, while it -still counts against `total_limit`; it can be explicitly rejected if the protected portion has -filled that hard limit. The completion payload remains unit; protected-budget rejection, -provider-capture failure, processing completion, and shutdown all resolve it exactly once. The -distinguishing information is emitted through outcome metrics and internal diagnostics, not a new -public API result. - -## ALB migration audit - -The existing async log buffer is more than a log receiver. Milestone 3 must replace the following -behaviors deliberately rather than moving only `LoggerHandle::log`. - -| Current ALB input or behavior | EventBuffer migration requirement | -| --- | --- | -| Normal logs and helper-produced `RESOURCE`, `REPLAY`, `LIFECYCLE`, and `INTERNAL_SDK` logs | Route every path through the concrete admission/consumer flow below; helpers only construct their existing type-specific fields and never bypass EventBuffer. | -| `AddLogField` / `RemoveLogField` | Update EventBuffer's copy-on-write map synchronously after the same custom-key validation the collector performs today. Do not emit a downstream workflow entry. | -| Feature-flag exposure | Keep it a protected ordered state operation. Capture both its session ID and the provider/field snapshots needed by `handle_state_insert` at admission; otherwise it would still call providers later and observe a different session/global context. While Milestone 3 still uses `PreConfigBuffer`, extend its pending feature-flag item to carry these captured inputs and replay that immutable payload after config. | -| Memory-pressure and opaque-entity updates | Preserve their ordered durable state-store writes. Before the state store is initialized, coalesce opaque-entity updates in an EventBuffer-owned pending slot rather than enqueueing individual state entries. Builder atomically takes that latest value, persists it, updates the public watch only on successful persistence, and marks the store ready; no stale startup entries remain to overwrite it later. After readiness, each update is a protected entry and updates the watch only after admission. Memory-pressure remains an ordered protected persistence entry and retains its existing prior-run initialization path. | -| `session_id` and `start_new_session` | In Milestone 1, remove ALB's `PersistPreparedSession` state entry and use `bd_session`'s generation-based coalesced persistence. A mutation becomes current immediately and wakes persistence without blocking the caller. In Milestone 3, EventBuffer receives only the immutable session ID already resolved from that in-memory state; it contains no session-persistence entry. | -| `FlushState` and `Block::Yes` logs | Classify every `FlushState` as protected, and classify every `Block::Yes` log as a protected log. They bypass `log_limit`, are never evicted for priority, and remain bounded by `total_limit`; an all-protected full buffer explicitly rejects the incoming operation and resolves its completion. `FlushState(Block::Yes)` and blocking logs are ordered barriers. A blocking flush drains all earlier admitted EventBuffer work, captures and waits for the current session persistence generation, then runs the existing stats, buffer, session, and workflow flushes. `FlushState(Block::No)` remains protected but does not change startup timing. Every blocking completion must resolve exactly once on processing, protected-budget rejection, admission failure, or shutdown so callers never wait forever. | -| Crash-report requests and the crash-monitor callback | Keep report scanning outside EventBuffer. Once a scan returns, take one EventBuffer lock and admit its reports as an ordered batch, applying normal priority eviction per report without producer interleaving. A batch is an atomic producer-order boundary, not all-or-nothing: every report gets its own admission result and metric, so a full protected budget can retain an ordered prefix and explicitly reject the remainder without soft overflow. Current-run reports capture current provider/field/session context at that admission. Previous-run reports are protected, use persisted prior global state and the prior-process session ID; if none exists, use the normal current-session preparation path and record the fallback. They do not capture current field-provider fields, but preserve the existing timestamp-provider use for `_logged_at`. The crash-monitor callback uses the same current-run admission helper. `CrashPending` remains an out-of-band gate-extension signal. | -| Config updates | Keep these control-plane messages outside EventBuffer. They have no producer admission order today and may perform expensive pipeline setup; configuration readiness is the explicit gate-release condition. | -| Workflow-injected logs | Keep them within the consumer's current processing transaction rather than re-admitting them at the edge. They must inherit immutable source context—at minimum the source session ID—so a later edge session transition cannot relabel generated logs. | -| Interceptors | Keep all interceptors on the single consumer and outside the EventBuffer lock. This includes internal-report counters, HTTP/battery aggregation, network-quality decoration, device matching fields, and the screenshot-ready side effect; moving them would change their serialized state and side effects. | - -### Normal and helper-produced log flow - -Every `LoggerHandle::log` path—including resource utilization, session replay, SDK start, app -update, and internal SDK helpers—calls one `EventBuffer::admit_log` API. Helpers construct their -existing message, fields, and `LogType`; priority follows from that type and level inside -EventBuffer rather than from a helper-specific queue path, except that `Block::Yes` promotes the -log to the protected class. - -1. For a current-process log, `bd_session` resolves the current session ID and schedules any - required persistence. A resolution failure is a terminal log drop; a successful implicit - rotation records a deferred callback for post-admission dispatch once persistence is attempted. - The caller then holds - `with_thread_local_logger_guard` and captures provider timestamp and fields outside the - EventBuffer lock. A provider failure is also a terminal drop; both outcomes record their - respective metrics and resolve any `Block::Yes` completion without entering EventBuffer. -2. `PreviousRunSessionID` logs skip current-process session, provider, and logger-field capture. - They retain their raw fields and override for the existing previous-global-state consumer path. - Normal logs and `OccurredAt` logs proceed with their captured provider data; the latter retains - its supplied occurrence timestamp. -3. EventBuffer acquires its lock and snapshots the COW logger-field map. The log entry retains the - original `LogLine` message, fields, matching fields, override, `CaptureSession`, provider - snapshot, field-map snapshot, session ID, and optional completion handle. A rejected log does - not roll back or otherwise alter an already-persisted session transition. -4. Admission applies the total/log limits and priority eviction policy. A `Block::Yes` log is - protected, so it bypasses `log_limit` and cannot be evicted; it is rejected only if it cannot - fit the remaining `total_limit` after evictable entries have been displaced. Rejection or - eviction resolves the entry's completion with a terminal drop outcome after releasing the lock. - Successful admission schedules `Notify`; it does not wait for the background consumer. In - either admission outcome, dispatch any deferred implicit-session callback only after the lock is - released, so callback-originated logging follows this source operation. -5. The consumer removes the entry in FIFO order, runs the existing interceptors, then normalizes - the original fields using the captured provider and logger-field snapshots. It uses the captured - session ID rather than querying mutable session state. For `OccurredAt`, it emits the supplied - timestamp and attaches captured provider time as `_logged_at`; the previous-run branch retains - its existing prior-global-state and `_logged_at` semantics. It then follows the existing replay, - buffer-writing, `CaptureSession`, and blocking-flush path. A successfully processed blocking log - resolves its completion exactly once after that path finishes. - -`CapturedLog` sizing includes provider snapshots, the session ID, and completion state. The -logger-field `Arc` is deliberately not charged once per retained log: EventBuffer instead enforces -the aggregate logger-field byte/count limit at `setField` time. Old COW snapshots retained by -queued logs are accepted as auxiliary memory, not a reason to evict or reject otherwise valid -events. Their worst case is bounded by the configured map limit times the bounded number of -retained map versions. Record live bytes, distinct-snapshot count, and rejected field mutations; -if that overhead proves material, replace the map representation with structural sharing rather -than coupling it to log-priority eviction. - -The consumer exposes `EventBuffer::next_batch(max_entries)` as one branch of the existing async -`select!`. That future first registers `Notify::notified()`, then checks and takes a bounded FIFO -batch under the lock; registering before the check prevents a missed wakeup. It releases the lock -before interceptors, normalization, persistence, and replay. If entries remain after a batch, the -next call is immediately ready; control returns to `select!` between batches so configuration, -crash-report processing, the pipeline, timers, resource utilization, replay recording, events, -and shutdown retain fair progress. No code awaits, runs callbacks, parses reports, updates config, -or flushes while holding the lock. - -## Milestone 1: generation-based session persistence - -- Refactor `bd_session` so a mutation increments an in-memory persistence generation and no - longer hands a cloned state snapshot to `PersistPreparedSession`. Remove that ALB state variant. -- Add one coalescing persistence flusher: snapshot the latest state/generation, persist, and loop - if a newer generation appeared. It has a single in-flight write, bounded state, and a wakeup—not - a FIFO queue of prepared operations or per-operation responses. -- Make `session_id` and `start_new_session` non-blocking with respect to persistence. They return - after their in-memory mutation has succeeded and record failure/latency telemetry from the later - attempt. Deferred callbacks use `SessionCallbackDispatcher` after that attempt rather than the - current initiating/consumer thread. Preserve pure previous-process lookup behavior. -- Preserve current automatic-rotation *detection* while ALB remains in place. The consumer still - resolves sessions at its current processing point; it merely wakes the coalescing flusher rather - than awaiting a persisted snapshot. `FlushState(Block::Yes)` captures a generation and waits - until it has been attempted. -- Add generation, coalescing, attempt/failure, and callback-outcome telemetry. Test concurrent - `session_id`/`start_new_session` mutations, a write completing after newer mutations, - last-activity persistence, automatic rotation, flush-through-generation, shutdown, and callback - dispatch. No EventBuffer, provider, log-admission, or startup-replay semantics change in this - milestone. - -## Milestone 2: EventBuffer state machine - -- Implement EventBuffer as an unused logger-internal component with the full entry model required - by this plan: captured logs (including protected `Block::Yes` logs), protected state/control - entries, completion handles, and - closed/shutdown state. -- Implement dual-limit admission, priority classification, FIFO delivery, bounded deque scans and - physical eviction, insertion-ID tie breaking, protected-entry handling, fallible container - growth, and terminal completion on rejection, eviction, or close. -- Implement the copy-on-write logger-field map with its independent aggregate byte/count limit, - field validation, and snapshot telemetry. -- Implement `next_batch(max_entries)` with the lost-wakeup-safe `Notify` protocol and bounded - batches. Test it independently from the async logger's `select!` loop. -- Add focused unit and concurrency tests for all capacity, priority, ordering, COW, completion, - close, and notification invariants. This remains an unused component milestone; logger ingress - behavior is unchanged. - -## Milestone 3: logger ingress migration - -- Construct EventBuffer with the logger and replace the ALB log/state channels and - `OrderedReceiver` with its synchronous handle and `next_batch` branch in the existing async - `select!` consumer. -- Move provider snapshot capture to `LoggerHandle`, move logger-managed fields into EventBuffer, - and split metadata normalization from provider capture. Enable this only after platform-provider - threading certification and its telemetry are in place. -- Move current log and feature-flag session resolution from the consumer to the logger edge using - the Milestone-1 in-memory `bd_session` API. Capture the returned session ID before provider capture and - EventBuffer admission. Implicit log/state rotations dispatch callbacks only after source - admission/drop as well as their persistence attempt. -- Migrate every ALB state and internal-ingress path in the audit table, including feature-flag - metadata/session capture, opaque-entity startup recovery, crash-report batches, interceptor - placement, generated-log context, and flush/blocking completion semantics. -- Preserve the current `PreConfigBuffer` and its immediate replay on initial configuration. - Consequently, Milestone 3 has up to the 11 MiB EventBuffer allowance plus the existing 1 MiB - startup buffer allowance while configuration is unavailable. Overflow in that startup buffer - retains the current FIFO behavior and metrics; priority-aware startup retention arrives in - milestone 4. -- Ship integration telemetry for EventBuffer admission, eviction, provider latency, lock latency, - consumer service time, session-persistence generation/coalescing/attempt time, and `select!` - fairness. Use - production measurements to validate the synchronous provider and locking cost before changing - startup semantics. - -## Milestone 4: soft startup replay gate - -After Milestone 3 is stable, replace `PreConfigBuffer` with EventBuffer startup buffering and add -the soft drain gate below. This delivers delayed replay and crash-log reordering without coupling -those startup semantics to the ingress migration. - -EventBuffer starts with its drain gate `Holding`. Once configuration has created the processing -pipeline, it reads the replay-delay runtime configuration and starts the base replay timer. The -gate opens only after that configuration-relative deadline has passed. A platform crash-pending -hint can extend the deadline while the gate is still holding, subject to the configured extension -limit. Holding continues to capture and prioritize events but does not deliver them. - -Before configuration is ready, `CrashPending` is retained as a pending extension hint and a -high-watermark crossing is retained as an early-release request; neither can deliver work without -a pipeline. At configuration readiness, apply the pending hint to the runtime-configured deadline. -If the buffer is already at the high watermark, release immediately with reason -`high_watermark`; otherwise arm the configured timer. - -Configuration construction, including restoration of already-persisted workflow actions, remains -outside the EventBuffer ordering domain and keeps its current startup behavior while the gate is -holding. `InitLifecycle::LogProcessingStarted` and the SDK "running" status move to the first -gate release, immediately before the first EventBuffer batch is delivered; creating the pipeline -alone is not reported as log processing. - -Removing `PreConfigBuffer` at this point means startup events are retained in their original -EventBuffer representation, so the same priority/eviction policy applies before and after -configuration is ready. - -### Previous-session replay ordering - -`CapturedLog` carries two independent classifications: its retention priority and its source -(`CurrentProcess` or `PreviousProcess`). A previous-process log is eligible for special ordering -only when EventBuffer admits it while the startup gate is holding. The eligibility bit is captured -on the entry; it is not inferred later from the source alone. - -When the gate releases, EventBuffer takes its deque under the lock, partitions the already-admitted -entries into reorderable previous-process logs and everything else, then restores the deque as: - -```text -previous-process logs (their original FIFO order) --> all other retained entries (their original FIFO order) -``` - -The same lock transition changes the gate to `Open`. Entries admitted afterwards always append to -the back, including a late previous-process crash log. The gate never reopens, so a crash report -that arrives later in the session keeps its higher retention priority but is not reordered ahead of -current-session work. This avoids retroactively changing workflow order after current-session -events have started flowing. - -Only prior-process logs move during this partition. Current state/control entries, fields, -session-bearing entries, and flush barriers stay in their original FIFO relation. The one-time -partition moves entries without cloning payloads; it is an intentional startup-only lock hold and -is instrumented separately. - -The gate is soft: admission of a protected event at or above an 80%-of-`total_limit` high -watermark opens it early with reason `high_watermark`. Low-priority traffic alone does not shorten -the startup window. If the consumer still cannot catch up, the normal hard-cap eviction policy -applies; priority-event loss is measured rather than exceeding capacity. - -`CrashPending` may extend the deadline only while the gate is holding. A high-watermark release, -a current-process session change after the gate has observed its first current-process session ID, -a flush barrier, or normal timer release seals the ordering window; later hints and late -previous-process logs cannot reopen it. - -The first current-process session-bearing entry establishes the gate's current-session baseline. A -later current-process entry whose captured session ID differs from that baseline is a replay -barrier: if admitted while holding, the buffer drains through that entry after partitioning -eligible previous-process logs first. This prevents retained previous-session logs from being -finalized under the newer session. `start_new_session` itself remains an in-memory session mutation; with no -following EventBuffer entry it has no replay-ordering effect. - -An admitted `FlushState(Block::Yes)` or blocking log is also a gate barrier: it seals the gate, -partitions eligible previous-process logs first, then drains through its ordered position before -its completion resolves. It does not bypass older work. An admission-rejected or -provider-capture-rejected blocking operation resolves immediately as a terminal drop and cannot -act as a barrier. `FlushState(Block::No)` stays behind the gate, matching its existing -fire-and-forget behavior. Neither admitted blocking operation may remain pending solely because -the soft startup delay has not elapsed. - -## Observability and validation - -- Preserve the existing log enqueue success/full/closed metrics for continuity until the channel - path is removed; add equivalent state metrics during the transition. -- Record EventBuffer admission, eviction, incoming drop, protected rejection, oversized rejection, - queued bytes by total/log/protected category, high-watermark replay, scheduled replay, crash-hint replay, - and time spent behind the drain gate. Break these down by event kind, log type, level, and - completion outcome. Record aggregate logger-field bytes/count, COW snapshot bytes/version - count, and field-limit rejections separately from event-buffer eviction. Record startup-window - sealing reason, reordered previous-process count, late previous-process count, and partition - duration separately from ordinary dequeue latency. -- Record provider duration/failure and EventBuffer lock wait/hold histograms before and after the - inline-provider migration, including state-operation snapshots. Record consumer batch length, - time between notification and dequeue, and service time for each external `select!` branch. -- In milestone 2, test dual-limit admission, priority eviction, FIFO retention, equal-priority - oldest retention, protected-entry behavior, oversized input, COW field snapshots and limits, - lifecycle close, completion on rejection/eviction/close, and Notify wake/drain races. Add - repeated arbitrary eviction tests that verify descending-index removal, plus allocation-failure - and callback-reentrancy coverage. -- In milestone 3, add old-log / session-start / new-log attribution, automatic session rotation, - concurrent session-start/log admission, edge-time in-memory session resolution, explicit- and - implicit-rotation callback dispatch/order, feature-flag state replay with captured metadata, - opaque-entity pre-store coalescing/admission/recovery, memory-pressure persistence, flush - ordering, both crash-report paths, provider reentrancy/threading, generated-log session - inheritance, and bounded-batch fairness with a continuously non-empty EventBuffer. -- In milestone 4, add prior-run metadata behavior, gate timer and crash extension, - high-watermark early replay, barrier release, previous-process FIFO partitioning, late - previous-process no-reorder behavior, captured-session-change gate activation, - blocking-flush gate activation, nonblocking-flush gate retention, and - PreConfigBuffer-to-EventBuffer migration coverage. -- Add contention benchmarks covering concurrent logging, field updates, and deliberately slow - providers as part of milestone 3. Verify the crate with `cargo nextest run -p bd-logger`. - -## Decision record - -- The plan deliberately avoids an extra ingress task and the residual blind-drop path of a bounded - Tokio ingress channel. -- It deliberately accepts that provider execution can add synchronous caller latency, subject to - measured provider and lock-tail latency. -- Session persistence is deliberately outside EventBuffer. `bd_session` applies a session mutation - immediately under its own mutex and coalesces durable writes by generation; logs and state - operations carry the resulting immutable session ID. EventBuffer admission never changes, - persists, or rolls back session state. -- EventBuffer is one ordering domain for producer data and state, not for configuration. Existing - configuration control-plane timing remains out of band and is made explicit through the startup - gate. -- The initial limits are an 11 MiB total budget and a 1 MiB evictable-log budget, preserving the - current separate log and state allowances without requiring separate ingress queues. Reducing - either is a later product decision informed by telemetry. -- Milestones 1 through 3 preserve current `PreConfigBuffer` startup behavior; milestone 4 is the - only milestone that changes initialization replay and workflow ordering. -- The current 1 MiB log limit, 10 MiB state limit, and 80% high watermark are initial defaults. - Runtime configurability can be added after telemetry establishes safe bootstrap and live-resize - semantics. -- Provider-capture failures preserve today's best-effort public behavior: the affected log is - dropped, its blocking completion becomes terminal, and diagnostics remain metrics/logging rather - than a new caller-visible error. Reentrant logging during provider capture remains rejected by - the thread-local guard. -- Current-process crash reports use admission-time provider, logger-field, and session snapshots; - prior-process reports use prior global state and the prior-process session ID when available. - This is the attribution rule rather than an unresolved choice. -- An all-protected full EventBuffer rejects the incoming protected entry. It never exceeds - `total_limit`, parks a pending admission, or relies on Tokio scheduling for room; rejection is - explicit and measured. -- The initial COW implementation accepts old map snapshots as bounded auxiliary memory. It will be - replaced with structural sharing only if the specified telemetry shows that retained versions are - material; it is not a prerequisite for the EventBuffer migration. - -### Rejected alternative: fork the previous-process workflow engine - -We considered taking a copy of the persisted workflow-engine state at startup and replaying -previous-process logs against an isolated, in-memory historical engine while current-process logs -continued immediately through the live engine. This would prevent a late previous-process log -from directly resetting or advancing the live engine's state. - -We are not selecting this as a replacement for the soft replay gate. A process boundary is not -necessarily a session boundary: a prior-process log and a current-process log may belong to the -same session. In that case the historical and live engines would advance independently, and there -is no general correct merge for workflow runs, extraction state, tracing, generated logs, or -triggered actions. The prior log must still be processed before the current work in the live -workflow ordering domain. - -The alternative also requires a fork of both the persisted workflow state and the exact workflow -configuration that produced it; the current state snapshot does not contain workflow definitions. -It would require a new action policy as well: workflow processing currently emits metrics, -flush/streaming intents, Sankey work, screenshots, and injected logs while advancing state. A -historical engine would either suppress those effects—changing crash-log workflow behavior—or need -per-effect idempotence and cross-process ownership rules. Those costs are disproportionate to the -cases where process and session boundaries happen to differ. - -An isolated historical engine remains a possible future policy for *late* previous-process logs -after the startup window has sealed, if product decides they must not affect current workflows. It -does not remove the need for the bounded replay gate that establishes the initial ordering. - -## Open questions for team review - -| Topic | Assumption in this plan | Decision needed | -| --- | --- | --- | -| Provider execution contract | Providers run inline on arbitrary application threads and may run concurrently. Provider time, fields, and failure are observed at `LoggerHandle` admission rather than later on the async task. | Are platform providers thread-safe, thread-affinity-free, and fast enough for this to be an SDK contract? If not, retain the ingress-task design or define a provider execution boundary. | -| `setField` contract | Field changes take effect at synchronous EventBuffer admission. The aggregate logger-field map has a configured byte/count limit, and an over-limit mutation is rejected without changing current fields. | Is immediate same-thread `setField(); log()` behavior desired on every platform, and how should callers observe a rejected field mutation? | -| Session callback contract | `bd_session` records an implicit log/state rotation against its persistence generation. Callbacks use `SessionCallbackDispatcher` after the generation has been attempted; after Milestone 3 implicit callbacks also wait for source admission/drop. | Is moving callbacks away from the current originating/consumer thread acceptable, which platform dispatcher provides the required affinity, and is source-admission-before-callback ordering acceptable for activity-session integrations? | -| Startup reordering | Only previous-process logs admitted before gate sealing are reordered ahead of current-process entries. Late previous-process logs retain high eviction priority but are never reordered. | What base delay, maximum crash-hint extension, and high-watermark threshold provide the desired crash coverage without delaying normal startup too much? | -| Startup capacity transition | In Milestone 3, EventBuffer has an 11 MiB hard total while the retained 1 MiB `PreConfigBuffer` may also hold work. Milestone 4 currently removes that second stage and keeps an 11 MiB EventBuffer. | Should Milestone 4 raise `total_limit` to 12 MiB to preserve the current worst-case startup retention budget, or is the intentional 1 MiB reduction acceptable once the duplicate staging buffer is gone? | -| Priority policy | Previous-process, lifecycle, `Block::Yes`, and state/control entries are protected; other log types and levels follow the proposed eviction ranking. | Confirm the taxonomy with product/workflow owners, including whether any customer log classes need to be promoted or demoted. | diff --git a/Cargo.toml b/Cargo.toml index e0f471fb3..91e325bab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ members = [ "fuzz", "logger-cli", ] +exclude = ["tools/dylints"] resolver = "2" [workspace.dependencies] diff --git a/Makefile b/Makefile index aa9102b7e..bf3111226 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,13 @@ setup: clippy: setup ci/check_license.sh SKIP_PROTO_GEN=1 SKIP_FILE_GEN=1 cargo clippy --workspace --bins --examples --tests -- --no-deps + $(MAKE) dylint + +.PHONY: dylint +dylint: + command -v cargo-dylint >/dev/null || cargo install cargo-dylint --version 6.0.3 --locked + command -v dylint-link >/dev/null || cargo install dylint-link --version 6.0.3 --locked + cargo dylint --all -- --workspace --all-targets # Leaving the below loop around to help with debugging flakes if needed. .PHONY: test diff --git a/ci/license_header.py b/ci/license_header.py index 07529e07a..1a3d6447f 100644 --- a/ci/license_header.py +++ b/ci/license_header.py @@ -21,6 +21,7 @@ './fuzz/corpus/', './proto/', './target/', + './tools/dylints/target/', './thirdparty/', ) @@ -50,7 +51,10 @@ def check_file(file_path: str): if (file_path.endswith('Cargo.toml') and not file_path == './Cargo.toml' and - not 'license-file = "../LICENSE"' in content): + '[package]' in content and + not ('license-file = "../LICENSE"' in content or + (file_path.startswith('./tools/dylints/') and + 'license-file = "../../../LICENSE"' in content))): raise Exception( f'license-file = "../LICENSE" not found in {file_path}') diff --git a/dylint.toml b/dylint.toml new file mode 100644 index 000000000..84dfebb26 --- /dev/null +++ b/dylint.toml @@ -0,0 +1,2 @@ +[workspace.metadata.dylint] +libraries = [{ path = "tools/dylints", pattern = "mutex_lock_must_unwrap" }] diff --git a/tools/dylints/Cargo.lock b/tools/dylints/Cargo.lock new file mode 100644 index 000000000..5a840c8c8 --- /dev/null +++ b/tools/dylints/Cargo.lock @@ -0,0 +1,1158 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clippy_utils" +version = "0.1.98" +source = "git+https://github.com/rust-lang/rust-clippy?rev=9fca3bc9fc2bc83c60bde26d18ed68f11564b228#9fca3bc9fc2bc83c60bde26d18ed68f11564b228" +dependencies = [ + "arrayvec", + "itertools", + "rustc_apfloat", + "serde", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compiletest_rs" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f150fe9105fcd2a57cad53f0c079a24de65195903ef670990f5909f695eac04c" +dependencies = [ + "diff", + "filetime", + "getopts", + "lazy_static", + "libc", + "log", + "miow", + "regex", + "rustfix", + "serde", + "serde_derive", + "serde_json", + "tester", + "windows-sys 0.59.0", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.19", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dylint" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5d2f27acc9d395eabf8b7001ca856d72e49d4c1c5c3c427c60473fb6112a08" +dependencies = [ + "anstyle", + "anyhow", + "cargo_metadata", + "dylint_internal", + "log", + "once_cell", + "semver", + "serde", + "serde_json", + "tempfile", +] + +[[package]] +name = "dylint_internal" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56d36d5cf7a909a48854ea667b94421643ae0658868b37e4a842fb3d2c6d30e" +dependencies = [ + "anstyle", + "anyhow", + "bitflags 2.13.1", + "cargo_metadata", + "git2", + "home", + "log", + "regex", + "serde", + "tar", + "thiserror 2.0.19", + "toml", +] + +[[package]] +name = "dylint_linting" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561e06d4cbeeaf506d1bce9e92dc737a83f143e8121d09db825b6d4b4bf55728" +dependencies = [ + "cargo_metadata", + "dylint_internal", + "paste", + "rustversion", + "serde", + "thiserror 2.0.19", + "toml", +] + +[[package]] +name = "dylint_testing" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616d3ae583c7daa06c035fcb4e9c3f1c84e14dc1f8bfc05aefcd57fe72a1902e" +dependencies = [ + "anyhow", + "cargo_metadata", + "compiletest_rs", + "dylint", + "dylint_internal", + "env_logger", + "once_cell", + "regex", + "serde_json", + "tempfile", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "git2" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" +dependencies = [ + "bitflags 2.13.1", + "libc", + "libgit2-sys", + "log", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libgit2-sys" +version = "0.18.7+1.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c7391e4b9f4ffab1a624223cc1d7385ff9a678f490768add717de7ea2f4d89" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "mutex_lock_must_unwrap" +version = "0.1.0" +dependencies = [ + "clippy_utils", + "dylint_linting", + "dylint_testing", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_apfloat" +version = "0.2.3+llvm-462a31f5a5ab" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486c2179b4796f65bfe2ee33679acf0927ac83ecf583ad6c91c3b4570911b9ad" +dependencies = [ + "bitflags 2.13.1", + "smallvec", +] + +[[package]] +name = "rustfix" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fa69b198d894d84e23afde8e9ab2af4400b2cba20d6bf2b428a8b01c222c5a" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "tester" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8bf7e0eb2dd7b4228cc1b6821fc5114cd6841ae59f652a85488c016091e5f" +dependencies = [ + "cfg-if", + "getopts", + "libc", + "num_cpus", + "term", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tools/dylints/Cargo.toml b/tools/dylints/Cargo.toml new file mode 100644 index 000000000..3c123b841 --- /dev/null +++ b/tools/dylints/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["mutex_lock_must_unwrap"] +resolver = "2" diff --git a/tools/dylints/mutex_lock_must_unwrap/.cargo/config.toml b/tools/dylints/mutex_lock_must_unwrap/.cargo/config.toml new file mode 100644 index 000000000..226eca535 --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.'cfg(all())'] +rustflags = ["-C", "linker=dylint-link"] + +# For Rust versions 1.74.0 and onward, the following alternative can be used +# (see https://github.com/rust-lang/cargo/pull/12535): +# linker = "dylint-link" diff --git a/tools/dylints/mutex_lock_must_unwrap/.gitignore b/tools/dylints/mutex_lock_must_unwrap/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/.gitignore @@ -0,0 +1 @@ +/target diff --git a/tools/dylints/mutex_lock_must_unwrap/Cargo.toml b/tools/dylints/mutex_lock_must_unwrap/Cargo.toml new file mode 100644 index 000000000..4f167f91c --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mutex_lock_must_unwrap" +version = "0.1.0" +authors = ["bitdrift"] +description = "Require std::sync::Mutex::lock() results to be immediately unwrapped" +edition = "2024" +license-file = "../../../LICENSE" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "9fca3bc9fc2bc83c60bde26d18ed68f11564b228" } +dylint_linting = "6.0" + +[dev-dependencies] +dylint_testing = "6.0" + +[package.metadata.rust-analyzer] +rustc_private = true diff --git a/tools/dylints/mutex_lock_must_unwrap/README.md b/tools/dylints/mutex_lock_must_unwrap/README.md new file mode 100644 index 000000000..65a60b596 --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/README.md @@ -0,0 +1,22 @@ +# `mutex_lock_must_unwrap` + +### What it does + +Requires every `std::sync::Mutex::lock()` call to be immediately followed by `unwrap()`. + +### Why is this bad? + +This codebase treats a poisoned standard mutex as a programming error. Continuing after poisoning +can expose state left inconsistent by a panic while the mutex was held. + +### Example + +```rust +let guard = mutex.lock().expect("mutex poisoned"); +``` + +Use instead: + +```rust +let guard = mutex.lock().unwrap(); +``` diff --git a/tools/dylints/mutex_lock_must_unwrap/rust-toolchain b/tools/dylints/mutex_lock_must_unwrap/rust-toolchain new file mode 100644 index 000000000..39b925b85 --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/rust-toolchain @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2026-05-28" +components = ["llvm-tools-preview", "rustc-dev"] diff --git a/tools/dylints/mutex_lock_must_unwrap/rustfmt.toml b/tools/dylints/mutex_lock_must_unwrap/rustfmt.toml new file mode 100644 index 000000000..b196eaa2d --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces = 2 diff --git a/tools/dylints/mutex_lock_must_unwrap/src/lib.rs b/tools/dylints/mutex_lock_must_unwrap/src/lib.rs new file mode 100644 index 000000000..c21d6fd1e --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/src/lib.rs @@ -0,0 +1,105 @@ +// shared-core - bitdrift's common client/server libraries +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +#![feature(rustc_private)] +#![warn(unused_extern_crates)] + +extern crate rustc_hir; +extern crate rustc_span; + +use clippy_utils::{ + diagnostics::span_lint, + get_parent_expr, + res::MaybeDef, + sym::{lock, unwrap}, +}; +use rustc_hir::{Expr, ExprKind, def::Res}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_span::symbol::sym; + +dylint_linting::declare_late_lint! { + /// ### What it does + /// + /// Checks that `std::sync::Mutex::lock()` is immediately followed by `unwrap()`. + /// + /// ### Why is this bad? + /// + /// This workspace treats a poisoned standard mutex as a programming error. Handling its + /// `LockResult` in another way can accidentally continue with poisoned state. + /// + /// ### Example + /// + /// ```rust + /// # use std::sync::Mutex; + /// # let mutex = Mutex::new(()); + /// let guard = mutex.lock().expect("mutex poisoned"); + /// ``` + /// + /// Use instead: + /// + /// ```rust + /// # use std::sync::Mutex; + /// # let mutex = Mutex::new(()); + /// let guard = mutex.lock().unwrap(); + /// ``` + pub MUTEX_LOCK_MUST_UNWRAP, + Warn, + "`std::sync::Mutex::lock()` must be immediately unwrapped" +} + +impl<'tcx> LateLintPass<'tcx> for MutexLockMustUnwrap { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if !is_std_mutex_lock(cx, expr) || is_immediately_unwrapped(cx, expr) { + return; + } + + span_lint( + cx, + MUTEX_LOCK_MUST_UNWRAP, + expr.span, + "call `std::sync::Mutex::lock()` as `.lock().unwrap()`", + ); + } +} + +// Resolve the called associated function so aliases, deref coercions, and UFCS calls are handled +// as consistently as ordinary method syntax. +fn is_std_mutex_lock(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let method_def_id = match expr.kind { + ExprKind::MethodCall(method, ..) if method.ident.name == lock => { + cx.typeck_results().type_dependent_def_id(expr.hir_id) + } + ExprKind::Call(function, [_]) => match function.kind { + ExprKind::Path(path) => match cx.qpath_res(&path, function.hir_id) { + Res::Def(_, def_id) if cx.tcx.item_name(def_id) == lock => Some(def_id), + _ => None, + }, + _ => None, + }, + _ => None, + }; + + method_def_id + .and_then(|method_def_id| cx.tcx.impl_of_assoc(method_def_id)) + .is_some_and(|impl_def_id| cx.tcx.type_of(impl_def_id).is_diag_item(cx, sym::Mutex)) +} + +fn is_immediately_unwrapped(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let Some(parent) = get_parent_expr(cx, expr) else { + return false; + }; + let ExprKind::MethodCall(method, receiver, [], _) = parent.kind else { + return false; + }; + + method.ident.name == unwrap && receiver.hir_id == expr.hir_id +} + +#[test] +fn ui() { + dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui"); +} diff --git a/tools/dylints/mutex_lock_must_unwrap/ui/lock_with_expect.rs b/tools/dylints/mutex_lock_must_unwrap/ui/lock_with_expect.rs new file mode 100644 index 000000000..3f83e56ff --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/ui/lock_with_expect.rs @@ -0,0 +1,13 @@ +// shared-core - bitdrift's common client/server libraries +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +use std::sync::Mutex; + +fn main() { + let mutex = Mutex::new(()); + let _guard = mutex.lock().expect("mutex poisoned"); +} diff --git a/tools/dylints/mutex_lock_must_unwrap/ui/lock_with_expect.stderr b/tools/dylints/mutex_lock_must_unwrap/ui/lock_with_expect.stderr new file mode 100644 index 000000000..6b424754d --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/ui/lock_with_expect.stderr @@ -0,0 +1,10 @@ +warning: call `std::sync::Mutex::lock()` as `.lock().unwrap()` + --> $DIR/lock_with_expect.rs:12:18 + | +LL | let _guard = mutex.lock().expect("mutex poisoned"); + | ^^^^^^^^^^^^ + | + = note: `#[warn(mutex_lock_must_unwrap)]` on by default + +warning: 1 warning emitted + diff --git a/tools/dylints/mutex_lock_must_unwrap/ui/lock_without_unwrap.rs b/tools/dylints/mutex_lock_must_unwrap/ui/lock_without_unwrap.rs new file mode 100644 index 000000000..3898bb692 --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/ui/lock_without_unwrap.rs @@ -0,0 +1,13 @@ +// shared-core - bitdrift's common client/server libraries +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +use std::sync::Mutex; + +fn main() { + let mutex = Mutex::new(()); + let _lock_result = mutex.lock(); +} diff --git a/tools/dylints/mutex_lock_must_unwrap/ui/lock_without_unwrap.stderr b/tools/dylints/mutex_lock_must_unwrap/ui/lock_without_unwrap.stderr new file mode 100644 index 000000000..25fcf288d --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/ui/lock_without_unwrap.stderr @@ -0,0 +1,10 @@ +warning: call `std::sync::Mutex::lock()` as `.lock().unwrap()` + --> $DIR/lock_without_unwrap.rs:12:24 + | +LL | let _lock_result = mutex.lock(); + | ^^^^^^^^^^^^ + | + = note: `#[warn(mutex_lock_must_unwrap)]` on by default + +warning: 1 warning emitted + diff --git a/tools/dylints/mutex_lock_must_unwrap/ui/main.rs b/tools/dylints/mutex_lock_must_unwrap/ui/main.rs new file mode 100644 index 000000000..52ad8228f --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/ui/main.rs @@ -0,0 +1,13 @@ +// shared-core - bitdrift's common client/server libraries +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +use std::sync::Mutex; + +fn main() { + let mutex = Mutex::new(()); + let _guard = mutex.lock().unwrap(); +} diff --git a/tools/dylints/mutex_lock_must_unwrap/ui/non_std_mutex.rs b/tools/dylints/mutex_lock_must_unwrap/ui/non_std_mutex.rs new file mode 100644 index 000000000..d6554cd46 --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/ui/non_std_mutex.rs @@ -0,0 +1,16 @@ +// shared-core - bitdrift's common client/server libraries +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +struct Mutex; + +impl Mutex { + fn lock(&self) {} +} + +fn main() { + Mutex.lock(); +} diff --git a/tools/dylints/mutex_lock_must_unwrap/ui/ufcs_lock_without_unwrap.rs b/tools/dylints/mutex_lock_must_unwrap/ui/ufcs_lock_without_unwrap.rs new file mode 100644 index 000000000..315b736c0 --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/ui/ufcs_lock_without_unwrap.rs @@ -0,0 +1,13 @@ +// shared-core - bitdrift's common client/server libraries +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +use std::sync::Mutex; + +fn main() { + let mutex = Mutex::new(()); + let _lock_result = Mutex::lock(&mutex); +} diff --git a/tools/dylints/mutex_lock_must_unwrap/ui/ufcs_lock_without_unwrap.stderr b/tools/dylints/mutex_lock_must_unwrap/ui/ufcs_lock_without_unwrap.stderr new file mode 100644 index 000000000..18aa1bcbc --- /dev/null +++ b/tools/dylints/mutex_lock_must_unwrap/ui/ufcs_lock_without_unwrap.stderr @@ -0,0 +1,10 @@ +warning: call `std::sync::Mutex::lock()` as `.lock().unwrap()` + --> $DIR/ufcs_lock_without_unwrap.rs:12:22 + | +LL | let _lock_result = Mutex::lock(&mutex); + | ^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(mutex_lock_must_unwrap)]` on by default + +warning: 1 warning emitted +