Skip to content

feat(lib): transport base class that implements shared ownership of transport - #336

Open
wharwood wants to merge 11 commits into
mainfrom
wh/instro-512-featlib-shared-transport-ownership-so-combined-instruments
Open

feat(lib): transport base class that implements shared ownership of transport#336
wharwood wants to merge 11 commits into
mainfrom
wh/instro-512-featlib-shared-transport-ownership-so-combined-instruments

Conversation

@wharwood

@wharwood wharwood commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds OwnershipContex TransportBase, a new base class in instro/lib/transports/ownership.pytransport_base.py that lets multiple drivers share one VisaDriver or ModbusDriver connection via open()/close(): the first open() opens the connection and reports first owner status so one-time device setup (e.g. taking a combined instrument's remote lock) runs exactly once, and the connection stays open until the last close() frees it, running an optional teardown callback in the right order before the socket closes. Both VisaDriver and ModbusDriver now inherit this base instead of duplicating lock/lifecycle bookkeeping, with a guarded public close() that declines (and logs) while any owner remains and a del GC backstop that bypasses the guard to avoid stranding a resource. This unblocks combined instruments like the EA PSB 10000 series, where a PSU-shaped driver and an ELoad-shaped driver need to share one physical connection without either surface prematurely closing it out from under the other. Closes INSTRO-512.

Type of change

  • Bug fix (fix)
  • New feature (feat)
  • Breaking change (feat! / fix!)
  • Refactor (refactor)
  • Documentation (docs)
  • Chore / tooling (chore)

Verification

This was tested with the added unit tests and mock transports.

Tests

  • Unit tests added or updated
  • Existing tests cover this change
  • No tests — explain why:

Checklist

  • PR title follows Conventional Commits (e.g. feat(driver): add support for Keysight E36300)
  • I have read CONTRIBUTING.md
  • Documentation updated if user-facing behavior changed
  • Code follows the style/conventions of the surrounding code

Notes for reviewers

This is the bases for drivers that will share a transport but implement more than one base instrument. It should be thoroughly discussed prior to merge.

@wharwood
wharwood requested a review from hoehner August 1, 2026 02:53
@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Claude finished @wharwood's task in 3m 5s —— View job


Review complete

  • Read trigger request and repo conventions (AGENTS.md)
  • Review diff against origin/main
  • Check ownership.py design against base-class scope rules
  • Check VisaDriver/ModbusDriver changes
  • Check tests
  • Check docs sync
  • Post inline comments

Overview

OwnershipContext is a justified extraction, not premature abstraction: VisaDriver and ModbusDriver previously duplicated the exact same holder-lock/close()/__del__ bookkeeping verbatim, and this PR unifies only that — it doesn't reach into device I/O or SCPI/Modbus semantics, so it doesn't collide with the repo's "no shared _check_errors/_write_checked" stance. The acquire()/release() contract (first-owner setup, last-release teardown ordering, guarded close(), __del__ GC backstop bypassing the guard) is coherent and the new AGENTS.md bullets correctly capture the two sharp edges (must call super().__init__(); must release() before propagating a failed post-acquire setup). Test coverage is solid — both transports get symmetric acquire/release/close/__del__ cases, plus a genuine end-to-end combined-instrument test (test_shared_transport.py) asserting teardown ordering (SYST:LOCK OFF before socket close).

Two inline nits posted:

  • A multi-line Example:: docstring on lock() lands in the new ownership.py file — worth trimming to one line per the INSTRO-248 convention even though it's relocated content.
  • The visa.mdx worked example omits the release-before-raise pattern that this same PR's new AGENTS.md bullet mandates — readers copying the doc snippet would miss the stranded-holder pitfall that test_shared_transport.py's stub correctly guards against.

No breaking-change concerns: existing non-shared callers never populate _holders, so close()/__del__ behavior is unchanged for them.

@mintlify

mintlify Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
instro 🟢 Ready View Preview Aug 1, 2026, 2:55 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

The PR introduces TransportBase to centralize transport locking, lifecycle management, and reference-counted ownership for shared VISA and Modbus connections.

  • Migrates VisaDriver and ModbusDriver onto the shared lifecycle implementation.
  • Preserves sessions acquired reentrantly during final-owner teardown callbacks.
  • Adds shared-ownership, teardown-ordering, exception-path, and transport-specific tests.
  • Documents transport lifecycle, extension contracts, and combined-instrument usage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported reentrant callback issue is fixed by retaining the session whenever the callback registers a new holder.

Important Files Changed

Filename Overview
instro/lib/transports/transport_base.py Adds the common locking and shared-ownership lifecycle, including the guard that preserves a session acquired reentrantly during final-release teardown.
instro/lib/transports/visa.py Migrates VISA session opening and teardown to the new transport lifecycle hooks.
instro/lib/transports/modbus.py Migrates Modbus connection management and locking to TransportBase.
tests/lib/test_shared_transport.py Exercises shared VISA ownership across combined logical instrument drivers.
tests/lib/test_visa_driver.py Covers holder accounting, callback ordering, callback failures, and preservation of reentrant holder acquisition.
tests/lib/test_modbus_driver.py Verifies shared ownership and lifecycle behavior for Modbus transports.

Sequence Diagram

sequenceDiagram
    participant A as Driver A
    participant B as Driver B
    participant T as TransportBase
    participant S as Session
    A->>T: open(A)
    T->>S: _open_session()
    T-->>A: "first owner = true"
    B->>T: open(B)
    T-->>B: "first owner = false"
    A->>T: close(A)
    Note over T,S: B still owns session
    B->>T: close(B, callback)
    T->>T: on_last_release()
    alt callback acquires a holder
        T->>T: open(new holder)
        Note over T,S: Preserve session for new holder
    else no new holder
        T->>S: _teardown_session()
    end
Loading

Reviews (2): Last reviewed commit: "docs(transports): add a Transports overv..." | Re-trigger Greptile

Comment thread instro/lib/transports/ownership.py Outdated
Comment on lines +47 to +49
self._teardown_session()

def close(self) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Reentrant callback invalidates ownership

When on_last_release directly or indirectly calls acquire() on the same transport, the reentrant lock allows a new holder to be registered before the finally block unconditionally closes its session, leaving that holder with stale ownership and failing subsequent I/O.

Prompt To Fix With AI
This is a comment left during a code review.
Path: instro/lib/transports/ownership.py
Line: 47-49

Comment:
**Reentrant callback invalidates ownership**

When `on_last_release` directly or indirectly calls `acquire()` on the same transport, the reentrant lock allows a new holder to be registered before the `finally` block unconditionally closes its session, leaving that holder with stale ownership and failing subsequent I/O.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread instro/lib/transports/ownership.py Outdated
Comment on lines +57 to +68
def lock(self) -> threading.RLock:
"""Return the reentrant resource lock for atomic multi-step sequences.

Example::

with driver.lock():
driver.write("CONF:VOLT:DC")
driver.write("RANGE 10")
value = driver.query("READ?")

Reentrant: the holding thread can call write/query/read inside the with.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Multi-line docstring (with an Example:: block) landing in a brand-new file — AGENTS.md's one-short-line-max rule (INSTRO-248) applies here even though the content was relocated from VisaDriver.lock(). Worth trimming to one line while it's moved.

Comment on lines +72 to +74
def open(self) -> None:
if self._visa.acquire(self): # True only for the first owner
self._visa.write("SYST:LOCK ON") # one-time device setup

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This worked example's open() doesn't show the release-before-raise pattern that the new AGENTS.md bullet ("A driver whose post-acquire device setup raises must release(self) before propagating") mandates — tests/lib/test_shared_transport.py's _SharedPSUDriver wraps the write in try/except and releases on failure, but this doc snippet doesn't. Readers copying this example would miss the stranded-holder pitfall.

@wharwood

wharwood commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@greptile @claude can we rereview this code?

@wharwood wharwood changed the title feat(lib): shared transport ownership for combined instruments feat(lib): transport base class that implements shared ownership of transport Aug 3, 2026
hoehner
hoehner previously approved these changes Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants