Dragon Data Backend¶
DragonDataBackend constructs and owns a dragon.data.ddict.DDict and hands back a DragonEndpoint — an opaque, serialized descriptor. It never constructs a Dragon client itself.
Overview¶
- Constructing a
DDictis itself the blocking startup call — it spins up the orchestrator and manager processes, with no separate "start" step;DragonDataBackendruns that construction in a thread so it doesn't block the event loop. wait_for_keysis always forced toTrue— downstream clients (RADEX's compiledDragonClient, and nativeDDict.__getitem__) rely on a blocking get, not an immediateKeyError, when a key hasn't landed yet.- All other
DDictconstruction arguments are accepted as arbitrary keyword arguments and forwarded toDDict(...)unchanged — this class does not mirror or re-validateDDict.__init__'s own parameter list. If you pass somethingDDictdoesn't like,DDictraises its own error.
Installation¶
Prerequisites
Requires the Dragon runtime, and scripts must be launched through it:
Basic Usage¶
import asyncio
import logging
import rhapsody
from rhapsody.api import ComputeTask
from rhapsody.api import Session
from rhapsody.backends import DragonExecutionBackend
from rhapsody.backends.data import DragonDataBackend
rhapsody.enable_logging(level=logging.INFO)
# NOTE: task functions below are each fully self-contained. A task may be
# invoked in a completely separate process/node with no knowledge of this
# module or anything else defined here -- every import and every bit of
# setup a task needs must live inside that task's own function body, never
# factored into a shared helper or relying on driver-scope state. The only
# input a task gets is whatever is explicitly passed as an argument
# (`descriptor`, here) -- DragonDataBackend hands that back from
# `.start()`, it never constructs a client itself. Unlike Redis, the
# Dragon client takes the descriptor directly as a constructor argument --
# no environment variables involved.
# func1 (producer) and func2 (consumer) are submitted together, with no
# ordering guarantee between them -- func2 uses wait_for_*, not get_*, so
# it correctly blocks until func1's data actually lands instead of racing
# it.
def func1(descriptor):
import numpy as np
from radex.clients.core import DragonClient
from radex.handles.handles import OutgoingHandle
client = DragonClient(descriptor=descriptor, timeout=5)
samples = np.arange(10, dtype=np.float64) ** 2 # [0, 1, 4, 9, ..., 81]
client.put_tensor(OutgoingHandle("samples"), samples)
client.put_scalar(OutgoingHandle("sample-count"), len(samples))
return len(samples)
def func2(descriptor):
from radex.clients.core import DragonClient
from radex.handles.handles import IncomingHandle
client = DragonClient(descriptor=descriptor, timeout=5)
samples = client.wait_for_tensor(IncomingHandle("samples"), 10)
count = client.wait_for_scalar(IncomingHandle("sample-count"), 10)
return {"count": int(count), "sum": float(samples.sum()), "mean": float(samples.mean())}
async def main():
session = Session()
exec_backend = await DragonExecutionBackend()
# RHAPSODY owns launching the Dragon DDict; RADEX only ever sees the
# resulting endpoint, never the launch mechanism.
data_backend = await DragonDataBackend(managers_per_node=1, n_nodes=1)
session.add_backend(exec_backend)
session.add_backend(data_backend)
descriptor = data_backend.endpoints[0].serialize()
tasks = [
ComputeTask(function=func1, args=(descriptor,)),
ComputeTask(function=func2, args=(descriptor,)),
]
futures = await session.submit_tasks(tasks)
await asyncio.gather(*futures)
for task in tasks:
print(f"Task {task.uid} in {task.state} state.")
print(f"Output: {task.return_value}")
await session.close()
if __name__ == "__main__":
asyncio.run(main())
Output
The full runnable file lives at examples/data/01-producer-consumer-dragon-radex.py.
Configuration Options¶
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
"dragon" |
Name this backend is registered under when attached to a Session |
**ddict_kwargs |
— | — | Forwarded directly to dragon.data.ddict.DDict(...) -- e.g. managers_per_node, n_nodes, trace. See the Dragon DDict reference for the full set |
working_set_size |
int (via **ddict_kwargs) |
2 |
Not DDict's own default of 1 -- that's incompatible with the wait_for_keys=True this class always forces. Override explicitly if you need something else |
wait_for_keys cannot be passed via **ddict_kwargs — the class hard-enforces True and raises ValueError if you try to override it (RADEX's compiled client refuses to attach to a DDict built with wait_for_keys=False, a constraint DDict itself has no way to know about).
Using a native client¶
DragonEndpoint.serialize() returns the same serialized descriptor DDict.attach() expects — no RADEX involved:
from dragon.data.ddict import DDict
client = DDict.attach(descriptor, timeout=10)
client["key"] = value # numpy arrays and other picklables work directly
value = client["key"] # blocks until the key exists, since wait_for_keys=True
client.detach() # releases this client's resources; does NOT destroy the dict
See examples/data/03-producer-consumer-dragon-native.py for the full pattern.