Skip to content

Storage

Eugene Palchukovsky edited this page May 16, 2026 · 3 revisions

Storage

OpenPit policies often need internal state — accumulated P&L, rate-limit timestamps, reserved margin, position counters. The Storage abstraction lets that state work transparently in any synchronization environment the SDK supports, with zero overhead when none is needed and full thread-safety when the embedding requires it.

This page documents the abstraction, the available synchronization policies, and how to consume it from each SDK.

Why a dedicated abstraction

OpenPit supports embeddings where policy state stays on one thread, moves between threads sequentially, or is accessed concurrently. A custom policy storing state in a plain map handles only the single-thread case; the moment calls cross threads - even sequentially - the policy needs a mechanism that establishes happens-before ordering between writes on one thread and reads on another. Storage provides that ordering without making the policy author design cross-thread coordination.

Across the language bindings, every storage created by a policy is account-keyed: the top-level key carries the AccountId. The per-account value can be any structure the policy needs (counter, sliding-window log, nested map). Sharding by account is part of the public storage contract - the chosen synchronization mode is independent from the key-shape rule.

Cooperative scheduling adds another wrinkle. A scheduled task may resume on a different OS thread than the one it suspended on. This is not necessarily multithreading in the classical sense - calls may still be sequential at the host layer - but it still requires the same happens-before guarantee. The engine handle itself carries the matching capability through the selected sync policy: Full sync is the mode for thread-shareable handles, Account sync supports sequential cross-thread invocation, and Local sync keeps the handle single-threaded.

Storage solves the policy-state side of these cases with one abstraction:

  • the synchronization policy is selected once when the storage is created;
  • every read or write goes through a scoped access handle that owns the appropriate access rights for the duration of access;
  • the user code only asks the storage to read or write a key;
  • when no synchronization is needed, the no-sync policy compiles down to direct map access - zero overhead.

Synchronization policies

The storage picks one of three built-in policies at construction time. The policy is part of the storage's type, so runtime overhead matches the chosen guarantees and nothing more.

No-sync policy

The host keeps every call on one thread. The storage performs no cross-thread coordination; sharing it across threads is rejected at the language level. This is the lowest-overhead choice - operations compile down to direct lookups.

Full-sync policy

Any thread may enter for any key in any order. The storage serializes reads and writes so the state stays consistent under concurrent load. This is the safe default when the host does not constrain the call pattern.

Account sync

The caller commits to processing each account through a single chain: one queue or one worker at a time. The same account must never be processed by two threads in parallel; different accounts may run concurrently with no coordination.

The storage coordinates the boundary between adding or removing accounts and accessing per-account state, but does not serialize the per-account access itself - the single-chain-per-account discipline does. Throughput scales with the number of independent accounts.

Custom

The Rust API exposes the policy abstraction so that custom synchronization policies can be plugged in - for example, a sharded coordination strategy or a lock-free data structure.

The cost model differs between direct SDK use and the language bindings:

  • Direct Rust - No-sync policy compiles down to zero synchronization overhead; Account sync adds one lock acquire on the index per access; Full sync adds two lock acquires (index and values) per access. The engine handle adds an atomic handle reference only for sync modes that produce a Send Rust handle (Full and Account); No-sync uses a non-atomic handle reference.
  • Language bindings - every mode goes through a thread-portable engine handle and a runtime sync-mode dispatch. The fixed binding overhead is small (a single atomic handle reference plus a runtime dispatch) compared to the per-mode storage cost above.

In all three modes, business-logic costs (policy evaluation, map operations) dominate the per-call wall time. The locking overhead matters only at very high throughput.

Engine integration

Each engine builder owns a storage builder whose policy type matches the engine's synchronization policy. Pass a reference to that storage builder to your custom policy's constructor; the custom policy creates one storage per internal table and keeps it for the lifetime of the engine.

Application code cannot create a StorageBuilder directly. The engine is the only owner of the builder, and policies that need storage must receive builder.storage_builder() from the engine builder during initialization.

The policy is fixed centrally once, when the engine is configured — every storage created from that builder shares the same locking guarantees.

Example: Custom Policy with Storage

Rust
use openpit::param::{AccountId, Asset, Pnl};
use openpit::storage::{LockingPolicyFactory, Storage, StorageBuilder};

pub struct MyPolicy<StorageLockingPolicyFactory>
where
    StorageLockingPolicyFactory: LockingPolicyFactory,
{
    realized: Storage<
        (AccountId, Asset),
        Pnl,
        StorageLockingPolicyFactory::Policy,
    >,
}

impl<StorageLockingPolicyFactory> MyPolicy<StorageLockingPolicyFactory>
where
    StorageLockingPolicyFactory: LockingPolicyFactory
        + openpit::storage::CreateStorageFor<(AccountId, Asset)>,
{
    pub fn new(
        storage_builder: &StorageBuilder<StorageLockingPolicyFactory>,
    ) -> Self {
        Self {
            realized: storage_builder.create(),
        }
    }

    pub fn record_pnl(
        &self,
        account: AccountId,
        settlement: Asset,
        delta: Pnl,
    ) {
        self.realized.with_mut(
            (account, settlement),
            || Pnl::ZERO,
            |entry, _is_new| {
                if let Ok(updated) = entry.checked_add(delta) {
                    *entry = updated;
                }
            },
        );
    }

    pub fn current_pnl(
        &self,
        account: AccountId,
        settlement: &Asset,
    ) -> Pnl {
        let key = (account, settlement.clone());
        self.realized
            .with(&key, |entry| *entry)
            .unwrap_or(Pnl::ZERO)
    }
}

The built-in policy types are NoLocking, FullLocking, and IndexLocking. Access is closure-based: with provides read-only scoped access, with_mut provides read/write scoped access with on-demand insertion, and remove deletes an entry. The access scope ends when the closure returns. Storage is intentionally not Clone - share it through an explicit owner type or borrow &Storage<...>.

Example: Engine-Owned Builder Use

Rust
use openpit::Engine;

let builder = Engine::<(), ()>::builder().full_sync();
let counters = builder.storage_builder().create::<&'static str, u64>();

counters.with_mut("ticks", || 0, |value, _is_new| {
    *value += 1;
});

assert_eq!(counters.with(&"ticks", |value| *value), Some(1));
assert!(counters.remove(&"ticks"));

Choosing a policy

  • Caller does not drive the engine from multiple threads → no-sync policy. It is the cheapest mode and adds no synchronization overhead by default.
  • Shared engine, no scheduling guarantees from the host → full-sync policy. Safe default.
  • Shared engine, each account is pinned to a single chain (queue or worker) → account sync. Trades a correctness obligation for unbounded per-account parallelism.
  • Anything else → custom policy passed through the engine builder.

Related Pages

  • Threading Contract: Per-call execution model the storage operates under
  • Policies: Built-in policies that already use storage internally
  • Policy API: Language-specific interfaces for custom policies

Clone this wiki locally