Skip to content

Concurrency and thread-safety

ORM::Factory's registry, sequences, and build pipeline are designed for concurrent use. The guarantees here are explicit; assumptions outside this page are not promises.

What is thread-safe

  • Reads of the factory registry (factory-by-name, factories, aliases, factory-names, variant-names-for, etc.) are safe from any thread, including while define is running on another thread.
  • Sequence generation (generate, generate-list, inline sequence attributes invoked during a build) is atomic. Every value emitted by a given sequence is unique and ordered consistently with calls.
  • rewind-sequences is safe to call concurrently with generate (a rewind racing a generate is well-defined: either the generate sees the pre-rewind counter or the post-rewind one — never a corrupted state).
  • build / create / build-stubbed / attributes-for across threads produce independent results. The per-build evaluator is created fresh for each invocation, and the build chain used for cycle detection is thread-local via a dynamic variable.

What is not thread-safe

  • Concurrent define and modify calls serialise behind a single registry lock, but they do not coordinate semantically: if two threads each try to define a factory named 'user', the first wins and the second raises X::ORM::Factory::DuplicateFactory. Treat definition as a one-time setup phase.
  • The detected persistence adapter (ORM::Factory.persistence) caches its result across the process. Setting an adapter from multiple threads at startup races; pick one thread (typically main) to call set-persistence / reset-persistence.
  • The adapter itself is not necessarily thread-safe — the ORM::ActiveRecord adapter shares a single connection process-wide and inherits AR's concurrency model.

The intended model is definition at startup, builds concurrently:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Main thread, before any workers spin up
define {
  .sequence: 'serial';
  .factory: 'user', {
    .fname:  'Greg';
    .serial: { ORM::Factory.generate('serial') };
  };
};

# Workers can now build concurrently
my @users = (^100).hyper(:degree(8)).map({
  ORM::Factory.build('user');
}).list;

@users.map(*.serial).unique.elems == 100;   # always

Verifying it

The specs/factory/concurrency-spec.raku suite exercises the guarantees above with hypered map blocks. If you change the registry, sequence, or build code, run that spec across all three adapters before merging.