This page says what a host changes to move statifier_persistence from
0.13.0 to 0.25.0, one minor at a time. A host here is the code that
embeds the package: the module that calls use StatifierPersistence.Ecto,
the migrations it runs, the options it passes to
StatifierPersistence.Executions and StatifierPersistence.Driver, the
telemetry handlers it attaches, and any storage adapter of its own. What
each release added is in CHANGELOG.md; this page lists
only what a host has to do about it, and says NONE where the answer is
nothing.
Take the minors in order, and move the pin with each one, as the README
recommends: {:statifier_persistence, "~> 0.14.0"}, then "~> 0.15.0",
then "~> 0.16.0", then "~> 0.17.0", then "~> 0.18.0", then
"~> 0.19.0", then "~> 0.20.0", then "~> 0.21.0", then "~> 0.22.0",
then "~> 0.23.0", then "~> 0.24.0", then "~> 0.25.0".
The statifier floor stays ~> 2.6 through 0.17.0; 0.18.0 moves it to
~> 2.9, and every later release through 0.24.2 keeps it there; 0.25.0
moves it to ~> 2.15.
Before you start: the database is at V07
0.13.0 needs migration V07, so a host running 0.13.0 has already run it. If yours has not, run it before anything below: an install at V06 takes it with its own migration,
defmodule MyApp.Repo.Migrations.AddStatifierPersistenceRetirement do
use Ecto.Migration
def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 7)
def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, version: 7)
endand an install still short of V06 follows the V06 ordering rule in the
StatifierPersistence.Ecto.Migrations documentation first (a host older
than 0.13 that has not reached V03 or V04 reads the section on them below).
No release before 0.17.0 adds a migration; 0.17.0 does (V08, under
"0.16 to 0.17"), and no release from 0.18.0 through 0.25.0 adds one, and neither do the
0.23.1, 0.24.1 and 0.24.2 patches.
If the database is older than V07: V03 and V04
A host that last migrated before 0.13 may not have reached V03 or V04, the
versions that add the outcome_blob column and the metadata index. Read
this section before the V07 step above if yours is one of them; a host that
is already at V05 or later skips it.
Upgrading to V03 before deploying 0.7.0
0.7.0 needs V03 of the package DDL, and the order matters: run the
migration first, then deploy the new code. outcome_blob is an
unconditional field on the generated execution schema, so 0.7.0 against a
V02 database fails on every query that touches the executions table, not
only on the fan-out write that introduced the column. The reverse order is
safe:
V03 on a database still served by 0.6.x adds a column nobody writes and
an index nobody's query needs yet.
From 0.12.0 an install that still owes V02, V03 or V04 - one capped below version 4 - runs V06 first, on its own, and the versions it skipped afterwards. For a host capped at V01:
def up do
StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 6, version: 6)
StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 2, version: 5)
end
def down do
StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, from: 5, version: 2)
StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, from: 6, version: 6)
endSubstitute your own cap in both spans - a host capped at V02 writes
from: 3 on the second up and version: 3 on the first down. V02, V03
and V04 alter the executions table, which on a database built before 0.12.0
carries that name only once V06 has renamed it (ADR-0011 decision 3). The
ordering rule is an upgrade rule only: V06's down/1 is a no-op, so the
V06 call in the down above does nothing. V02-V05's arms name the
executions table under the name V06 gave it, and V01 - below the cap, in
your own earlier migration - is what drops it. An install already at V05
needs none of this: from: 6 is the whole upgrade.
A host already on V02 picks V03 up with an ordinary migration of its own:
defmodule MyApp.Repo.Migrations.AddStatifierPersistenceOutcomeBlob do
use Ecto.Migration
def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 3)
def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, version: 3)
endA host whose first migration is capped - up(for: MyApp.Persistence, version: 2), which is what keeps a fresh clone and an already-migrated
database on the same sequence of steps - caps its rollback the same way,
with from: 2:
defmodule MyApp.Repo.Migrations.AddStatifierPersistence do
use Ecto.Migration
def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, version: 2)
def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, from: 2)
endLeaving that down uncapped is the failure this ceiling exists to
prevent: Ecto rolls migrations back newest first, so mix ecto.rollback --all runs the V03 migration's down and then this one's, which without
from: starts at V03 again and fails on a column that is already gone
(no such column: outcome_blob). Rolling back a single step is
unaffected either way.
V03 does two things, and only one of them is cheap.
The outcome_blob column is a nullable :binary added to the
executions table. Postgres adds a nullable column with no default as a
catalog-only change, so this part is fast whatever the table's size. It takes the
configured :blob_type with the three blob columns V01 created
(identity_blob, chart_blob and position_blob), so a :blob_type whose
underlying database type is not binary needs the same hand-written ALTER
How to encrypt the blob columns and place host columns describes for those
three - now for outcome_blob as well.
The metadata GIN index is the part to plan for. V03's up/1
issues a plain CREATE INDEX, not CREATE INDEX CONCURRENTLY: it
takes a SHARE lock on the executions table for the whole build, which
blocks every INSERT, UPDATE and DELETE against that table until the
index is finished. Reads are unaffected. On a small or idle executions table
this is imperceptible. On a large one it is an outage of every write that
table takes - which, for a host stepping executions durably, means every
step of every execution.
How long that is depends on the row count, the width of the metadata
maps, and the server, so measure rather than guess. (sp-461 is a
separate measurement issue and not a number for this build: it measures
the settlement read cost against a GIN-indexed metadata column at
increasing fan-out widths.)
The concurrent build is what a host with a large executions table wants, and
0.8.0 ships it as V04 - see below.
The index is not optional in effect: without it, every fan-out child
completion asks whether its N siblings are terminal with a jsonb
containment query, and each one is a sequential scan of the whole executions
table.
On an Ecto adapter that is not Postgres, the index is skipped. GIN
and jsonb_path_ops are Postgres spellings, so up/1 creates the index
only when the migration's repo runs on Ecto.Adapters.Postgres, and
down/1 drops it under the same condition. Everything else in V03 - the
outcome_blob column included - is created on every adapter, which is
what lets a SQLite host run this package's DDL at all. (In 0.7.0 it could
not: the index raised, the whole migration rolled back, and the column
went with it. Fixed in 0.7.1.)
What is skipped with the index is what the index served. Both metadata
queries this package issues -
StatifierPersistence.Storage.Ecto.list_executions_by_metadata/2 and the
status projection list_execution_states_by_metadata/2 - are jsonb
containment SQL, which a non-Postgres backend does not parse. So on such
an adapter the Ecto adapter declares no metadata support: a metadata:
map at create is refused with {:error, :metadata_unsupported}, the two
listings refuse with {:error, :child_listing_unsupported} and
{:error, :execution_states_unsupported} - and the two raw adapter callbacks
behind them answer {:error, :metadata_unsupported} rather than issuing
SQL the backend cannot parse, for a host that reaches them directly - and
a durable subchart or a fan-out
over that store is refused at open rather than started and left with
children nothing can settle. Storing, loading, stepping and resuming
executions are unaffected. Per-execution locking is a separate Postgres-only
surface - lock_execution/3 is pg_advisory_xact_lock plus
SELECT ... FOR UPDATE -
and is tracked in sp-5lm.
Building the metadata index concurrently: V04
0.8.0 adds V04, whose whole job is to rebuild V03's metadata GIN index
with CREATE INDEX CONCURRENTLY - same name, same expression, same
jsonb_path_ops opclass, built without the SHARE lock. It ships in the
same versioned helper as everything else, so a fresh database picks it up
with the one-call recipe and needs nothing else.
The one thing V04 cannot do for you is turn off the transaction it runs
in. CREATE INDEX CONCURRENTLY cannot run inside a transaction block,
and Ecto reads @disable_ddl_transaction and @disable_migration_lock
from the module Ecto.Migrator runs - your migration, not a module
it delegates to. So V04 wants a migration of its own:
defmodule MyApp.Repo.Migrations.RebuildStatifierPersistenceMetadataIndex do
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 4)
def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, version: 4)
endCalled from an ordinary transactional migration instead, V04 leaves V03's index in place and does nothing else. That is deliberate rather than a failure mode: the index it would have built is the one already there, under the same name, and raising would break the one-call recipe every fresh database and test harness uses, where a plain build on an empty executions table costs nothing. It logs a warning when the executions table already holds rows, which is the case where the plain build did block writes and the two attributes are what you were missing.
down/1 for V04 does nothing at all: what it leaves behind is V03's
index, and V03's down/1 is what drops it. A non-transactional migration
has no rollback, so if the rebuild is interrupted, re-run the migration -
the drop is drop_if_exists, so it clears a missing or an invalid
leftover either way.
A host with a large executions table that has not yet reached V03 is the
one case V04 does not solve by itself, because V03 still builds the index
plainly on the way past. Such a host adds the outcome_blob column by
hand, skipping V03's helper call entirely - after V06 has given the table
its current name:
defmodule MyApp.Repo.Migrations.AddStatifierPersistenceOutcomeBlob do
use Ecto.Migration
def up do
alter table("executions") do
add(:outcome_blob, :binary, null: true)
end
end
endsubstituting your configured table name and prefix, and then runs the V04
migration above for the index. That is a smaller hand-written migration
than 0.7.x asked for - the index half is V04's now - and the warning
about never calling up(from: 3) afterwards still applies: V03's
create/1 is a plain create, not create_if_not_exists, so a second
run fails on the index that is already there.
On an Ecto adapter that is not Postgres, V04 is a no-op, both directions, under the same adapter check V03 uses: there is no index to rebuild, because V03 created none. Such a host needs neither attribute.
0.13 to 0.14
Schema: NONE. :needs_migration, the new fifth execution status, is a
new string in the existing status column, which has no constraint.
- Move every node to 0.14 before any execution is parked. A node still
on 0.13 does not read
:needs_migration. OnlyStatifierPersistence.Executions.migrate/4underon_failure: :parkparks an execution, so a host that never calls it with:parknever writes the status. - If you pass
routes:,invoke_types:orsend_types:tocreate/4besideexecutor:, move them insideinitialize:. They were ignored there, and a top-levelsend_types:left the execution without your registered send types for its whole life. Dialyzer now reports them, becausecreate/4andstep/5each name their own option type. It also reportsentry:,invoke_id:orchild_count:oncreate/4, which are the package's own telemetry options, andinitialize:,metadata:orlinkage:onstep/5, which did nothing there; drop them. - If you match an execution's status exhaustively, add a clause for
:needs_migration, or a catch-all. - If you match the answer of
StatifierPersistence.Executions.executions_on/2orStatifierPersistence.Storage.count_executions_by_content_hash/2as a closed map, accept a sixth key,needs_migration; a{:pinned, counts}refusal carries it underexecutions. - If you handle
[:statifier_persistence, :adapter, :call]and match itscallbackexhaustively, add:supports_retired_info?and:fetch_retired_info: a create's retired-chart check reports those two in place of:fetch_charton both bundled adapters. A handler that raises on them is detached by:telemetry. - If you match
StatifierPersistence.PinSource.reason/0exhaustively, add{:thrown, value}and{:exited, reason}: a pin source that throws or exits is now refused instead of escaping the call. - If you will park executions, handle the migration arm: an event
delivered through
step/5or aStatifierPersistence.Driverdoor to a parked execution answers{:error, {:needs_migration, execution}}and writes nothing. Redelivering after the execution leaves the arm - a correctedmigrate/4, orStatifierPersistence.Executions.unpark/3- is yours. - If you will migrate executions onto a newer chart, pass the pin
source that knows your pending timers as
pin_sources:onmigrate/4, besidefrom_machine:andto_machine:. A plan that leaves unmapped or drops a state that could own a timer is refused with{:error, {:no_pin_source, states}}without one. It is the sameStatifierPersistence.PinSourcemodule you pass toretire_chart/4; a host whose timers arestatifier_obanjobs can adoptStatifierOban.Timer.PinSourcefor it. A host that never migrates: NONE. - If you ship a storage adapter of your own: store and read back
:needs_migration, count it under its own key incount_executions_by_content_hash/2, count a durable child's pin while its parent is:activeor:needs_migration, and refuse to retire a chart a:needs_migrationexecution is on, or one a durable child's pin names while that child's parent is:needs_migration.StatifierPersistence.Testing.StorageConformancechecks each. The two new callbackssupports_retired_info?/1andfetch_retired_info/2are optional. leading_columns:onuse StatifierPersistence.Ectois NONE for an existing install: it places host columns only in tables V01 and V05 create. A host that wants a column of its own at a fixed position sets it before its tables exist; see How to encrypt the blob columns and place host columns.
0.14 to 0.15
0.15.0
Schema: NONE.
- If you call
migrate/4, check your plans against three new refusals, each a finding inside{:migration_refused, findings}(or a park underon_failure: :park):{:invocation_outside_configuration, _, _}for a live invocation kept or moved onto a state the migrated execution is not in,{:illegal_configuration, state_ids}for a configuration the to chart cannot hold, and{:invocation_element_changed, _, _}for a live invocation kept or moved onto another<invoke>element. Give an unnamed<invoke>anidbefore you edit the state that holds it, and add a clause for each finding, or a catch-all, to everycaseoverStatifierPersistence.Executions.migration_finding/0. StatifierPersistence.Executions.migrate_tree/4, which moves a parent and its durable children together, is new and optional: NONE unless you adopt it. On a storage adapter of your own it answers{:error, :tree_migration_unsupported}until you implement the two optional callbackssupports_tree_migration?/1andwrite_tree_migration/2.[:statifier_persistence, :child, :answered]gains adeliverykey. NONE is required; a handler that should notice a durable child's answer a parked parent refused reads it.
0.15.1
Schema: NONE, and no error shape a host matches is added or removed.
- If you attach handlers to
StatifierPersistence.Telemetry.events/0and match the event name exhaustively, add clauses for[:statifier_persistence, :execution, :step, :exception]and[:statifier_persistence, :execution, :unparked]. - If you pair step spans, close a span on
:exceptionas well as on:stop: a drive that raises, throws or exits - your executor or event builder included - now closes its span with:exceptionin place of:stop. - If you call
migrate/4ormigrate_tree/4, a stored active invocation whose ordinal names no<invoke>element of its state is now refused with{:invocation_element_changed, key, target}when the plan keeps it at that ordinal. Repair the stored position.
0.15 to 0.16
Schema: NONE.
- If your handler on
[:statifier_persistence, :child, :answered]matchesdeliveryexhaustively, add:parent_unfetchedand:parent_chart_unresolved, or a catch-all. On those two valuesoutcomeis the child's own andfailed_countisnil, even for a fan-out. timestamps_position:andcolumn_collations:onuse StatifierPersistence.Ectoare NONE for an existing install: likeleading_columns:, they apply only to tables V01 and V05 create, and their defaults are the layout earlier releases create.
0.16 to 0.17
0.17.0 adds ended_at, the time an execution ended, and
migration V08, which adds the column and an index on it.
Run V08 before you deploy the new version. The execution schema the new version generates reads
ended_aton every query, so an executions table without the column fails every read. Migrate first, then deploy:defmodule MyApp.Repo.Migrations.AddStatifierPersistenceEndedAt do use Ecto.Migration def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 8) def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, version: 8) endV08 runs inside an ordinary transaction, on every backend. Nodes still on 0.16 keep working while the column exists: the 0.16 schema does not declare it. Rolling V08 back drops the column and every stamp in it.
Expect
nilon executions that ended before V08. V08 backfills nothing: a row already terminal readsended_atasniluntil a later terminal write reaches it, and that write stamps its own time.StatifierPersistence.Executions.ended?/1answers whether an execution carries the stamp.If your schema is hand-written DDL, add a nullable
utc_datetime_usecended_atcolumn to the executions table and an index on it;StatifierPersistence.Ecto.Migrations.expected_version/0answers 8.If you ship a storage adapter of your own, store
ended_atfromStatifierPersistence.Storage.Adapter.execution_record/0, and inupdate_execution/2keep a stored stamp over the one a later record carries.StatifierPersistence.Testing.StorageConformancechecks both.leading_columns:andtimestamps_position:do not moveended_at: V08 alters an existing table, so the column lands at the end.StatifierPersistence.Retention.prune/3is new in 0.17.0: it clears the position blob and the input log of every execution that ended before a cutoff you pass, and keeps the execution row. Nothing calls it for you, so NONE is required;docs/retention.mdsays which rows you may delete for a finished execution.The two storage adapter callbacks that back it,
supports_execution_pruning?/1andprune_executions/3, are optional, and NONE is required of an adapter of your own: one that exports neither stays conformant, andStatifierPersistence.Retention.prune/3answers{:error, :execution_pruning_unsupported}on it. The in-memory and Ecto adapters implement both.prune_executionstakes a fourth argument from 0.19.0; see "0.18 to 0.19" below.
0.17 to 0.18
Schema: NONE. 0.18.0 requires statifier ~> 2.9.
- Let
statifierresolve to 2.9. 0.18.0 readsStatifier.MachineState'slast_selection, which statifier 2.9.0 adds. A host that pinsstatifieritself below 2.9 lifts that pin, then updates both packages together. - If you call
StatifierPersistence.Executions.migrate/4on a durable child, expect{:error, {:linked, execution}}:migrate/4now refuses an execution that carries a linkage and writes nothing, under eitheron_failure:value, so a:parkcall parks nothing. Move a child withStatifierPersistence.Executions.migrate_tree/4, the child as the root, which moves its row and its linkage pin together. If you matchStatifierPersistence.Executions.migrate_error/0exhaustively, add a clause for{:linked, execution}, or a catch-all. - If you fetch a completed child's record and match
donedata: nil, expect its donedata instead.StatifierPersistence.Execution.from_record/1reads a recorded{:done, donedata}answer back asdonedata, so a fetched completed child of a durable subchart, a fan-out child's included, carries the donedata it answered its parent with. Every other record still readsdonedata: nil. - If you deliver a refused answer again, you can take it from the
child's record: a single child now records its answer on its own
execution record before its parent's door is tried, on an adapter that
stores answers (
StatifierPersistence.Storage.execution_outcome_supported?/1). Fetch the child and answer throughStatifierPersistence.Driver.answer_parent/3with{:done, execution.donedata}, or{:failed, reason: execution.failure}for a failed child. A single child that ended before 0.18.0 has no recorded answer; delivering its answer again still needs the answer you held. A host that never redelivers: NONE. - If you attach handlers to
StatifierPersistence.Telemetry.events/0and match the event name exhaustively, add a clause for[:statifier_persistence, :execution, :step, :reentered]. It fires inside the step span, once pererror.communicationevent the persist tail re-entered after an executor failure, carryingname,originandopts. A host that folds the events it delivered to rebuild a position folds each one after the event of the step that produced it, throughStatifier.Interpreter.deliver_internal(machine_state, :platform, name, origin, opts); a host that does not: NONE. [:statifier_persistence, :execution, :step, :stop]gains aselectionkey::selectedor:nonewhen the event the step delivered ran an external round, whether or not tracing is on, andnilon a stop that delivered no event or returned no position. NONE is required; a handler that matches the stop's metadata as a closed map accepts the new key.
0.18 to 0.19
Schema: NONE. The statifier floor stays ~> 2.9.
- If you wrote a storage adapter of your own that prunes, move its
optional
prune_executions/3toStatifierPersistence.Storage.Adapter.prune_executions/4. The fourth argument is the scope,[]when the host gave none, and an adapter still exporting onlyprune_executions/3is no longer counted as declaring pruning:StatifierPersistence.Retention.prune/3answers{:error, :execution_pruning_unsupported}for it. With[], select the batch as before. An adapter that can confine a batch to the scope's column equalities carries them on every statement the batch runs; one that cannot answers{:error, :unscoped_adapter}for any scope that is not[]and clears nothing. A host whose adapters are the two this package ships: NONE. - If you match
StatifierPersistence.Storage.Adapter.error/0exhaustively, add a clause for:unscoped_adapter, or a catch-all. It is the refusal of a scope from an adapter that cannot confine a batch to one partition; the in-memory adapter answers it for anyscope:. StatifierPersistence.Retention.prune/3gains ascope:option that confines a prune to the rows holding its column equalities, over columns you placed with:leading_columns("Pruning one partition" indocs/retention.md). Left out, the prune covers the whole store as before: NONE is required.
0.19 to 0.20
Schema: NONE. The statifier floor stays ~> 2.9.
- If you attach handlers to
StatifierPersistence.Telemetry.events/0and match the event name exhaustively, add clauses for the three events of the spanStatifierPersistence.Executions.migrate_batch/3opens:[:statifier_persistence, :execution, :migrate_batch, :start],[..., :stop]and[..., :exception], or a catch-all. - If your handler on
[:statifier_persistence, :adapter, :call]matchescallbackexhaustively, add:list_execution_ids_by_content_hash, or a catch-all. - If you wrote a storage adapter of your own, you may implement the
new optional callback
StatifierPersistence.Storage.Adapter.list_execution_ids_by_content_hash/3under theStatifierPersistence.Storage.Adapter.supports_content_hash_query?/1capability. An adapter that leaves it out keeps working;migrate_batch/3against it, andStatifierPersistence.Storage.list_execution_ids_by_content_hash/3, answer{:error, :content_hash_query_unsupported}. A host whose adapters are the two this package ships: NONE.
0.20 to 0.21
Schema: NONE. The statifier floor stays ~> 2.9.
- If an executor calls back into the execution it is stepping, expect
{:error, {:reentrant_step, execution_id}}from that call. Every public door ofStatifierPersistence.Executionsthat takes an execution id (create/4,step/5,fail/4,cancel/3,unpark/3,migrate/4,migrate_tree/4,inputs/2) now refuses an execution whose executor is running in the calling process, before it reads or writes anything. Before, the nested call's write was overwritten by the outer step with nothing reported. A door for another execution is served as before, and a host that never calls back into the execution it is stepping: NONE. - If you match
StatifierPersistence.Executions.error/0exhaustively, add a clause for{:reentrant_step, execution_id}, or a catch-all. StatifierPersistence.Retention.prune/3gainssingle_batch: true, which prunes one batch and answers that batch's counts plusmore?, so a host can hold one short transaction of its own per batch. Left out, the prune drains every batch as before: NONE is required.- If you run
StatifierPersistence.Testing.StorageConformanceagainst a storage adapter of your own, your suite now runs one more case, "a door called from inside its own execution's executor refuses, and the outer step is stored". It needs nolock_execution/3and carries no tag, so it runs by default.
0.21 to 0.22
Schema: NONE. The statifier floor stays ~> 2.9.
- If an event builder handed to
StatifierPersistence.Executions.step/5calls back into the execution it was handed, expect{:error, {:reentrant_step, execution_id}}from that call, as an executor gets since 0.21.0. Before, the nested write was overwritten by the outer step with nothing reported. A builder that never calls back into its own execution: NONE. - If you run a dry run of
StatifierPersistence.Executions.migrate_batch/3from inside an executor or an event builder, the execution being stepped now answers{:would_refuse, {:reentrant_step, execution_id}}, before it is read, as the apply refuses it throughmigrate/4. The other executions the batch lists are previewed as before; a dry run called from anywhere else: NONE. - If you migrate executions whose chart schedules timers, expect
migrate/4,migrate_tree/4andmigrate_batch/3to refuse a plan that keeps a state whose delayed<send>to the execution itself names, as a literal event, one the from chart listens for and the to chart no longer does. The refusal is{:migration_refused, findings}carrying{:timer_event_removed, state_id, event}(a park underon_failure: :park, and{:would_refuse, {:migration_refused, findings}}in a batch dry run). The check is static over the two charts, so it refuses whether or not a timer is pending. Before, the migration succeeded and a timer that state had scheduled fired later into a chart that ignored its event. Handle the event in the to chart, or drop the state. A plan that drops it is then checked against the pin sources you pass (ADR-0013 decision 6): with none it answers{:no_pin_source, states}, and a source that cannot answer refuses it. - If you match
StatifierPersistence.Executions.migration_finding/0exhaustively, add a clause for{:timer_event_removed, state_id, event}, or a catch-all. - If you run
StatifierPersistence.Testing.StorageConformanceagainst a storage adapter of your own that exportslock_execution/3, your suite now runs one more case, "adapter: a door called from inside its own executor refuses before it reaches lock_execution/3". It is the 0.21.0 re-entrancy case again, run through the default serialization strategy and so under your adapter's own lock: the nested door must refuse before it asks for the lock, whether your lock admits its own holder or would wait for it. It carries@tag :postgreswith the two lock cases, so a host runningStatifierPersistence.Storage.Ectooff Postgres now excludes five cases rather than four. An adapter that does not exportlock_execution/3: NONE.
0.22 to 0.23
0.23.0
Schema: NONE. The statifier floor stays ~> 2.9.
- If a create, migration or tree migration can run while the same
chart is being retired, expect the one that loses the race to refuse.
StatifierPersistence.Executions.create/4andmigrate/4answer{:error, {:chart_retired, info}}, andmigrate_tree/4answers{:error, {:tree_refused, refusals}}carrying that arm for the node, where before the write could land its execution on the retired chart; orretire_chart/4answers{:error, {:pinned, counts}}, where before it could retire a chart an execution was about to stand on. Both answers already existed. This holds onStatifierPersistence.Storage.Ectoover Postgres under the default serialization strategy, and onStatifierPersistence.Storage.InMemorywith two differences there: a tree that loses at its write, rather than at its re-read of the chart, answers the bare{:error, {:chart_retired, info}}, and a create that loses at its insert is refused after its executor has run the create's effects. On another backend, or under aserialization:strategy of your own, the window is narrowed and not closed. A host that never retires a chart while writing to it: NONE. - If a tombstone read can meet a retirement of the same chart, or you
call
Executions.create/4,migrate/4ormigrate_tree/4inside a transaction of your own, expect waiting. OnStatifierPersistence.Storage.Ectoover Postgres,Storage.check_chart_retired/2(so every create, migration and tree migration) takes the chart's shared advisory lock: a read waits for a retirement of the same chart that has not yet committed, and a write inside your own transaction holds the lock until you commit, so a retirement of that chart waits that long. A test under the Ecto SQL sandbox is one such transaction. No answer changes. The in-memory adapter and an Ecto repo on another backend take no such lock, and under aserialization:strategy of your own, outside a transaction of yours, the lock is held only for the read's own statement. What waits on what is in What the chart lock reaches. - If your own two-key Postgres advisory locks use
1397769032as their first key, they now share a key space with the Ecto adapter's per-chart lock; pick another first key. - If you run
StatifierPersistence.Testing.StorageConformanceagainst a storage adapter of your own that exportsretire_chart/3, your suite now runs two more cases, a create and a migration whose hash is retired after their first check. They need nolock_execution/3and carry no tag. - If your executor writes through the store's repo and a step's
position save can answer an error (the only such error found on
StatifierPersistence.Storage.Ectois{:error, :execution_not_found}for a row gone from under the step), those writes now roll back with the step instead of committing without its position, and so does the step's input log entry, when the step runs under the default serialization strategy and its lock opened the outermost transaction.StatifierPersistence.Executions.step/5answers the same error as before; deliver the event again to re-drive the whole step. Inside a transaction of your own, under aserialization:strategy of your own, and onStatifierPersistence.Storage.InMemory, nothing changes: the step answers the same error and the rollback, if any, is yours. A budget-exhausted step still commits its:failedrecord. A host whose position saves never fail: NONE.
0.23.1
Schema: NONE, the statifier floor stays ~> 2.9, and no
answer a host matches is added or removed.
- If you run
StatifierPersistence.Testing.StorageConformancefrom two or more async test modules against one Postgres database, the suite no longer deadlocks (40P01) in the tombstone-check case or in the cases that retire a hash after a create's or a migration's first check: each module that uses the suite now retires chart hashes of its own, derived from the module's name. Nothing in your suite changes.
0.23 to 0.24
0.24.0
Schema: NONE. The statifier floor stays ~> 2.9, and no answer a
host matches is added or removed.
- If two stores share one Postgres database, two hosts or two test
suites each with its own charts table, they no longer wait on each
other for a chart hash they share: the Ecto adapter's per-chart
advisory lock is now keyed by the store (the charts table under its
prefix) as well as the content hash. Every caller of one store still
shares the lock, and creates, migrations and retirements on one store
are ordered as in 0.23.0. The lock's first key,
1397769032, is unchanged, so a host whose own two-key advisory locks use another first key, as "0.22 to 0.23" asks, sees no overlap it did not see before. What the lock reaches is in What the chart lock reaches. A host with two stores in one database changes nothing to take this. A host with one store per database, or whose repo is not Postgres, sees no change at all: NONE. - If a test module of yours uses
StatifierPersistence.Testing.StorageConformanceand calls one of the helpers the suite defines in it (input_log_execution/2, for one), add the prefix to the call: every attribute, function and nested module the suite puts into your module is now named under one reserved prefix,conformance_(Conformancefor a nested module), and the suite's moduledoc states that prefix as the contract. The telemetry handler__conformance_forward_adapter_call__/4is nowconformance_forward_adapter_call/4. A helper of your own that shared a name with one of the suite's no longer collides with it; one named under the prefix does. A module that calls none of the suite's helpers: NONE.
0.24.1
Schema: NONE, the statifier floor stays ~> 2.9, and no
answer a host matches is added or removed.
A documentation release: no library code differs from 0.24.0, and the
"~> 0.24.0" pin already takes it. NONE.
0.24.2
Schema: NONE, the statifier floor stays ~> 2.9, and no
answer a host matches is added or removed.
A documentation release: no library code differs from 0.24.1, and the
"~> 0.24.0" pin already takes it. This page is now listed in the
HexDocs sidebar, under Upgrading. NONE.
0.24 to 0.25
0.25.0
Schema: NONE. The statifier floor moves from ~> 2.9 to ~> 2.15,
the first release carrying host function providers, and no answer a host
matches is removed. One arm is added to a tuple a host may match:
{:function_providers, reason}, and it is answered only when a host passes
the new function_providers: option and the list does not resolve.
- If your
statifierrequirement allows 2.9 through 2.14, move it to~> 2.15:{:statifier, "~> 2.15"}, or a looser one that still excludes every release before 2.15. A host that takesstatifieronly through this package needs no change in its ownmix.exs, andmix deps.update statifierfetches 2.15. Nothing instatifier_persistenceanswers differently for a chart that calls no host function. What statifier itself changed between 2.9 and 2.15, including a document it now refuses to compile, is in statifier's CHANGELOG and upgrading page, which a host reads for the releases it skips. - If your charts call functions of your own in a
condor anexpr, give the list of yourPredicator.FunctionProvidermodules tofunction_providers:instead of stamping it by hand withStatifier.MachineState.put_function_providers/2. It goes in three places: onStatifierPersistence.Executions.step/5; insideinitialize:onStatifierPersistence.Executions.create/4; and onStatifierPersistence.Driver.new/3as a driver-level default that each call may override. The package stamps the list on the loaded position besideroutes:,invoke_types:andsend_types:, so a resumed execution evaluates the call with no touch ofStatifier.MachineStatefrom you. A durable subchart child the driver creates does not inherit the list. A host that registers no function provider changes nothing: NONE. - If you pass
function_providers:and match on the errors ofStatifierPersistence.Executionsor the driver, add the{:function_providers, reason}arm to the match. A list that does not resolve is refused at the entry with{:error, {:function_providers, reason}}, before any lock, load or write, onstep/5,create/4, and the driver'screate/3,send_event/4,done_invocation/5,failed_invocation/5andanswer_parent/3.reasonis one of{:not_a_list, value},{:invalid_provider, entry, message}and{:shadowed_names, names}(StatifierPersistence.Executions.function_providers_reason/0). A host that passes no list never sees the arm. - If you call
StatifierPersistence.Driver.start_child_at/6on a driver that carries afunction_providers:list, add{:refused, {:function_providers, reason}}to the refusals you match: a list that does not resolve is refused there before any child is created or anything is written, in the door's own{:refused, reason}shape. It emits no[:statifier_persistence, :child, :refused]event. A driver with a list that resolves, or with none, starts children as before.