Skip to content

Threading Contract

Eugene Palchukovsky edited this page Aug 1, 2026 · 8 revisions

Threading Contract

Canonical threading contract for OpenPit SDK handles across language bindings.

The SDK never spawns OS threads. Every public method executes on the OS thread that invoked it. The engine handle's threading capability follows from the chosen sync policy:

  • Full sync - concurrent invocation on the same handle is safe, including calls for the same account. Locks protect individual storage accesses rather than serializing an entire engine call, so same-account pipelines may interleave between accesses.
  • No sync - the handle stays on the OS thread that created the engine.
  • Account sync - concurrent invocation on the same handle from multiple threads is safe iff the caller guarantees that calls for the same account are never concurrent (sharded workers, one channel per account hash, or any equivalent pinning scheme). Without that guarantee, concurrent invocation is undefined behavior. Sequential cross-thread invocation is always safe. The Go and C++ SDKs ship optional Async Engine helpers that implement such per-account dispatch.

The pure-Rust AccountSyncEngine handle is deliberately Send + !Sync, so concurrent invocation through one handle is rejected by the type system. The caller-sharded rule above describes binding handles and dispatch adapters that provide their own per-account routing. It is not a hidden serialization guarantee supplied by FullSync.

Selecting A Sync Policy In Each SDK

The policy is chosen once, when the engine is built, and cannot change afterwards.

  • Go - NoSync(), FullSync(), or AccountSync() on the engine builder. Goroutines migrate between OS threads by default, which the SDK supports on every policy; see Thread Migration And Callbacks.
  • Python - no_sync(), full_sync(), or account_sync() on the engine builder. Public methods acquire the GIL when needed, and the SDK does not release it across callback boundaries, so Python policies always execute on the calling thread. Synchronous code and an event loop pinned to one thread are both satisfied by no-sync; a synchronizing policy is needed only when the engine is genuinely shared across OS threads.
  • JavaScript - no selection is offered. The WebAssembly engine is single-threaded and always uses no-op locking, the engine and its handles cannot be transferred to another thread, there is no async engine, and every policy callback runs synchronously on the calling thread. Use one engine instance per worker or isolate when parallelism is needed.
  • C++ - openpit::SyncPolicy::None, openpit::SyncPolicy::Full, or openpit::SyncPolicy::Account, passed to the engine builder.
  • Rust - no_sync(), full_sync(), or account_sync() on the engine builder.

Drop Copy And Implicit Cleanup

Drop-copy apply and finalization do not turn a FullSync policy pipeline into one isolated critical section. Eager policy state may be observed by an interleaving same-account call before an evaluation failure compensates it or before the caller finalizes an accepted operation. Integrations that need pipeline isolation must serialize the whole same-account operation lifetime externally or use the Go or C++ async engine. Those helpers pin apply and every explicit commit, rollback, or close to the account queue, and they refuse an order with no readable account instead of queueing it.

Implicit cleanup is the exception, for reservations and drop-copy operations alike: neither async wrapper has a destructor hook, so the rollback that runs when the last owner is released happens on that owner's thread rather than on the account queue. Call an explicit Close-flavored method to keep finalization in the lane. See Async Engine.

That off-lane rollback still runs mutation finalizers, and a finalizer has no right to fail on any thread. One that fails there has no caller left to report to, so the engine kill switch is the only signal; see Account Blocking - Mutation Finalizer Contract.

Thread Migration And Callbacks

Runtime migration of the caller between OS threads during one SDK call is supported (a goroutine moving across worker threads, a coroutine resuming on a different thread than it suspended on). Callbacks invoked by the SDK back into host code may run on a different OS thread than the caller, so callback code must not rely on thread-local OS state.

User Data Ownership

The user data fields on Reject, Order, ExecutionReport, and AccountAdjustment are opaque caller tokens. The SDK never inspects, dereferences, or frees them. Their lifetime, thread-safety, and meaning are entirely the caller's responsibility.

Policy State

Custom policies that need internal state across calls should use Storage - the synchronization-aware key-value abstraction that matches the engine's sync policy automatically and removes the need for external locking around policy state.

Clone this wiki locally