without-durability-sqlite 0.0.6__tar.gz → 0.0.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {without_durability_sqlite-0.0.6 → without_durability_sqlite-0.0.8}/PKG-INFO +5 -5
- {without_durability_sqlite-0.0.6 → without_durability_sqlite-0.0.8}/README.md +2 -2
- {without_durability_sqlite-0.0.6 → without_durability_sqlite-0.0.8}/pyproject.toml +3 -3
- {without_durability_sqlite-0.0.6 → without_durability_sqlite-0.0.8}/pyproject.toml.orig +3 -3
- {without_durability_sqlite-0.0.6 → without_durability_sqlite-0.0.8}/src/without_durability_sqlite/store.py +183 -7
- {without_durability_sqlite-0.0.6 → without_durability_sqlite-0.0.8}/src/without_durability_sqlite/__init__.py +0 -0
- {without_durability_sqlite-0.0.6 → without_durability_sqlite-0.0.8}/src/without_durability_sqlite/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: without-durability-sqlite
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
4
4
|
Summary: A without-durability checkpoint store and queue backed by one SQLite file, with no server and no third-party driver.
|
|
5
5
|
Author: Josh Karpel
|
|
6
6
|
Author-email: Josh Karpel <josh.karpel@gmail.com>
|
|
@@ -14,8 +14,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
16
16
|
Classifier: Typing :: Typed
|
|
17
|
-
Requires-Dist: without-async==0.0.
|
|
18
|
-
Requires-Dist: without-durability==0.0.
|
|
17
|
+
Requires-Dist: without-async==0.0.8
|
|
18
|
+
Requires-Dist: without-durability==0.0.8
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
|
|
@@ -44,8 +44,8 @@ There is one writer at a time by construction, so `BEGIN IMMEDIATE` takes the
|
|
|
44
44
|
write lock for the whole transaction and the fence check and the write it guards
|
|
45
45
|
cannot be interleaved: Postgres needs `FOR UPDATE` on the claim row to get that
|
|
46
46
|
and Redis needs a Lua script, while here the transaction *is* the exclusion. And
|
|
47
|
-
there is nothing to co-locate, because the datastore is a file, so `transact
|
|
48
|
-
`arrive` reach every table an application keeps in it. That last one is the same
|
|
47
|
+
there is nothing to co-locate, because the datastore is a file, so `transact`,
|
|
48
|
+
`arrive`, and `deliver` reach every table an application keeps in it. That last one is the same
|
|
49
49
|
guarantee DBOS gets from Postgres, for an application that never needed Postgres.
|
|
50
50
|
|
|
51
51
|
What it costs is the shape of the whole thing: one machine. Every process sharing
|
|
@@ -23,8 +23,8 @@ There is one writer at a time by construction, so `BEGIN IMMEDIATE` takes the
|
|
|
23
23
|
write lock for the whole transaction and the fence check and the write it guards
|
|
24
24
|
cannot be interleaved: Postgres needs `FOR UPDATE` on the claim row to get that
|
|
25
25
|
and Redis needs a Lua script, while here the transaction *is* the exclusion. And
|
|
26
|
-
there is nothing to co-locate, because the datastore is a file, so `transact
|
|
27
|
-
`arrive` reach every table an application keeps in it. That last one is the same
|
|
26
|
+
there is nothing to co-locate, because the datastore is a file, so `transact`,
|
|
27
|
+
`arrive`, and `deliver` reach every table an application keeps in it. That last one is the same
|
|
28
28
|
guarantee DBOS gets from Postgres, for an application that never needed Postgres.
|
|
29
29
|
|
|
30
30
|
What it costs is the shape of the whole thing: one machine. Every process sharing
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "without-durability-sqlite"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.8"
|
|
8
8
|
description = "A without-durability checkpoint store and queue backed by one SQLite file, with no server and no third-party driver."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -21,8 +21,8 @@ classifiers = [
|
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"without-async==0.0.
|
|
25
|
-
"without-durability==0.0.
|
|
24
|
+
"without-async==0.0.8",
|
|
25
|
+
"without-durability==0.0.8",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[[project.authors]]
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "without-durability-sqlite"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.8"
|
|
8
8
|
description = "A without-durability checkpoint store and queue backed by one SQLite file, with no server and no third-party driver."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -24,8 +24,8 @@ classifiers = [
|
|
|
24
24
|
"Typing :: Typed",
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
|
-
"without-async==0.0.
|
|
28
|
-
"without-durability==0.0.
|
|
27
|
+
"without-async==0.0.8",
|
|
28
|
+
"without-durability==0.0.8",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[tool.uv.sources]
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# be interleaved with anything. Postgres needs `FOR UPDATE` on the claim row to get
|
|
12
12
|
# that, because there readers and writers run concurrently and a statement's snapshot
|
|
13
13
|
# can be stale; Redis needs a Lua script. Here the transaction *is* the exclusion.
|
|
14
|
-
# - There is nothing to co-locate. `transact` and `
|
|
14
|
+
# - There is nothing to co-locate. `transact`, `arrive`, and `deliver` reach the whole datastore
|
|
15
15
|
# because the datastore is a file, so the question the other two stores have to keep
|
|
16
16
|
# asking (are these two writes in one local commit?) has one answer and it is yes.
|
|
17
17
|
#
|
|
@@ -59,6 +59,7 @@ from collections.abc import Callable
|
|
|
59
59
|
from contextlib import closing
|
|
60
60
|
from dataclasses import dataclass
|
|
61
61
|
from dataclasses import field
|
|
62
|
+
from datetime import UTC
|
|
62
63
|
from datetime import datetime
|
|
63
64
|
from datetime import timedelta
|
|
64
65
|
from pathlib import Path
|
|
@@ -68,11 +69,15 @@ from typing import cast
|
|
|
68
69
|
from without_async import Milliseconds
|
|
69
70
|
from without_durability.codec import JSON
|
|
70
71
|
from without_durability.codec import CheckpointCodec
|
|
72
|
+
from without_durability.interfaces import INBOX
|
|
73
|
+
from without_durability.interfaces import INBOX_DIGITS
|
|
71
74
|
from without_durability.interfaces import LEASE
|
|
72
75
|
from without_durability.interfaces import Delivery
|
|
76
|
+
from without_durability.interfaces import Entry
|
|
73
77
|
from without_durability.interfaces import Fenced
|
|
74
78
|
from without_durability.interfaces import Pass
|
|
75
79
|
from without_durability.interfaces import Recorded
|
|
80
|
+
from without_durability.interfaces import Written
|
|
76
81
|
from without_durability.interfaces import check_duration
|
|
77
82
|
from without_durability.stepwise import now_utc
|
|
78
83
|
|
|
@@ -89,19 +94,49 @@ BUSY_TIMEOUT = Milliseconds(5_000)
|
|
|
89
94
|
# `value` is TEXT rather than a richer type, which is the same shape the Redis store's
|
|
90
95
|
# hash field has and leaves the same question open: what goes *in* the text is the
|
|
91
96
|
# store's injected `CheckpointCodec`, defaulting to JSON because that is what makes a
|
|
92
|
-
# checkpoint readable by anything that can open the file.
|
|
93
|
-
# one of these tables is addressed by its primary key and never by a rowid, so the extra
|
|
94
|
-
# indirection would be pure overhead.
|
|
97
|
+
# checkpoint readable by anything that can open the file.
|
|
95
98
|
#
|
|
96
99
|
# `NOT NULL` on `value` keeps "no row" and "a row holding JSON null" distinguishable, so
|
|
97
100
|
# a step that legitimately records `None` is not read back as a step that never ran.
|
|
101
|
+
#
|
|
102
|
+
# `seq` is what `load`'s ordering guarantee rests on, and it is the same column Postgres
|
|
103
|
+
# names: assigned on insert, in insertion order, and left alone by the conflict update
|
|
104
|
+
# below, which is exactly the property the guarantee needs. The writer that first recorded
|
|
105
|
+
# a step decides where it sits, and a later write that loses moves neither the value nor
|
|
106
|
+
# the position. Nothing else here is a candidate, since the only other order on offer is
|
|
107
|
+
# the unique index's, which is step name: a different question with a plausible enough
|
|
108
|
+
# answer to pass a careless test.
|
|
109
|
+
#
|
|
110
|
+
# `INTEGER PRIMARY KEY` makes it an alias for the rowid rather than a column beside one,
|
|
111
|
+
# so it costs no storage and needs no sequence. Naming it is not decoration either. SQLite
|
|
112
|
+
# reserves the right to renumber rowids in "any tables that do not have an explicit INTEGER
|
|
113
|
+
# PRIMARY KEY" when a database is `VACUUM`ed, and this store hands an operator a file it
|
|
114
|
+
# invites any tool to open, so the guarantee would otherwise rest on a promise SQLite
|
|
115
|
+
# declines to make. Declaring the column is what puts this table outside that sentence.
|
|
116
|
+
#
|
|
117
|
+
# `written_at` is what `history` reads, and its `DEFAULT` does the same work `seq` does:
|
|
118
|
+
# evaluated on insert, and left alone by every conflict update below, so a losing write
|
|
119
|
+
# moves the value, the position, and the time equally not at all. The clock is SQL's for
|
|
120
|
+
# the reason the claim's is, which here is consistency with the other two stores rather
|
|
121
|
+
# than a guarantee: SQLite *is* the caller's machine, so there are no two clocks to
|
|
122
|
+
# disagree.
|
|
123
|
+
#
|
|
124
|
+
# `(workflow, step)` is `UNIQUE` rather than the primary key because a table gets one
|
|
125
|
+
# primary key and `seq` is now it. Nothing else changes: both columns are `NOT NULL`, so
|
|
126
|
+
# the constraint admits exactly the rows the primary key did, and it is still what the
|
|
127
|
+
# upserts below name as their conflict target.
|
|
128
|
+
#
|
|
129
|
+
# The claim and queue tables stay `WITHOUT ROWID` because each is addressed by its primary
|
|
130
|
+
# key and never by a position, so for those the indirection would be pure overhead.
|
|
98
131
|
SCHEMA = """
|
|
99
132
|
CREATE TABLE IF NOT EXISTS workflow_checkpoint (
|
|
133
|
+
seq INTEGER PRIMARY KEY,
|
|
100
134
|
workflow TEXT NOT NULL,
|
|
101
135
|
step TEXT NOT NULL,
|
|
102
136
|
value TEXT NOT NULL,
|
|
103
|
-
|
|
104
|
-
|
|
137
|
+
written_at REAL NOT NULL DEFAULT (unixepoch('now', 'subsec')),
|
|
138
|
+
UNIQUE (workflow, step)
|
|
139
|
+
);
|
|
105
140
|
|
|
106
141
|
CREATE TABLE IF NOT EXISTS workflow_claim (
|
|
107
142
|
workflow TEXT PRIMARY KEY,
|
|
@@ -169,14 +204,59 @@ ON CONFLICT (workflow, step) DO UPDATE SET value = workflow_checkpoint.value
|
|
|
169
204
|
RETURNING value
|
|
170
205
|
"""
|
|
171
206
|
|
|
207
|
+
# `supply` under a key this statement mints instead of one the caller brought: the append
|
|
208
|
+
# that puts a message in a workflow's inbox.
|
|
209
|
+
#
|
|
210
|
+
# The number is the highest `seq` in the table plus one, which is global rather than per
|
|
211
|
+
# workflow and so leaves gaps in any one workflow's run of keys. That is what the contract
|
|
212
|
+
# allows and what makes this a single statement: a per-workflow count would be a second
|
|
213
|
+
# read, and reading a maximum and then inserting against it is the exact race that would
|
|
214
|
+
# need a transaction to close. There are no concurrent writers here to lose it to, since
|
|
215
|
+
# SQLite admits one, but the store that has them (Postgres) reaches for the same shape and
|
|
216
|
+
# the two are easier to hold in one head this way.
|
|
217
|
+
#
|
|
218
|
+
# There is no `ON CONFLICT` clause and deliberately none: the key is fresh by construction,
|
|
219
|
+
# so a collision means the numbering is broken and a `UNIQUE` violation is the loud version
|
|
220
|
+
# of that. An upsert here would quietly hand back somebody else's message instead.
|
|
221
|
+
APPEND = f"""
|
|
222
|
+
INSERT INTO workflow_checkpoint (workflow, step, value)
|
|
223
|
+
SELECT
|
|
224
|
+
:workflow,
|
|
225
|
+
'{INBOX}' || printf('%0{INBOX_DIGITS}d', COALESCE((SELECT MAX(seq) FROM workflow_checkpoint), 0) + 1),
|
|
226
|
+
:value
|
|
227
|
+
RETURNING step, value
|
|
228
|
+
"""
|
|
229
|
+
|
|
172
230
|
FENCE = "SELECT token FROM workflow_claim WHERE workflow = ?"
|
|
173
231
|
ALREADY = "SELECT value FROM workflow_checkpoint WHERE workflow = ? AND step = ?"
|
|
174
232
|
WRITE = "INSERT INTO workflow_checkpoint (workflow, step, value) VALUES (?, ?, ?)"
|
|
175
|
-
|
|
233
|
+
# `ORDER BY seq` is the whole of the ordering guarantee, and it is load-bearing rather
|
|
234
|
+
# than a formality: `WHERE workflow = ?` is served by the unique index on
|
|
235
|
+
# `(workflow, step)`, so without it the rows come back sorted by step name.
|
|
236
|
+
LOAD = "SELECT step, value FROM workflow_checkpoint WHERE workflow = ? ORDER BY seq"
|
|
237
|
+
HISTORY = "SELECT step, value, written_at FROM workflow_checkpoint WHERE workflow = ? ORDER BY seq"
|
|
176
238
|
# Hand the workflow back early, but keep the token, so the next claim gets the next
|
|
177
239
|
# number up and a pass that comes back from the dead still loses.
|
|
178
240
|
RELEASE = "UPDATE workflow_claim SET held_until = unixepoch('now', 'subsec') WHERE workflow = ? AND token = ?"
|
|
179
241
|
|
|
242
|
+
# Forget every record a workflow has. Paired with `SUPERSEDE` below and never run without
|
|
243
|
+
# it, which is what the transaction in `discard` is for.
|
|
244
|
+
DISCARD = "DELETE FROM workflow_checkpoint WHERE workflow = ?"
|
|
245
|
+
|
|
246
|
+
# Take the fencing token *up*, so a pass still holding one is refused at its next write.
|
|
247
|
+
#
|
|
248
|
+
# An `UPDATE` rather than the upsert `CLAIM` is, and the difference is what it declines to
|
|
249
|
+
# do: a workflow with no claim row has no `Pass` outstanding, since a `Pass` is only ever
|
|
250
|
+
# handed out by a `claim` that wrote one, so there is nothing to fence and a row minted
|
|
251
|
+
# here would be a tombstone for a workflow nobody ever claimed. The deadline is moved to
|
|
252
|
+
# now at the same time, so the workflow is claimable again immediately: what is kept is the
|
|
253
|
+
# ordering, not the claim.
|
|
254
|
+
SUPERSEDE = """
|
|
255
|
+
UPDATE workflow_claim
|
|
256
|
+
SET token = token + 1, held_until = unixepoch('now', 'subsec')
|
|
257
|
+
WHERE workflow = ?
|
|
258
|
+
"""
|
|
259
|
+
|
|
180
260
|
# Take the oldest visible workflow and push it a lease into the future. There is no
|
|
181
261
|
# `SKIP LOCKED` here and none is wanted: it exists so one poller does not queue behind
|
|
182
262
|
# another's row lock, and SQLite has no concurrent writers to step over.
|
|
@@ -204,6 +284,11 @@ ON CONFLICT (namespace, workflow) DO UPDATE SET visible_at = excluded.visible_at
|
|
|
204
284
|
# different `visible_at`, so the equality is the whole check.
|
|
205
285
|
FINISH = "DELETE FROM workflow_queue WHERE namespace = ? AND workflow = ? AND visible_at = ?"
|
|
206
286
|
|
|
287
|
+
# Withdraw the workflow's right to run, whatever its row currently means. Unconditional
|
|
288
|
+
# where `FINISH` compares the receipt, which is the difference between finishing a pass
|
|
289
|
+
# (leave anything that asked for another) and cancelling the workflow (leave nothing).
|
|
290
|
+
CANCEL = "DELETE FROM workflow_queue WHERE namespace = ? AND workflow = ?"
|
|
291
|
+
|
|
207
292
|
# Suspend until a deadline, under the same comparison and for the same reason. A workflow
|
|
208
293
|
# holds one row here, so writing the deadline unconditionally would land on top of a
|
|
209
294
|
# `make_ready` that arrived while the pass was ending and push a confirmation out to a
|
|
@@ -416,6 +501,14 @@ class SqliteCheckpointer:
|
|
|
416
501
|
rows = await self.database.run(lambda connection: connection.execute(LOAD, (workflow,)).fetchall())
|
|
417
502
|
return {step: self.codec.decode(encoded) for step, encoded in rows}
|
|
418
503
|
|
|
504
|
+
async def history(self, workflow: str) -> dict[str, Written]:
|
|
505
|
+
"""The same records `load` returns, each with the moment it was written."""
|
|
506
|
+
rows = await self.database.run(lambda connection: connection.execute(HISTORY, (workflow,)).fetchall())
|
|
507
|
+
return {
|
|
508
|
+
step: Written(value=self.codec.decode(encoded), at=datetime.fromtimestamp(written_at, UTC))
|
|
509
|
+
for step, encoded, written_at in rows
|
|
510
|
+
}
|
|
511
|
+
|
|
419
512
|
async def claim(self, workflow: str, lease: timedelta) -> Pass | None:
|
|
420
513
|
taken = await self.database.run(
|
|
421
514
|
lambda connection: connection.execute(
|
|
@@ -485,6 +578,37 @@ class SqliteCheckpointer:
|
|
|
485
578
|
)
|
|
486
579
|
return self.codec.decode(cast(tuple[str], stored)[0])
|
|
487
580
|
|
|
581
|
+
async def append(self, workflow: str, value: object) -> Entry:
|
|
582
|
+
"""File `value` in this workflow's inbox, under the next key in the table."""
|
|
583
|
+
stored = await self.database.run(
|
|
584
|
+
lambda connection: connection.execute(
|
|
585
|
+
APPEND,
|
|
586
|
+
{"workflow": workflow, "value": self.codec.encode(value)},
|
|
587
|
+
).fetchone()
|
|
588
|
+
)
|
|
589
|
+
key, encoded = cast(tuple[str, str], stored)
|
|
590
|
+
return Entry(key=key, value=self.codec.decode(encoded))
|
|
591
|
+
|
|
592
|
+
async def discard(self, workflow: str) -> int:
|
|
593
|
+
"""
|
|
594
|
+
Forget every record this workflow has, and raise its fence, in one transaction.
|
|
595
|
+
|
|
596
|
+
One commit rather than two statements, because the two are only right together: a
|
|
597
|
+
crash between them either leaves the records deleted with the fence unraised, so
|
|
598
|
+
the pass that was mid-flight writes them back one at a time, or the reverse, which
|
|
599
|
+
fences a live pass for a deletion that never happened.
|
|
600
|
+
|
|
601
|
+
What is left behind is one claim row carrying a number. Nothing here sweeps it, in
|
|
602
|
+
keeping with the rest of this store, where nothing expires and tidying the file is
|
|
603
|
+
the deployment's homework.
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
def one_commit(cursor: sqlite3.Cursor) -> int:
|
|
607
|
+
cursor.execute(SUPERSEDE, (workflow,))
|
|
608
|
+
return cursor.execute(DISCARD, (workflow,)).rowcount
|
|
609
|
+
|
|
610
|
+
return await self.database.run(lambda connection: transacted(connection, one_commit))
|
|
611
|
+
|
|
488
612
|
async def release(self, holder: Pass) -> None:
|
|
489
613
|
await self.database.run(lambda connection: connection.execute(RELEASE, (holder.workflow, holder.token)))
|
|
490
614
|
|
|
@@ -593,6 +717,22 @@ class SqliteScheduler:
|
|
|
593
717
|
"""Nothing to take over by hand: an abandoned workflow becomes visible on its own."""
|
|
594
718
|
return None
|
|
595
719
|
|
|
720
|
+
async def cancel(self, workflow: str) -> None:
|
|
721
|
+
"""
|
|
722
|
+
Drop the workflow's row, whichever of the three things its `visible_at` means.
|
|
723
|
+
|
|
724
|
+
One `DELETE` covers queued, sleeping, and out with a worker, because this table
|
|
725
|
+
holds one row per workflow and the visibility is the only thing that differs
|
|
726
|
+
between them. That is the same collapse that leaves `wake_due` and `reclaim` with
|
|
727
|
+
nothing to do.
|
|
728
|
+
|
|
729
|
+
The half of `cancel` a queue sweep cannot reach comes free with it: a pass still in
|
|
730
|
+
flight answers with `wake_at`, which is an `UPDATE` conditional on the visibility
|
|
731
|
+
still being the one it took, and a deleted row has none. So the deadline it was
|
|
732
|
+
about to write updates nothing and a deleted workflow is not put back to sleep.
|
|
733
|
+
"""
|
|
734
|
+
await self.database.run(lambda connection: connection.execute(CANCEL, (self.namespace, workflow)))
|
|
735
|
+
|
|
596
736
|
async def done(self, delivery: Delivery) -> None:
|
|
597
737
|
"""
|
|
598
738
|
Drop the workflow, unless something asked for another pass while this one ran.
|
|
@@ -644,3 +784,39 @@ class SqliteDurable:
|
|
|
644
784
|
return self.checkpointer.codec.decode(cast(tuple[str], stored)[0])
|
|
645
785
|
|
|
646
786
|
return await self.checkpointer.database.run(lambda connection: transacted(connection, one_commit))
|
|
787
|
+
|
|
788
|
+
async def deliver(self, workflow: str, value: object) -> Entry:
|
|
789
|
+
"""Append the message and make the workflow ready, together or not at all."""
|
|
790
|
+
visible_at = self.scheduler.now().timestamp()
|
|
791
|
+
|
|
792
|
+
def one_commit(cursor: sqlite3.Cursor) -> Entry:
|
|
793
|
+
stored = cursor.execute(
|
|
794
|
+
APPEND,
|
|
795
|
+
{"workflow": workflow, "value": self.checkpointer.codec.encode(value)},
|
|
796
|
+
).fetchone()
|
|
797
|
+
cursor.execute(
|
|
798
|
+
SCHEDULE,
|
|
799
|
+
{"namespace": self.scheduler.namespace, "workflow": workflow, "visible_at": visible_at},
|
|
800
|
+
)
|
|
801
|
+
key, encoded = cast(tuple[str, str], stored)
|
|
802
|
+
return Entry(key=key, value=self.checkpointer.codec.decode(encoded))
|
|
803
|
+
|
|
804
|
+
return await self.checkpointer.database.run(lambda connection: transacted(connection, one_commit))
|
|
805
|
+
|
|
806
|
+
async def delete(self, workflow: str) -> int:
|
|
807
|
+
"""
|
|
808
|
+
Cancel the workflow's wakeups and forget its records, together or not at all.
|
|
809
|
+
|
|
810
|
+
Three statements in one commit, so the ordering `SplitDurable` has to reason about
|
|
811
|
+
does not arise: there is no window in which the records are gone and a wakeup is
|
|
812
|
+
not, and none in which the fence has been raised for a deletion that did not
|
|
813
|
+
happen. Which is the same thing `arrive` gets from this store and for the same
|
|
814
|
+
reason, one file.
|
|
815
|
+
"""
|
|
816
|
+
|
|
817
|
+
def one_commit(cursor: sqlite3.Cursor) -> int:
|
|
818
|
+
cursor.execute(CANCEL, (self.scheduler.namespace, workflow))
|
|
819
|
+
cursor.execute(SUPERSEDE, (workflow,))
|
|
820
|
+
return cursor.execute(DISCARD, (workflow,)).rowcount
|
|
821
|
+
|
|
822
|
+
return await self.checkpointer.database.run(lambda connection: transacted(connection, one_commit))
|
|
File without changes
|
|
File without changes
|