Skip to content

[RFC] Slice lifecycle has no single authority — create/track/remove is re-implemented per consumer and races #3715

Description

@K5PTB

Summary

This is an RFC on slice lifecycle management. AetherSDR has no single authority for slice create / track /
remove: the same lifecycle is hand-rolled independently by many consumers, with divergent behavior and
recurring create/close race conditions. This has been a long-standing source of fragility (several
candidate issues are listed at the end). The goal here is to agree on a unified approach to slice
lifecycle before anyone implements it — feedback on scope and design is what's wanted.

We surfaced the full extent of this while implementing and testing CAT split VFO (#3702, #3703), and a unified
solution would also resolve the split-VFO problems we found there. But the problem is general — it spans the
GUI, DAX, TCI, and the CAT protocols alike.

The problem — no single authority for slice lifecycle

A survey of slice create / remove / track shows the same do-it-yourself pattern repeated everywhere, with no
component that owns "a slice exists / is mine / is gone":

  • Creation — 8+ sites, two classes. Some go through RadioModel::addSlice()/addSliceOnPan() (which
    discards the radio's create-ack id, RadioModel.cpp:1595); several others bypass it entirely with raw
    slice create command strings
    (MainWindow_Wiring.cpp:2860, MainWindow_Controllers.cpp:445 & :968,
    MainWindow_Shortcuts.cpp:780, plus RadioModel.cpp:5485) — so they get neither the id nor uniform
    maxSlices enforcement.
  • Removal — raw slice remove N strings scattered across MainWindow.cpp:6125,
    MainWindow_Wiring.cpp:2359 & :2729, with no single teardown path.
  • Lifecycle reaction — 15+ independent sliceAdded/sliceRemoved subscribers (TciServer, DaxApplet,
    RxApplet, TciApplet, MainWindow_Session ×3, MainWindow_DigitalModes, MainWindow_DspApplets,
    RadioSetupDialog, pan-follow at MainWindow.cpp:8320, …), each re-deriving which slices matter to it
    and separately handling out-of-band create/remove (Multi-Flex / front-panel / rigctld — see the explicit
    "removed out-of-band" handling at MainWindow_Wiring.cpp:733).
  • Identity held by raw pointer — holding a SliceModel* across async time is hand-guarded with
    QPointer<SliceModel> independently in 8+ sites (TciServer.cpp, FlexControlDialog, KiwiSdrApplet,
    Ax25HfPacketDecodeDialog, AgcTCalibrator, AgcCalibrationDialog, FreeDvReporterDialog,
    SpectrumOverlayMenu). One such raw-pointer-across-time bug was the singleShot-timer UAF fixed in 67d3d23.

That absence of an owner is the shared root of the create/close races, the DAX/TCI ownership gap (#3305), and
the out-of-band-change handling sprinkled through the GUI.

Fragmented solutions — the same lifecycle, three different ways

Split VFO is the sharpest example: the "create a transient slice → figure out which one I just made → track
it → remove it on teardown" pattern is implemented three times, each with its own identity heuristic:

  1. rigctldsrc/core/RigctlProtocol.cpp: a deferred-promotion state machine. set_split_vfo/set_freq VFOB arms m_pendingSplitEnable (RigctlProtocol.h:120) and tryPromoteTxSlice()
    (RigctlProtocol.cpp:932) reconciles once the new slice appears asynchronously.
  2. SmartCAT (Flex/TS-2000)src/core/SmartCatProtocol.cpp: a flag (m_splitEnabled,
    SmartCatProtocol.h:167) plus reuse of the operator-configured VFO B slice. (Create-on-demand was
    deliberately not implemented here precisely because of this fragility — see Flex/TS-2000 CAT (SmartCAT) behavior diverges from SmartSDR for Mac & Windows #3702 DAX audio channels and Hamlib/rigctld interface for digital mode apps #7.)
  3. GUI splitsrc/gui/MainWindow.cpp: m_splitActive / m_splitTxSliceId / m_splitRxSliceId, where
    the created slice is identified by a heuristic in the slice-added handler (MainWindow.cpp:5988:
    "first new non-RX slice while split active and no TX slice yet"), cleared on slice-removed, and torn down
    with slice remove (MainWindow.cpp:6125).

(For reference, TCI is not a fourth creator — it never calls create/remove; it is a lifecycle consumer
plus a QPointer holder, both counted above. Its split_enable SET is a no-op, TciProtocol.cpp:746:
"not fully controllable from TCI (would need to create a second slice)" — i.e. a feature blocked by the
missing shared create path, #1686.)

Root cause

RadioModel::addSlice() is fire-and-forget — it emits slice create and does not return the created
slice's id (the radio acks it asynchronously). So every caller reconstructs "which slice is mine" by
snapshot/diff or a positional heuristic, which is inherently racy when create/remove overlap. The raw-string
create/remove sites don't even go through addSlice, so there is no chokepoint at which to fix this once.

Concrete failures we observed

These surfaced while testing split VFO (the failures landed on the rigctld / SmartCAT paths), but they are
symptoms of the lifecycle gap, not of split specifically:

  • The deferred-promotion machine could hang when the radio was at slice capacity — set_split_vfo 1
    armed a create that would never land (fixed narrowly in rigctld CAT behavior diverges from SmartSDR for Mac #3703 bug b with a maxSlices guard).
  • set_split_freq could write to the wrong slice when m_pendingTxSlice wasn't set synchronously (the
    MacLoggerDX race, guarded in rigctld_test 5.5b).
  • The integration tests had to add settle-delays / polling loops around create (poll up to 5 s for the
    slice to appear) and removal (poll for teardown) to avoid races rather than fixing them — i.e. the tests
    work around the lifecycle fragility instead of the fragility being eliminated.

Direction for a unified approach (not yet implemented)

No fix is implemented yet — only a work-in-progress prototype came out of the split-VFO work. The points
below are direction for discussion, not a committed design.

Enabling insight: the radio already returns the new slice's idslice create replies with the slice
index, and RadioModel::addSlice() receives it in its sendCmd callback (RadioModel.cpp:1595,
"new slice created, index = ") then discards it. Every "which slice is mine?" heuristic exists only
because that id is thrown away; surfacing it needs no protocol change.

A unified approach would likely provide:

  • One create/remove chokepoint in RadioModel that surfaces the create-ack id and enforces maxSlices
    once — so every creator stops reinventing it and gets the id directly instead of guessing.
  • A stable identity/handle (id + generation) to retire the scattered raw SliceModel* + QPointer
    guards and close the id-reuse/ABA hole.
  • A normalized lifecycle (created/removed carrying a Local-vs-OutOfBand origin) so the 15+ subscribers
    stop re-deriving who caused a change and the out-of-band cases (Multi-Flex / front panel / external
    controllers) are explicit.
  • Per-slice ownership + one-place teardown so transient slices are removed on disconnect/shutdown, and an
    owner reconciles a slice that vanished out-of-band instead of hanging or double-removing.

It could land incrementally and behavior-neutral until the last step (introduce the chokepoint/handle behind
today's signals, route the bypass sites, then port the per-consumer owners). Beyond removing the duplication,
a unified lifecycle would also fix the split-VFO problems we found — making the polled rigctld_test 5.x
split cases deterministic, unblocking TCI split_enable (#1686), and giving the deferred SmartCAT
create-on-demand (#3702 #7) a safe home.

WIP design sketch (illustrative, not implemented)
// 1. One create/remove chokepoint that surfaces the id (id is already on the wire, today discarded)
void RadioModel::createSlice(const SliceSpec& spec,                 // pan/freq/antenna/mode
                             std::function<void(int sliceId)> onCreated,
                             std::function<void()> onFailed = {});
void RadioModel::removeSlice(int sliceId);
// existing no-arg addSlice() stays as a thin delegate; a CI guard bans raw "slice create"/"slice remove".

// 2. A stable handle — retires raw SliceModel* + QPointer; generation closes ABA/id-reuse
class SliceHandle {                          // value type: { id, generation }
public:
    SliceModel* get(const RadioModel&) const;   // nullptr if gone (or ABA-stale)
    bool        valid(const RadioModel&) const;
    int         id() const;
};

// 3. A registry in RadioModel owning id -> (SliceModel, generation, owner, origin), emitting:
signals:
    void sliceCreated(SliceHandle, SliceOrigin);   // Local | OutOfBand
    void sliceRemoved(SliceHandle, RemoveReason);  // Local | OutOfBand | Shutdown
// ownership tokens: GuiUser | RigctldSplit | SmartCatSplit | Dax | Tci | ...

// 4. Transient owners (split TX, future FX.25/VARA/Winlink/KISS) = a thin state machine over the registry:
//    Idle -> Creating -> Active -> Removing -> Idle
//    - release() during Creating flags remove-on-arrival (vs today's machine that can hang, #3703 bug b)
//    - pre-check maxSlices; treat onFailed/sliceCreateFailed as a clean failure, not an armed create

Concurrency: creators run on protocol/GUI threads; the registry would be the single marshalling point
(mutations queued to the model thread, replacing the ad-hoc QMetaObject::invokeMethod(... QueuedConnection)
at each create site, e.g. RigctlProtocol.cpp:1082).

Open questions: SliceHandle generation source (client counter vs radio token); whether the registry should
also own DAX/TCI stream lifecycle (#3305) or just slices; capacity backpressure across competing owners.

Candidate variants to triage

These open issues may share this slice-lifecycle / slice-identity root cause; each needs triage to confirm
vs. an unrelated slice feature.

Stronger siblings:

Lower confidence:

Metadata

Metadata

Assignees

No one assigned

    Labels

    GUIUser interfaceenhancementImprovement to existing featuremaintainer-reviewRequires maintainer review before any action is takenmulti-panMulti-panadapter layout and slicingpriority: mediumMedium priorityprotocolSmartSDR protocol

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions