Skip to content

Payloads

A payload is the bytes of one entity: an SDF, a PDBQT, a pose. Payloads live in append-only volume files under volumes/, next to project.db. The ms_payloads table says where each one is. Your tables keep only an id:

CREATE TABLE ligands (
    id INTEGER PRIMARY KEY,
    name TEXT,
    structure_payload_id INTEGER REFERENCES ms_payloads(id)
);

ms_flow never needs your schema. It finds the columns that hold payload ids by their REFERENCES ms_payloads(id) foreign key. Moving bytes, for example when compacting, rewrites ms_payloads and never your tables. A payload id never changes.

Write payloads from a job

A task returns Payload objects in place of ids. The sink stores the bytes and writes the ids, in the same transaction as the rows:

from ms_flow.engine import job, task
from ms_flow.payloads import Payload
from ms_flow.sinks import table_sink


@task()
def prepare(name: str) -> dict:
    sdf = build_sdf(name)  # your code
    return {"name": name, "structure_payload_id": Payload(sdf, kind="sdf", series="ligands")}


@job(task=prepare, output=table_sink("ligands", columns=("name", "structure_payload_id")))
def import_ligands(params, project):
    yield from params["names"]

graph_sink works the same way, in any node.

  • series groups payloads with the same lifetime, so they share volumes and compact well together. Use one series per kind of data ("ligands", "poses").
  • codec is "zstd" (the default), "zlib" or "none". Use "none" for data that is already compressed.
  • The payload is compressed when it is pickled, so the compression happens in the worker. The process that owns the project only appends bytes.

When the sink commits, the bytes are already on disk (fdatasync). A crash never leaves a row that points to missing bytes.

Compression dictionaries

A zstd dictionary trained on a series compresses small records much better: 5.9× against 4.3× without it on real SDF records. It is also faster. Train one when a series has enough payloads (10 000 are sampled across the whole series):

project.payloads.train_dictionary("ligands")

A job hands the dictionary to its tasks as a File. Executors without the project's disk carry it once per job:

@task()
def prepare(element: dict) -> dict:
    sdf = build_sdf(element["name"])
    return {"name": element["name"], "structure_payload_id":
            Payload(sdf, series="ligands", dictionary=element["dictionary"])}


@job(task=prepare, output=table_sink("ligands", columns=("name", "structure_payload_id")))
def import_ligands(params, project):
    dictionary = project.payloads.dictionary("ligands")  # None until one is trained
    return [{"name": name, "dictionary": dictionary} for name in params["names"]]

The dictionary id travels inside each zstd frame, so reading needs nothing else. Payloads compressed with an older dictionary stay readable. Compaction moves them to the series' current dictionary.

Read payloads

store = project.payloads
data = store.get(payload_id)            # bytes
found = store.get_many(ids)             # {id: bytes}, read in disk order
for payload_id, data in store.iter_bytes(ids):  # bounded memory, for exports
    ...

A job whose tasks need the bytes uses with_payloads as its source. Each row gets the bytes of its payload under the key you name, so a task never sees a path:

from ms_flow.payloads import with_payloads
from ms_flow.query import QuerySpec, Rows


@job(task=dock, output=...)
def docking(params, project):
    rows = Rows(project, QuerySpec(table="ligands"))
    return with_payloads(project, rows, {"structure_payload_id": "structure"})

Rows are read 500 at a time in disk order, so memory stays bounded however big the table is. A program that only reads paths gets a file in two ways:

  • store.materialize(payload_id, directory, suffix) writes the payload once and returns it as a File. Use it for a receptor shared by every task.
  • scratch_file(data, suffix) is a context manager. It gives a temporary file (in /dev/shm when it has room) and deletes it on leaving.

Corrupt or missing bytes raise PayloadCorrupt, which carries the payload id.

Delete payloads

Delete your rows, then release the payload ids they held:

with sqlite3.connect(project.db_path) as conn:
    ids = [r[0] for r in conn.execute(
        "DELETE FROM ligands WHERE name LIKE 'tmp%' RETURNING structure_payload_id")]
project.payloads.release(ids)

release only drops payloads that no foreign key references, so releasing an id still in use does nothing. A volume left with no live payload is deleted at once, without copying anything.

Maintenance

Call What it does
check(full=False) Returns a Report: missing volumes, references past a file's end, unreferenced payloads, empty volumes, dead share per volume, missing dictionaries, orphan files. full=True also reads and verifies every payload. report.ok means every referenced payload is readable; report.clean also means there is nothing to clean up.
compact(min_dead=0.5, order_by=None) Copies the live payloads of sealed volumes that are at least half dead (or small) into new volumes, then deletes the old ones. Ids do not change. order_by is SQL that returns payload ids in the order you want them next to each other on disk (SELECT structure_payload_id FROM ligands ORDER BY score).
repair() Clears what crashes leave behind: unreferenced payloads, orphan volume files, empty volumes. Never deletes a referenced payload. Returns a new check().

Compaction can run while other processes read. A reader whose volume disappears reads the new location of the payload. To order compaction with your other jobs, submit it as a job:

from ms_flow.payloads import compaction

flow.submit(compaction())

The compaction job waits for jobs writing payloads to finish, and jobs reading payloads wait for it.

On a 100 000-molecule project (225 MB stored), a cold check(full=True) takes 4.3 s. Compaction copies live data at about 34 MB/s, limited by the disk.

Reference

payloads

Per-entity payloads in append-only volumes.

The bytes of each entity (an SDF, a PDBQT, a pose) live in volumes/<id:09d>.vol next to project.db; a row of ms_payloads says where. Apps keep only <role>_payload_id INTEGER REFERENCES ms_payloads(id): moving bytes rewrites ms_payloads and never an app table, and ms_flow never needs the app schema.

Volume: a 32-byte header (MSVOL, version, volume id, series), then needles. Needle: a 16-byte header (crc32, stored length, raw length, codec, flags), then the bytes. A volume has one writer and only grows; a sealed volume never changes.

Each payload says its codec (none, zlib, zstd), so volumes mix them. A Payload compresses itself when it is pickled, i.e. in the worker that made it: the writer only appends bytes. A PayloadGroup stores several payloads as one needle, which compresses them better (the poses of one docking, about half); each part keeps its own row and id, with its place in the needle's raw bytes (part_offset, part_length). A zstd frame names its dictionary (dicts/<id>.zdict, trained per series with PayloadStore.train_dictionary), so reading needs no table.

Maintenance: check reports, release drops payloads nobody references, compact copies the live needles of mostly dead volumes to new ones (recompressing them with the series' current dictionary) and repair clears what crashes leave. Ids never change: compacting rewrites the location of a row, with compare-and-set (I6).

POSIX only (pread/pwritev). Nothing here imports SQL at module level: tasks import Payload in workers.

Classes:

Name Description
Payload

Bytes to store. series groups payloads with the same lifetime in the same volumes.

PayloadStore

Writes and reads the payloads of one project.

Report

What PayloadStore.check found. Only broken and missing_dicts are errors (I5):

PayloadCorrupt

The stored bytes of a payload are missing, truncated or fail their checksum.

Functions:

Name Description
with_payloads

A job's source whose rows carry the bytes of their payloads, so a task gets bytes and never a path.

compaction

PayloadStore.compact as a job of ms_flow, flow.submit(compaction()): it waits for the

scratch_file

data as a temporary file, for a program that only reads paths: in /dev/shm when it is

Payload dataclass

Payload(data: bytes, kind: str = '', series: str = 'default', codec: str = DEFAULT_CODEC, dictionary: str | None = None, raw_length: int | None = None)

Bytes to store. series groups payloads with the same lifetime in the same volumes.

codec is none, zlib or zstd; dictionary is the path of a zstd dictionary (PayloadStore.dictionary(series), a File a job hands its tasks). raw_length is set once data holds the compressed bytes: pickling compresses, so it happens in the worker.

Methods:

Name Description
packed

This payload with data compressed by its codec (itself if it already is).

packed

packed() -> Payload

This payload with data compressed by its codec (itself if it already is).

Source code in src/ms_flow/payloads.py
113
114
115
116
117
118
119
120
def packed(self) -> Payload:
    """This payload with `data` compressed by its codec (itself if it already is)."""
    if self.raw_length is not None:
        return self
    if self.codec not in CODECS:
        raise ValueError(f"unknown codec {self.codec!r}; one of {sorted(CODECS)}")
    return Payload(_compress(self.codec, self.data, self.dictionary), self.kind, self.series, self.codec,
                   None, len(self.data))

PayloadStore

PayloadStore(project: Any)

Writes and reads the payloads of one project.

Methods:

Name Description
put

Store payloads inside the caller's transaction; return their ids, in order: one per

referencing_columns

(table, column) of every app column that holds payload ids.

get_many

Bytes by id. Read in (volume, offset) order; unknown ids raise KeyError.

kinds

The kind each payload was stored with (a format: sdf, pdbqt...), by id; unknown ids are left out.

iter_bytes

(id, bytes) for each id, window ids at a time, each window in (volume, offset) order: to export.

materialize

The payload as the file <directory>/<id><suffix>, written once: what a job hands its

train_dictionary

Train a zstd dictionary on samples payloads spread over series (the head of a

dictionary

The current zstd dictionary of series as a File, or None if it has none yet.

check

What is wrong or left over; full also reads every needle (crc32, codec, dictionary).

release

Delete the payload rows of ids that no app row references (after the app deleted its

compact

Copy the live needles of sealed volumes with at least min_dead dead bytes (or of two or

repair

Clear what check reports as garbage: cut the tails of dead writers, delete unreferenced

close

Close files. Call it with no transaction of this store open.

Source code in src/ms_flow/payloads.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
def __init__(self, project: Any):
    import weakref

    from ms_flow.core.database import ProjectStore, resolve_project_store

    if isinstance(project, (str, os.PathLike)):
        store = ProjectStore.open_cached(ProjectStore.resolve_db_path(project))
    else:
        store = resolve_project_store(project)
    self.engine = store.engine
    self.directory = Path(store.db_path).parent / "volumes"
    self._lock = threading.Lock()  # guards _free and _transactions; never held during I/O
    self._free: dict[str, list[_Volume]] = {}  # active volumes no open transaction is appending to
    self._transactions: dict[Any, _Transaction] = {}  # root SessionTransaction -> its writes
    self._hooked: weakref.WeakSet = weakref.WeakSet()
    self._fds: OrderedDict[int, list] = OrderedDict()  # volume id -> [fd, users, evicted]
    self._fd_lock = threading.Lock()
    self._add_part_columns()
    self._recover()

put

put(session: Any, payloads: Iterable[Payload | PayloadGroup]) -> list[int]

Store payloads inside the caller's transaction; return their ids, in order: one per Payload, one per part of a PayloadGroup (an empty group stores nothing).

The bytes are on disk (fdatasync) when this returns, so the caller's commit never points to lost bytes. If that transaction does not commit, the volumes go back to the size they had before it.

Source code in src/ms_flow/payloads.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
def put(self, session: Any, payloads: Iterable[Payload | PayloadGroup]) -> list[int]:
    """Store `payloads` inside the caller's transaction; return their ids, in order: one per
    `Payload`, one per part of a `PayloadGroup` (an empty group stores nothing).

    The bytes are on disk (fdatasync) when this returns, so the caller's commit never
    points to lost bytes. If that transaction does not commit, the volumes go back to
    the size they had before it.
    """
    payloads = list(payloads)
    if not payloads:
        return []
    connection = session.connection()  # begins the caller's transaction
    tx = self._transaction(session)
    cursor = connection.connection.dbapi_connection.cursor()
    try:
        pending: dict[_Volume, list] = {}
        placed = []
        for payload in payloads:
            sizes = None
            if isinstance(payload, PayloadGroup):
                payload, sizes = payload.packed()
                if not sizes:
                    continue
            payload = payload.packed()  # done already when it came from a worker
            data, codec = payload.data, CODECS[payload.codec]
            size = len(data)
            volume = self._volume_for(tx, cursor, payload.series, NEEDLE + size)
            crc = zlib.crc32(data)
            pending.setdefault(volume, [volume.end, []])[1] += (
                _NEEDLE.pack(crc, size, payload.raw_length, codec, 0),
                data,
            )
            row = (volume.id, volume.end, size, payload.raw_length, codec, crc, payload.kind)
            if sizes is None:
                placed.append((*row, None, None))
            else:
                placed += [(*row, start, length) for start, length in zip(accumulate(sizes, initial=0), sizes)]
            volume.end += NEEDLE + size
        for volume, (offset, buffers) in pending.items():
            try:
                _write_all(volume.fd, buffers, offset)
            except OSError as error:  # a full disk: the caller's rollback cuts what got in
                raise OSError(error.errno, f"cannot write payloads to {self.path(volume.id)}: "
                                           f"{error.strerror}") from error
        _fault("appended")
        for volume in pending:
            _datasync(volume.fd)
            cursor.execute("UPDATE ms_volumes SET size = ? WHERE id = ?", (volume.end, volume.id))
        if tx.dir_dirty:  # a new volume file must survive a crash too
            _fsync_dir(self.directory)
            tx.dir_dirty = False
        _fault("synced")
        ids = []
        for row in placed:
            cursor.execute(
                'INSERT INTO ms_payloads (volume_id, "offset", length, raw_length, codec, crc32, kind,'
                " part_offset, part_length) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
                row,
            )
            ids.append(cursor.lastrowid)
        return ids
    finally:
        cursor.close()

referencing_columns

referencing_columns() -> list[tuple[str, str]]

(table, column) of every app column that holds payload ids.

Read from the schema (PRAGMA foreign_key_list): an app declares REFERENCES ms_payloads(id) and registers nothing with ms_flow.

Source code in src/ms_flow/payloads.py
483
484
485
486
487
488
489
490
491
492
493
494
495
496
def referencing_columns(self) -> list[tuple[str, str]]:
    """(table, column) of every app column that holds payload ids.

    Read from the schema (`PRAGMA foreign_key_list`): an app declares
    `REFERENCES ms_payloads(id)` and registers nothing with ms_flow.
    """
    with self.engine.connect() as conn:
        tables = [r[0] for r in conn.exec_driver_sql("SELECT name FROM sqlite_master WHERE type = 'table'")]
        return [
            (table, fk[3])
            for table in tables
            for fk in conn.exec_driver_sql(f'PRAGMA foreign_key_list("{table}")')
            if fk[2] == "ms_payloads"
        ]

get_many

get_many(ids: Iterable[int]) -> dict[int, bytes]

Bytes by id. Read in (volume, offset) order; unknown ids raise KeyError.

Source code in src/ms_flow/payloads.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
def get_many(self, ids: Iterable[int]) -> dict[int, bytes]:
    """Bytes by id. Read in (volume, offset) order; unknown ids raise KeyError."""
    wanted = list(dict.fromkeys(int(i) for i in ids))
    located = self._locate(wanted)
    missing = [i for i in wanted if i not in located]
    if missing:
        raise KeyError(f"unknown payload ids: {missing[:10]}")
    found, needle, raw = {}, None, b""
    for i in sorted(located, key=lambda i: located[i][:2]):
        location = located[i]
        if location[:2] != needle:  # the parts of a group: one read, one decompression
            needle, raw = location[:2], self._read(i, location)
        found[i] = _part(raw, location)
    return found

kinds

kinds(ids: Iterable[int]) -> dict[int, str]

The kind each payload was stored with (a format: sdf, pdbqt...), by id; unknown ids are left out.

Source code in src/ms_flow/payloads.py
518
519
520
521
522
523
524
525
526
527
def kinds(self, ids: Iterable[int]) -> dict[int, str]:
    """The `kind` each payload was stored with (a format: `sdf`, `pdbqt`...), by id; unknown ids are left out."""
    ids = list(dict.fromkeys(int(i) for i in ids))
    found = {}
    with self.engine.connect() as conn:
        for start in range(0, len(ids), LOCATE_CHUNK):
            chunk = ids[start : start + LOCATE_CHUNK]
            found.update(tuple(row) for row in conn.exec_driver_sql(
                f"SELECT id, kind FROM ms_payloads WHERE id IN ({','.join('?' * len(chunk))})", tuple(chunk)))
    return found

iter_bytes

iter_bytes(ids: Iterable[int], window: int = READ_WINDOW)

(id, bytes) for each id, window ids at a time, each window in (volume, offset) order: to export.

Source code in src/ms_flow/payloads.py
529
530
531
532
533
def iter_bytes(self, ids: Iterable[int], window: int = READ_WINDOW):
    """`(id, bytes)` for each id, `window` ids at a time, each window in (volume, offset) order: to export."""
    ids = iter(ids)
    while batch := list(islice(ids, window)):
        yield from self.get_many(batch).items()

materialize

materialize(payload_id: int, directory: str | PathLike, suffix: str | None = None)

The payload as the file <directory>/<id><suffix>, written once: what a job hands its tasks as a File (a receptor), which executors without the disk carry once per job. Without suffix, the payload's kind is the extension (.pdbqt).

Source code in src/ms_flow/payloads.py
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
def materialize(self, payload_id: int, directory: str | os.PathLike, suffix: str | None = None):
    """The payload as the file `<directory>/<id><suffix>`, written once: what a job hands its
    tasks as a `File` (a receptor), which executors without the disk carry once per job.
    Without `suffix`, the payload's kind is the extension (`.pdbqt`)."""
    from ms_flow.engine.files import File

    if suffix is None:
        kind = self.kinds([payload_id]).get(int(payload_id))
        suffix = f".{kind}" if kind else ""
    path = Path(directory) / f"{int(payload_id)}{suffix}"
    if not path.exists():
        path.parent.mkdir(parents=True, exist_ok=True)
        part = path.with_name(f".{path.name}.{os.getpid()}.{threading.get_ident()}")
        part.write_bytes(self.get(payload_id))
        os.replace(part, path)
    return File(path)

train_dictionary

train_dictionary(series: str, samples: int = DICT_SAMPLES, size: int = DICT_SIZE)

Train a zstd dictionary on samples payloads spread over series (the head of a library is not like the rest of it) and make it the series' current one: a File.

Source code in src/ms_flow/payloads.py
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
def train_dictionary(self, series: str, samples: int = DICT_SAMPLES, size: int = DICT_SIZE):
    """Train a zstd dictionary on `samples` payloads spread over `series` (the head of a
    library is not like the rest of it) and make it the series' current one: a `File`."""
    import random

    from ms_flow.engine.files import File

    with self.engine.connect() as conn:
        ids = [r[0] for r in conn.exec_driver_sql(
            "SELECT p.id FROM ms_payloads p JOIN ms_volumes v ON v.id = p.volume_id WHERE v.series = ?",
            (series,))]
    if len(ids) < 100:
        raise ValueError(f"series {series!r} has {len(ids)} payloads; a dictionary needs a few thousand")
    picked = sorted(random.Random(0).sample(ids, min(samples, len(ids))))
    data = [b for _, b in self.iter_bytes(picked)]
    with self.engine.begin() as conn:
        dict_id = conn.exec_driver_sql(
            "INSERT INTO ms_payload_dicts (series, samples, created_at) VALUES (?, ?, ?)",
            (series, len(data), datetime.now().isoformat(" "))).lastrowid
        trained = _zstd().train_dictionary(size, data, dict_id=dict_id)
        path = self.directory / "dicts" / f"{dict_id}.zdict"
        path.parent.mkdir(parents=True, exist_ok=True)
        part = path.with_name(path.name + ".part")
        with open(part, "wb") as file:
            file.write(trained.as_bytes())
            file.flush()
            os.fsync(file.fileno())
        os.replace(part, path)
        _fsync_dir(path.parent)
    return File(path)

dictionary

dictionary(series: str)

The current zstd dictionary of series as a File, or None if it has none yet.

Source code in src/ms_flow/payloads.py
651
652
653
654
655
656
657
658
def dictionary(self, series: str):
    """The current zstd dictionary of `series` as a `File`, or None if it has none yet."""
    from ms_flow.engine.files import File

    with self.engine.connect() as conn:
        row = conn.exec_driver_sql(
            "SELECT max(id) FROM ms_payload_dicts WHERE series = ?", (series,)).scalar()
    return None if row is None else File(self.directory / "dicts" / f"{row}.zdict")

check

check(full: bool = False) -> Report

What is wrong or left over; full also reads every needle (crc32, codec, dictionary).

Source code in src/ms_flow/payloads.py
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
def check(self, full: bool = False) -> Report:
    """What is wrong or left over; `full` also reads every needle (crc32, codec, dictionary)."""
    report = Report()
    with self.engine.connect() as conn:
        volumes = {r[0]: r[1:] for r in conn.exec_driver_sql("SELECT id, state, size FROM ms_volumes")}
        stats = {r[0]: r[1:] for r in conn.exec_driver_sql(
            f'SELECT volume_id, sum({NEEDLE} + length), max("offset" + {NEEDLE} + length)'
            f" FROM {_NEEDLES} GROUP BY volume_id")}
        report.unreferenced = [r[0] for r in conn.exec_driver_sql(
            f"SELECT id FROM ms_payloads WHERE {self._unreferenced()}")]
        dicts = [r[0] for r in conn.exec_driver_sql("SELECT id FROM ms_payload_dicts")]
    short = {}  # volume id -> its size on disk, when rows point past it
    for volume_id, (state, size) in volumes.items():
        live, end = stats.get(volume_id, (0, HEADER))
        try:
            on_disk = self.path(volume_id).stat().st_size
        except FileNotFoundError:
            if volume_id in stats:
                report.missing_volumes.append(volume_id)
            continue
        if state == "trash" or (state == "sealed" and volume_id not in stats):
            report.empty.append(volume_id)
        elif state == "sealed":
            report.dead[volume_id] = round(1 - live / max(size - HEADER, 1), 4)
        if end > on_disk:
            short[volume_id] = on_disk
    with self.engine.connect() as conn:
        for volume_id in report.missing_volumes:
            for (pid,) in conn.exec_driver_sql("SELECT id FROM ms_payloads WHERE volume_id = ?", (volume_id,)):
                report.broken[pid] = f"volume {volume_id} is missing"
        for volume_id, on_disk in short.items():
            for (pid,) in conn.exec_driver_sql(
                    f'SELECT id FROM ms_payloads WHERE volume_id = ? AND "offset" + {NEEDLE} + length > ?',
                    (volume_id, on_disk)):
                report.broken[pid] = f"past the end of volume {volume_id}"
        if full:
            rows = conn.exec_driver_sql('SELECT id, volume_id, "offset", length, raw_length, codec, crc32'
                                        ' FROM ms_payloads ORDER BY volume_id, "offset"')
            needle, error = None, None
            for pid, *location in rows:
                if tuple(location[:2]) != needle:  # the parts of a group: read once
                    needle, error = tuple(location[:2]), None
                    try:
                        self._read(pid, tuple(location))
                    except Exception as failure:  # a report: whatever stops a read goes in it
                        error = str(failure)
                if error is not None and pid not in report.broken:
                    report.broken[pid] = error
    report.missing_dicts = [d for d in dicts if not (self.directory / "dicts" / f"{d}.zdict").exists()]
    known = set(volumes)
    report.orphans = sorted(volume_id for volume_id in self._volume_files()
                            if volume_id not in known and self._unowned(volume_id))
    return report

release

release(ids: Iterable[int]) -> int

Delete the payload rows of ids that no app row references (after the app deleted its rows); a sealed volume left without rows goes, unread. Returns how many rows went.

Source code in src/ms_flow/payloads.py
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
def release(self, ids: Iterable[int]) -> int:
    """Delete the payload rows of `ids` that no app row references (after the app deleted its
    rows); a sealed volume left without rows goes, unread. Returns how many rows went."""
    ids = list(dict.fromkeys(int(i) for i in ids))
    guard = self._unreferenced()
    released = 0
    with self.engine.begin() as conn:
        for start in range(0, len(ids), LOCATE_CHUNK):
            chunk = ids[start : start + LOCATE_CHUNK]
            released += conn.exec_driver_sql(
                f"DELETE FROM ms_payloads WHERE id IN ({','.join('?' * len(chunk))}) AND {guard}",
                tuple(chunk)).rowcount
        self._trash_empty(conn)
    self._empty_trash()
    return released

compact

compact(min_dead: float = 0.5, small: int | None = None, order_by: str | None = None) -> dict

Copy the live needles of sealed volumes with at least min_dead dead bytes (or of two or more volumes of a series under small bytes, a quarter of VOLUME_SIZE by default) to new volumes, move their rows there and delete the old ones. Needles are recompressed with the series' current dictionary.

order_by is SQL that gives payload ids in the order wanted next to each other on disk (SELECT current_payload_id FROM molecules ORDER BY score); the rest keep theirs. Never touches an active volume, so it can run next to writers and readers (I6, I7).

Source code in src/ms_flow/payloads.py
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
def compact(self, min_dead: float = 0.5, small: int | None = None, order_by: str | None = None) -> dict:
    """Copy the live needles of sealed volumes with at least `min_dead` dead bytes (or of two or
    more volumes of a series under `small` bytes, a quarter of VOLUME_SIZE by default) to new volumes, move their rows there and
    delete the old ones. Needles are recompressed with the series' current dictionary.

    `order_by` is SQL that gives payload ids in the order wanted next to each other on disk
    (`SELECT current_payload_id FROM molecules ORDER BY score`); the rest keep theirs.
    Never touches an active volume, so it can run next to writers and readers (I6, I7).
    """
    small = VOLUME_SIZE // 4 if small is None else small
    self._empty_trash()
    with self.engine.connect() as conn:
        rows = conn.exec_driver_sql(
            f"SELECT v.id, v.series, v.size, coalesce(sum({NEEDLE} + p.length), 0) FROM ms_volumes v"
            f" LEFT JOIN {_NEEDLES} p ON p.volume_id = v.id WHERE v.state = 'sealed' GROUP BY v.id").fetchall()
    picked: dict[str, list[int]] = {}
    for series in {r[1] for r in rows}:
        mine = [(vid, 1 - live / max(size - HEADER, 1), size) for vid, s, size, live in rows if s == series]
        chosen = [vid for vid, dead, _ in mine if dead >= min_dead]
        smalls = [vid for vid, dead, size in mine if dead < min_dead and size < small]
        chosen += smalls if len(smalls) > 1 else []  # one small volume alone gains nothing
        if chosen:
            picked[series] = chosen
    stats = {"volumes": 0, "moved": 0, "read_mb": 0.0, "written_mb": 0.0}
    for series, volume_ids in picked.items():
        self._compact_series(series, volume_ids, order_by, stats)
    self._empty_trash()
    return stats

repair

repair(report: Report | None = None) -> Report

Clear what check reports as garbage: cut the tails of dead writers, delete unreferenced rows, empty volumes and orphan files. A referenced row is never deleted. Returns a new check.

Source code in src/ms_flow/payloads.py
825
826
827
828
829
830
831
832
833
834
835
836
837
def repair(self, report: Report | None = None) -> Report:
    """Clear what `check` reports as garbage: cut the tails of dead writers, delete unreferenced
    rows, empty volumes and orphan files. A referenced row is never deleted. Returns a new check."""
    report = report or self.check()
    self._recover()
    self.release(report.unreferenced)
    for volume_id in report.orphans:
        if self._unowned(volume_id):
            self.path(volume_id).unlink(missing_ok=True)
    with self.engine.begin() as conn:
        self._trash_empty(conn)
    self._empty_trash()
    return self.check()

close

close() -> None

Close files. Call it with no transaction of this store open.

Source code in src/ms_flow/payloads.py
922
923
924
925
926
927
928
929
930
931
932
933
934
def close(self) -> None:
    """Close files. Call it with no transaction of this store open."""
    with self._lock:
        volumes = [v for free in self._free.values() for v in free]
        self._free.clear()
    for volume in volumes:
        os.close(volume.fd)
    with self._fd_lock:
        for entry in self._fds.values():
            entry[2] = True
            if entry[1] == 0:
                os.close(entry[0])
        self._fds.clear()

Report dataclass

Report(broken: dict[int, str] = dict(), missing_volumes: list[int] = list(), missing_dicts: list[int] = list(), unreferenced: list[int] = list(), empty: list[int] = list(), orphans: list[int] = list(), dead: dict[int, float] = dict())

What PayloadStore.check found. Only broken and missing_dicts are errors (I5): the rest is garbage that repair clears.

Attributes:

Name Type Description
clean bool

Nothing to repair either.

clean property

clean: bool

Nothing to repair either.

PayloadCorrupt

PayloadCorrupt(payload_id: int, reason: str)

Bases: Exception

The stored bytes of a payload are missing, truncated or fail their checksum.

Source code in src/ms_flow/payloads.py
92
93
94
def __init__(self, payload_id: int, reason: str):
    super().__init__(f"payload {payload_id}: {reason}")
    self.payload_id = payload_id

with_payloads

with_payloads(project: Any, rows: Iterable[dict], fields: dict[str, str], window: int = READ_WINDOW, kinds: bool = False)

A job's source whose rows carry the bytes of their payloads, so a task gets bytes and never a path.

fields maps an id column to the key that gets its bytes ({"current_payload_id": "ligand"}; a NULL id gives None). Rows are read window at a time in (volume, offset) order, so at most that many hold bytes here, and the engine reads only its window ahead (I8). What rows knows (open, remaining, reads of ms_flow.query.Rows) passes through. With kinds, each key also gets <key>_kind, the kind its payload was stored with, so a task knows how to parse it.

Source code in src/ms_flow/payloads.py
959
960
961
962
963
964
965
966
967
968
969
970
971
def with_payloads(project: Any, rows: Iterable[dict], fields: dict[str, str], window: int = READ_WINDOW,
                  kinds: bool = False):
    """A job's source whose rows carry the bytes of their payloads, so a task gets bytes and never a path.

    `fields` maps an id column to the key that gets its bytes (`{"current_payload_id": "ligand"}`;
    a NULL id gives `None`). Rows are read `window` at a time in (volume, offset) order, so at most
    that many hold bytes here, and the engine reads only its window ahead (I8). What `rows` knows
    (`open`, `remaining`, `reads` of `ms_flow.query.Rows`) passes through. With `kinds`, each key
    also gets `<key>_kind`, the kind its payload was stored with, so a task knows how to parse it.
    """
    from ms_flow.core.database import resolve_project_store

    return _WithPayloads(resolve_project_store(project).payloads, rows, fields, window, kinds)

compaction

compaction(min_dead: float = 0.5, order_by: str | None = None)

PayloadStore.compact as a job of ms_flow, flow.submit(compaction()): it waits for the jobs writing payloads (every table writer says it writes ms_payloads), and a job reading ms_payloads or ms_volumes waits for it. It runs in the process that owns the project (I4).

Source code in src/ms_flow/payloads.py
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
def compaction(min_dead: float = 0.5, order_by: str | None = None):
    """`PayloadStore.compact` as a job of ms_flow, `flow.submit(compaction())`: it waits for the
    jobs writing payloads (every table writer says it writes `ms_payloads`), and a job reading
    `ms_payloads` or `ms_volumes` waits for it. It runs in the process that owns the project (I4)."""
    from ms_flow.engine.flow import job, task

    class Compactor:
        tables = ("ms_payloads", "ms_volumes")

        def __init__(self, params, project):
            from ms_flow.core.database import resolve_project_store

            self.store = resolve_project_store(project).payloads

        def write(self, results):
            pass

        def finish(self):
            self.stats = self.store.compact(min_dead, order_by=order_by)

    @task(thread=True)
    def nothing(element):
        return None

    return job(task=nothing, name="compact payloads", output=Compactor, reads=("ms_payloads",))(
        lambda params, project: [None])

scratch_file

scratch_file(data: bytes, suffix: str = '')

data as a temporary file, for a program that only reads paths: in /dev/shm when it is there and has room for it, in the system's temporary folder if not. Deleted on leaving.

Source code in src/ms_flow/payloads.py
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
@contextmanager
def scratch_file(data: bytes, suffix: str = ""):
    """`data` as a temporary file, for a program that only reads paths: in /dev/shm when it is
    there and has room for it, in the system's temporary folder if not. Deleted on leaving."""
    directory = None
    if os.path.isdir("/dev/shm") and os.access("/dev/shm", os.W_OK):
        if shutil.disk_usage("/dev/shm").free > 2 * len(data):  # ponytail: room now, not while it is written
            directory = "/dev/shm"
    fd, name = tempfile.mkstemp(suffix=suffix, prefix="msflow-", dir=directory)
    try:
        with os.fdopen(fd, "wb") as file:
            file.write(data)
        yield Path(name)
    finally:
        with suppress(FileNotFoundError):
            os.unlink(name)