Files
truf-server/tests/test_result_spool_backpressure.py
T
2026-09-30 20:30:56 +03:00

648 lines
25 KiB
Python

import multiprocessing
import os
from pathlib import Path
from types import SimpleNamespace
import sys
import tempfile
import threading
import time
import unittest
from unittest import mock
import uuid
ROOT = Path(__file__).resolve().parents[1]
APP_DIR = ROOT / 'app'
sys.path.insert(0, str(APP_DIR))
import console_runner
from process_identity import current_process_identity
from result_spool import ResultSpool, SpoolCapacityError
from scanner_db import ScannerDB
class SharedPublisherDB:
def __init__(self, lease, entered, release_gate, ingest_count, hold=False, owner_state=True):
self.conn = SimpleNamespace(is_postgres=True)
self.lease = lease
self.entered = entered
self.release_gate = release_gate
self.ingest_count = ingest_count
self.hold = hold
self.owner_state = owner_state
self.owns_lease = False
def require_runtime_safety_schema(self):
return True
def try_acquire_result_spool_publisher(self):
self.owns_lease = self.lease.acquire(False)
return self.owns_lease
def result_spool_publisher_state(self):
if not self.owner_state:
return {
'status': 'held', 'authenticated': False,
'application_name': 'unknown', 'holder_identity': 'unknown',
}
return {
'status': 'held', 'authenticated': True,
'application_name': 'truf-source:live_owner', 'pid': 123,
'state': 'active', 'holder_identity': '123:fixture-start',
}
def release_result_spool_publisher(self):
if not self.owns_lease:
return False
self.owns_lease = False
self.lease.release()
return True
def ingest_scan_event(self, envelope):
with self.ingest_count.get_lock():
self.ingest_count.value += 1
if self.hold:
self.entered.set()
if not self.release_gate.wait(15):
raise RuntimeError('fixture publisher release timed out')
return {
'ingested': True,
'scan_event_id': envelope['scan_event_id'],
'scan_event_hash': envelope['scan_event_hash'],
'duplicate': False,
}
def publisher_worker(
directory,
lease,
entered,
release_gate,
ingest_count,
outcomes,
hold=False,
stop_event=None,
):
try:
spool = ResultSpool(
directory,
max_event_bytes=4096,
max_events=20,
max_total_bytes=65536,
min_free_bytes=0,
lock_timeout_sec=2,
)
db = SharedPublisherDB(lease, entered, release_gate, ingest_count, hold=hold)
drained = console_runner.wait_for_result_spool_ready(
spool,
db,
stop_event=stop_event,
wait_seconds=0.05,
diagnostic_interval=0.2,
)
outcomes.put(('ok', len(drained)))
except KeyboardInterrupt:
outcomes.put(('stopped', 0))
except BaseException as exc:
outcomes.put((type(exc).__name__, str(exc)))
class CapacityProgressDB:
conn = SimpleNamespace(is_postgres=True)
def require_runtime_safety_schema(self):
return True
def result_spool_reservation_progress(self, reservations):
return {
'safe_progress': bool(reservations),
'counts': {
'exact_live': len(reservations),
'exact_expired': 0,
'stale_or_reassigned': 0,
},
'earliest_progress_in_sec': 60,
}
def capacity_worker(directory, index, start, stop_event, outcomes):
try:
spool = ResultSpool(
directory,
max_event_bytes=1024,
max_events=100,
max_total_bytes=2500,
min_free_bytes=0,
lock_timeout_sec=2,
)
start.wait(10)
reservation = console_runner.reserve_result_spool_claims(
spool,
CapacityProgressDB(),
f'source-{index}:{os.getpid()}:cycle',
1,
60,
stop_event=stop_event,
wait_seconds=0.05,
diagnostic_interval=1,
)
time.sleep(0.05)
if not spool.release_reservation(reservation):
raise RuntimeError('worker reservation release failed')
outcomes.put(('acquired', index))
except KeyboardInterrupt:
outcomes.put(('stopped', index))
except BaseException as exc:
outcomes.put((type(exc).__name__, index))
class ResultSpoolBackpressureTests(unittest.TestCase):
@staticmethod
def context():
return multiprocessing.get_context('spawn')
def test_completion_handoff_cannot_be_misread_as_reservation_renewal_loss(self):
class RacingSpool:
def __init__(self):
self.write_entered = threading.Event()
self.allow_write = threading.Event()
self.renew_calls = 0
def write_event(self, *args, **kwargs):
self.write_entered.set()
if not self.allow_write.wait(5):
raise RuntimeError('fixture write release timed out')
return SimpleNamespace(event_id='durable-event')
def renew_reservation(self, reservation_id, lease_seconds):
self.renew_calls += 1
return False
spool = RacingSpool()
lease_token = 'lease-token'
active = {lease_token}
reserved = {lease_token}
active_lock = threading.Lock()
outcomes = {}
renewal_attempted = threading.Event()
writer = threading.Thread(target=lambda: outcomes.__setitem__('write',
console_runner.write_reserved_result_spool_event(
spool, {}, 'reservation', 7, lease_token,
reserved, active, active_lock,
).event_id
))
def renew():
renewal_attempted.set()
outcomes['renew'] = console_runner.renew_active_result_spool_reservation(
spool, 'reservation', 1800, reserved, active_lock,
)
writer.start()
self.assertTrue(spool.write_entered.wait(5))
heartbeat = threading.Thread(target=renew)
heartbeat.start()
self.assertTrue(renewal_attempted.wait(5))
spool.allow_write.set()
writer.join(5)
heartbeat.join(5)
self.assertFalse(writer.is_alive())
self.assertFalse(heartbeat.is_alive())
self.assertEqual(outcomes, {'write': 'durable-event', 'renew': True})
self.assertEqual(spool.renew_calls, 0)
self.assertEqual(active, set())
self.assertEqual(reserved, set())
def test_absent_reservation_with_active_token_still_fails_closed(self):
spool = SimpleNamespace(renew_reservation=mock.Mock(return_value=False))
reserved = {'lease-token'}
self.assertFalse(console_runner.renew_active_result_spool_reservation(
spool, 'reservation', 1800, reserved, threading.Lock(),
))
spool.renew_reservation.assert_called_once_with('reservation', 1800)
def test_two_publishers_wait_handoff_and_ingest_once(self):
with tempfile.TemporaryDirectory() as temp_dir:
directory = os.path.join(temp_dir, 'spool')
spool = ResultSpool(
directory, max_event_bytes=4096, max_events=20,
max_total_bytes=65536, min_free_bytes=0,
)
reservation = spool.reserve_claims('unrelated-live-claim', 1, 60, bytes_per_event=1024)
event_id = str(uuid.uuid4())
spool.write_event({'version': 1, 'scan_event_id': event_id, 'result': {}})
context = self.context()
lease = context.Lock()
entered = context.Event()
release_gate = context.Event()
ingest_count = context.Value('i', 0)
outcomes = context.Queue()
owner = context.Process(
target=publisher_worker,
args=(directory, lease, entered, release_gate, ingest_count, outcomes, True),
)
owner.start()
self.assertTrue(entered.wait(10))
waiter = context.Process(
target=publisher_worker,
args=(directory, lease, entered, release_gate, ingest_count, outcomes),
)
waiter.start()
time.sleep(0.3)
release_gate.set()
for process in (owner, waiter):
process.join(20)
self.assertFalse(process.is_alive())
self.assertEqual(process.exitcode, 0)
results = sorted(outcomes.get(timeout=5) for _ in range(2))
self.assertEqual(results, [('ok', 0), ('ok', 1)])
self.assertEqual(ingest_count.value, 1)
self.assertFalse(os.path.exists(spool._event_path(event_id)))
self.assertTrue(os.path.exists(spool._reservation_path(reservation)))
self.assertTrue(spool.release_reservation(reservation))
def test_waiter_honors_shutdown_without_touching_pending_event(self):
with tempfile.TemporaryDirectory() as temp_dir:
directory = os.path.join(temp_dir, 'spool')
spool = ResultSpool(
directory, max_event_bytes=4096, max_events=20,
max_total_bytes=65536, min_free_bytes=0,
)
event_id = str(uuid.uuid4())
spool.write_event({'version': 1, 'scan_event_id': event_id, 'result': {}})
context = self.context()
lease = context.Lock()
entered = context.Event()
release_gate = context.Event()
stop_event = context.Event()
ingest_count = context.Value('i', 0)
outcomes = context.Queue()
owner = context.Process(
target=publisher_worker,
args=(directory, lease, entered, release_gate, ingest_count, outcomes, True),
)
owner.start()
self.assertTrue(entered.wait(10))
waiter = context.Process(
target=publisher_worker,
args=(directory, lease, entered, release_gate, ingest_count, outcomes, False, stop_event),
)
waiter.start()
stop_event.set()
waiter.join(15)
self.assertFalse(waiter.is_alive())
self.assertEqual(waiter.exitcode, 0)
waiter_result = outcomes.get(timeout=5)
self.assertEqual(waiter_result, ('stopped', 0))
self.assertTrue(os.path.exists(spool._event_path(event_id)))
release_gate.set()
owner.join(20)
self.assertFalse(owner.is_alive())
self.assertEqual(owner.exitcode, 0)
self.assertEqual(outcomes.get(timeout=5), ('ok', 1))
self.assertEqual(ingest_count.value, 1)
def test_unknown_competing_publisher_identity_is_fatal(self):
with tempfile.TemporaryDirectory() as temp_dir:
directory = os.path.join(temp_dir, 'spool')
spool = ResultSpool(
directory, max_event_bytes=4096, max_events=20,
max_total_bytes=65536, min_free_bytes=0,
)
spool.write_event({'version': 1, 'scan_event_id': str(uuid.uuid4()), 'result': {}})
context = self.context()
lease = context.Lock()
self.assertTrue(lease.acquire(False))
try:
db = SharedPublisherDB(
lease, context.Event(), context.Event(), context.Value('i', 0),
owner_state=False,
)
with self.assertRaisesRegex(RuntimeError, 'unknown or unauthenticated'):
console_runner.wait_for_result_spool_ready(
spool, db, wait_seconds=0.01, diagnostic_interval=0.01,
)
finally:
lease.release()
@staticmethod
def empty_spool():
return SimpleNamespace(
next_pending_event=lambda: None,
assert_claims_allowed=lambda: True,
)
@staticmethod
def sequenced_db(acquisitions, states=None, release=True, state_error=None):
class DB:
conn = SimpleNamespace(is_postgres=True)
def __init__(self):
self.acquisitions = list(acquisitions)
self.states = list(states or [])
self.release_result = release
self.state_error = state_error
self.acquire_calls = 0
self.release_calls = 0
def require_runtime_safety_schema(self):
return True
def try_acquire_result_spool_publisher(self):
self.acquire_calls += 1
return self.acquisitions.pop(0)
def result_spool_publisher_state(self):
if self.state_error:
raise self.state_error
return self.states.pop(0)
def release_result_spool_publisher(self):
self.release_calls += 1
return self.release_result
return DB()
def test_release_between_try_and_inspect_retries_acquire(self):
db = self.sequenced_db(
[False, True],
states=[{'status': 'free', 'authenticated': True}],
)
with mock.patch.object(console_runner.time, 'sleep') as sleep:
self.assertEqual(
console_runner.wait_for_result_spool_ready(
self.empty_spool(), db, wait_seconds=0.1, diagnostic_interval=30,
),
{},
)
self.assertEqual(db.acquire_calls, 2)
self.assertEqual(db.release_calls, 1)
sleep.assert_called_once_with(0.1)
def test_holder_handoff_between_observations_retries_safely(self):
owner_a = {
'status': 'held', 'authenticated': True,
'application_name': 'truf-source:github', 'holder_identity': '10:start-a',
}
owner_b = {
'status': 'held', 'authenticated': True,
'application_name': 'truf-source:gitlab', 'holder_identity': '11:start-b',
}
db = self.sequenced_db([False, False, True], states=[owner_a, owner_b])
with mock.patch.object(console_runner.time, 'sleep'):
self.assertEqual(
console_runner.wait_for_result_spool_ready(
self.empty_spool(), db, wait_seconds=0.1, diagnostic_interval=30,
),
{},
)
self.assertEqual(db.acquire_calls, 3)
self.assertEqual(db.release_calls, 1)
def test_owner_lookup_error_remains_fatal(self):
db = self.sequenced_db([False], state_error=OSError('catalog lookup failed'))
with self.assertRaisesRegex(OSError, 'catalog lookup failed'):
console_runner.wait_for_result_spool_ready(
self.empty_spool(), db, wait_seconds=0.1, diagnostic_interval=30,
)
self.assertEqual(db.acquire_calls, 1)
self.assertEqual(db.release_calls, 0)
def test_shutdown_during_release_inspection_race_stops_wait(self):
class StopDuringWait:
def is_set(self):
return False
def wait(self, timeout):
return True
db = self.sequenced_db(
[False], states=[{'status': 'free', 'authenticated': True}],
)
with self.assertRaises(KeyboardInterrupt):
console_runner.wait_for_result_spool_ready(
self.empty_spool(), db, stop_event=StopDuringWait(),
wait_seconds=0.1, diagnostic_interval=30,
)
self.assertEqual(db.acquire_calls, 1)
self.assertEqual(db.release_calls, 0)
def test_exact_release_is_required_on_acquired_session(self):
successful = self.sequenced_db([True], release=True)
self.assertEqual(
console_runner.drain_result_spool(self.empty_spool(), successful), {},
)
self.assertEqual((successful.acquire_calls, successful.release_calls), (1, 1))
uncertain = self.sequenced_db([True], release=False)
with self.assertRaisesRegex(RuntimeError, 'lease release was not confirmed'):
console_runner.drain_result_spool(self.empty_spool(), uncertain)
self.assertEqual((uncertain.acquire_calls, uncertain.release_calls), (1, 1))
def test_scanner_db_tracks_exact_same_session_acquire_release(self):
class Cursor:
def __init__(self, row):
self.row = row
def fetchone(self):
return self.row
class Connection:
is_postgres = True
def __init__(self):
self.closed = False
self.calls = []
def execute(self, sql, params=None):
self.calls.append(sql)
if 'pg_try_advisory_lock' in sql:
return Cursor({'acquired': True})
if 'pg_advisory_unlock' in sql:
return Cursor({'released': True})
raise AssertionError(sql)
def commit(self):
return None
def close(self):
self.closed = True
connection = Connection()
db = ScannerDB.__new__(ScannerDB)
db.conn = connection
db._result_spool_publisher_held = False
db.last_error = ''
self.assertTrue(db.try_acquire_result_spool_publisher())
with self.assertRaisesRegex(RuntimeError, 'already held'):
db.try_acquire_result_spool_publisher()
self.assertTrue(db.release_result_spool_publisher())
self.assertFalse(db.release_result_spool_publisher())
self.assertEqual(sum('pg_try_advisory_lock' in sql for sql in connection.calls), 1)
self.assertEqual(sum('pg_advisory_unlock' in sql for sql in connection.calls), 1)
def test_uncertain_release_closes_session_to_prevent_lock_leak(self):
class Cursor:
def fetchone(self):
return {'released': False}
class Connection:
is_postgres = True
def __init__(self):
self.closed = False
def execute(self, sql, params=None):
return Cursor()
def commit(self):
return None
def close(self):
self.closed = True
connection = Connection()
db = ScannerDB.__new__(ScannerDB)
db.conn = connection
db._result_spool_publisher_held = True
db.last_error = ''
self.assertFalse(db.release_result_spool_publisher())
self.assertTrue(connection.closed)
self.assertIsNone(db.conn)
self.assertFalse(db._result_spool_publisher_held)
def test_twelve_sources_wait_for_capacity_then_handoff_without_loss(self):
with tempfile.TemporaryDirectory() as temp_dir:
directory = os.path.join(temp_dir, 'spool')
spool = ResultSpool(
directory, max_event_bytes=1024, max_events=100,
max_total_bytes=2500, min_free_bytes=0,
)
blocker = spool.reserve_claims('unrelated-owner', 1, 60, bytes_per_event=1024)
context = self.context()
start = context.Event()
never_stop = context.Event()
outcomes = context.Queue()
processes = [
context.Process(
target=capacity_worker,
args=(directory, index, start, never_stop, outcomes),
)
for index in range(12)
]
for process in processes:
process.start()
start.set()
time.sleep(0.5)
self.assertTrue(os.path.exists(spool._reservation_path(blocker)))
self.assertTrue(spool.release_reservation(blocker))
for process in processes:
process.join(30)
self.assertFalse(process.is_alive())
self.assertEqual(process.exitcode, 0)
results = sorted(outcomes.get(timeout=5) for _ in processes)
self.assertEqual(results, [('acquired', index) for index in range(12)])
self.assertEqual(list(os.scandir(spool.reservation_directory)), [])
self.assertEqual(spool._pending_paths()[0], [])
def test_capacity_wait_honors_shutdown_and_retains_owner_reservation(self):
with tempfile.TemporaryDirectory() as temp_dir:
directory = os.path.join(temp_dir, 'spool')
spool = ResultSpool(
directory, max_event_bytes=1024, max_events=100,
max_total_bytes=2500, min_free_bytes=0,
)
blocker = spool.reserve_claims('unrelated-owner', 1, 60, bytes_per_event=1024)
context = self.context()
start = context.Event()
stop_event = context.Event()
outcomes = context.Queue()
process = context.Process(
target=capacity_worker,
args=(directory, 1, start, stop_event, outcomes),
)
process.start()
start.set()
time.sleep(0.3)
stop_event.set()
process.join(15)
self.assertFalse(process.is_alive())
self.assertEqual(process.exitcode, 0)
self.assertEqual(outcomes.get(timeout=5), ('stopped', 1))
self.assertTrue(os.path.exists(spool._reservation_path(blocker)))
self.assertTrue(spool.release_reservation(blocker))
def test_intrinsically_impossible_capacity_request_remains_fatal(self):
with tempfile.TemporaryDirectory() as temp_dir:
spool = ResultSpool(
os.path.join(temp_dir, 'spool'),
max_event_bytes=1024, max_events=100,
max_total_bytes=2500, min_free_bytes=0,
)
with self.assertRaises(SpoolCapacityError):
console_runner.reserve_result_spool_claims(
spool, CapacityProgressDB(), 'owner', 3, 60,
wait_seconds=0.01, diagnostic_interval=0.01,
)
def test_capacity_without_safe_progress_evidence_remains_fatal(self):
class UnsafeProgressDB(CapacityProgressDB):
def result_spool_reservation_progress(self, reservations):
return {'safe_progress': False}
with tempfile.TemporaryDirectory() as temp_dir:
spool = ResultSpool(
os.path.join(temp_dir, 'spool'),
max_event_bytes=1024, max_events=100,
max_total_bytes=2500, min_free_bytes=0,
)
blocker = spool.reserve_claims('unrelated-owner', 1, 60, bytes_per_event=1024)
with self.assertRaisesRegex(RuntimeError, 'unsafe or indeterminate'):
console_runner.reserve_result_spool_claims(
spool, UnsafeProgressDB(), 'owner', 1, 60,
wait_seconds=0.01, diagnostic_interval=0.01,
)
self.assertTrue(os.path.exists(spool._reservation_path(blocker)))
self.assertTrue(spool.release_reservation(blocker))
def test_exact_stopped_owner_refund_releases_only_its_unused_reservation(self):
with tempfile.TemporaryDirectory() as temp_dir, mock.patch.dict(os.environ, {
'SCANNER_DB_URL': '', 'DATABASE_URL': '',
}):
spool = ResultSpool(
os.path.join(temp_dir, 'spool'),
max_event_bytes=1024, max_events=20,
max_total_bytes=65536, min_free_bytes=0,
)
db = ScannerDB(db_path=os.path.join(temp_dir, 'scanner.db'))
identity = current_process_identity()
owner = f'source:{identity.pid}:cycle'
owned = spool.reserve_claims(owner, 1, 60, bytes_per_event=1024)
unrelated = spool.reserve_claims('other-owner', 1, 60, bytes_per_event=1024)
target = 'https://github.com/example/stopped-owner'
db.enqueue_targets('source', 'github', 'q', [target])
claims = db.claim_targets(
'source', 'github', 1, owner, 60,
return_rows=True, claim_batch=owned,
)
spool.bind_claims(owned, claims)
candidates = spool.stopped_owner_unused_reservations('source', identity)
self.assertEqual([row['reservation_id'] for row in candidates], [owned])
self.assertTrue(db.refund_stopped_result_spool_claims(candidates[0]))
self.assertTrue(spool.release_stopped_owner_reservation(owned, 'source', identity))
row = db.target_queue_item('source', 'github', target)
self.assertEqual((row['status'], row['attempts']), ('pending', 0))
self.assertFalse(os.path.exists(spool._reservation_path(owned)))
self.assertTrue(os.path.exists(spool._reservation_path(unrelated)))
self.assertTrue(spool.release_reservation(unrelated))
db.close()
if __name__ == '__main__':
unittest.main()