Skip to content

Commit 115d03f

Browse files
committed
refactor(handle_source_conversation_deleted): utils.delete_file_object() is non-atomic
1 parent 1dd18ef commit 115d03f

File tree

4 files changed

+16
-21
lines changed

4 files changed

+16
-21
lines changed

API2.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ direction TB
155155
[*] --> CacheLookup : process(event)
156156
CacheLookup: status = redis.get(event.id)
157157
158-
CacheLookup --> IdempotentBranch : status in {102 Processing, 200 OK, 207 MultiStatus}
158+
CacheLookup --> IdempotentBranch : status in {102 Processing, 200 OK}
159159
CacheLookup --> StartBranch : status == None
160160
161161
state "Enforce idempotency" as IdempotentBranch {
@@ -176,12 +176,9 @@ state "handle_<event.type>()" as Handler {
176176
Handler --> OK
177177
state "Cache and report success" as SuccessBranch {
178178
OK : 200 OK
179-
MultiStatus : 207 MultiStatus
180-
181179
OK --> UpdateCache
182-
MultiStatus --> UpdateCache
183180
184-
UpdateCache : redis.set(event.id, status, ttl)
181+
UpdateCache : redis.set(event.id, OK, ttl)
185182
UpdateCache --> [*] : return (status, delta)
186183
}
187184

securedrop/journalist_app/api2/events.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import asdict
2-
from typing import Any, Dict
2+
from typing import List
33

44
from db import db
55
from journalist_app import utils
@@ -255,26 +255,27 @@ def handle_source_conversation_truncated(event: Event, minor: int) -> EventResul
255255
),
256256
)
257257

258-
deleted: Dict[ItemUUID, Any] = {}
258+
deleted: List[ItemUUID] = []
259259
for item in source.collection:
260260
if item.interaction_count <= event.data.upper_bound:
261261
try:
262262
utils.delete_file_object(item)
263-
deleted[item.uuid] = None
264263
except ValueError:
265-
deleted[item.uuid] = item
266-
267-
if any(deleted.values()):
268-
status = EventStatusCode.MultiStatus
269-
else:
270-
status = EventStatusCode.OK
264+
# `utils.delete_file_object()` is non-atomic: it guarantees
265+
# database deletion but not filesystem deletion. The former
266+
# is all we need for consistency with the client, and the
267+
# latter will be caught by monitoring for "disconnected"
268+
# submissions.
269+
pass
270+
finally:
271+
deleted.append(item.uuid)
271272

272273
db.session.refresh(source)
273274
return EventResult(
274275
event_id=event.id,
275-
status=(status, None),
276+
status=(EventStatusCode.OK, None),
276277
sources={source.uuid: source},
277-
items=deleted,
278+
items={item_uuid: None for item_uuid in deleted},
278279
)
279280

280281
@staticmethod

securedrop/journalist_app/api2/types.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@ class EventType(StrEnum):
4444
class EventStatusCode(IntEnum):
4545
Processing = 102
4646
OK = 200
47-
# This event decomposes into multiple actions, some of which succeeded and
48-
# some of which failed. Retries for failures must be submitted in a new event.
49-
MultiStatus = 207
5047
# We already saw and processed this event
5148
AlreadyReported = 208
5249
BadRequest = 400

securedrop/tests/test_journalist_api2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,10 +1170,10 @@ def test_api2_source_conversation_truncated(
11701170

11711171
status_code, msg = response.json["events"][event.id]
11721172
# Because some deletes may fail (simulated) and some succeed, the handler
1173-
# returns 200 if all succeed, or 207 (MultiStatus) if any fail.
1173+
# returns 200 if all succeed.
11741174
# The test_files fixtures never cause delete_file_object() to raise,
11751175
# so OK (200) is expected.
1176-
assert status_code in (200, 207)
1176+
assert status_code == 200
11771177

11781178
# Verify item-wise results
11791179
returned_items = response.json["items"]

0 commit comments

Comments
 (0)