From 5a9aac18ea0dbbbdabaa96ecafc96d34e9fc29c5 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 7 Oct 2026 02:35:21 -0400 Subject: [PATCH] Qualify idle legacy process termination without inventing completed work --- .../indeehub-legacy-maintenance-controller.md | 12 +++++++-- scripts/indeehub-maintenance-controller.py | 23 +++++++++++++--- .../test_indeehub_maintenance_controller.py | 26 +++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/indeehub-legacy-maintenance-controller.md b/docs/indeehub-legacy-maintenance-controller.md index a357ca8c..ef294fb8 100644 --- a/docs/indeehub-legacy-maintenance-controller.md +++ b/docs/indeehub-legacy-maintenance-controller.md @@ -1,6 +1,6 @@ # Legacy IndeeHub maintenance controller -Status: isolated source implementation. Twelve pure Python fake-runtime regressions +Status: isolated source implementation. Fourteen pure Python fake-runtime regressions pass; no live invocation or production qualification. The controller is not part of the already signed private app candidate and needs no new app image/API. @@ -55,7 +55,7 @@ prove compatibility with newly changed data. No automatic DB/media restore exist ## Qualification and remaining integration -`python3 tests/regression/test_indeehub_maintenance_controller.py` passes twelve +`python3 tests/regression/test_indeehub_maintenance_controller.py` passes fourteen fake-runtime cases in temporary directories, without services/network/containers. Source nginx template guard coverage also passes its parser check. Production adapter compilation, actual Podman event format/systemd clean-exit behavior, @@ -76,3 +76,11 @@ A pre-acquire snapshot/preflight failure may leave no controller journal. An Aborted node journal with target_startup_began=false then permits idempotent no-op acknowledgement, without touching any other operation’s admission fence. A matching fence without its controller journal requires recovery investigation. + +Read-only source evidence from actual old API/ffmpeg shows neither has SIGTERM +shutdown hooks. The controller permits worker143 only after a paused queue has +zero active jobs. Legacy API143 additionally requires closed/stopped frontend, +stopped worker, and a fresh empty projects/contents/payments/shareholders/ +subscriptions/library_items store with no other active DB transaction. This is +a narrow first-upgrade compatibility path, not evidence populated work completed. +Populated or ambiguous legacy state remains a refused forward cutover. diff --git a/scripts/indeehub-maintenance-controller.py b/scripts/indeehub-maintenance-controller.py index 6ce10ff9..79d04a12 100644 --- a/scripts/indeehub-maintenance-controller.py +++ b/scripts/indeehub-maintenance-controller.py @@ -107,6 +107,20 @@ class Controller: original=self.queue('status');self.record['queue_was_paused']=original['paused'];self.record['queue_original_counts']=original['counts'];self.save() state=self.queue('pause');require(state['paused'],'Worker admission did not close') self.record['queue_pause_confirmed']=True;self.save() + def legacy_api_idle(self): + # Narrow first-upgrade compatibility for the observed legacy API which + # has no SIGTERM hooks. Existing customer/business work is never inferred + # completed: this path requires a fresh empty store behind closed ingress. + require(self.record.get('stopped',{}).get('indeedhub',{}).get('confirmed'),'Frontend ingress must already be stopped') + require(self.record.get('stopped',{}).get('indeedhub-ffmpeg',{}).get('confirmed'),'Transcode worker must already be stopped') + require(self.record.get('queue_pause_confirmed') is True and self.record.get('last_queue_counts',{}).get('active')==0,'Worker queue is not proven idle') + tables=('projects','contents','payments','shareholders','subscriptions','library_items') + fields=','.join("'%s',(SELECT count(*) FROM public.%s)"%(name,name) for name in tables) + sql="SELECT json_build_object("+fields+",'other_active_transactions',(SELECT count(*) FROM pg_stat_activity WHERE datname=current_database() AND pid<>pg_backend_pid() AND state<>'idle'))" + counts=json.loads(self.run(['podman','exec','indeedhub-postgres','psql','-XAt','-U','indeedhub','-d','indeedhub','-c',sql])) + require(set(counts)==set(tables)|{'other_active_transactions'},'Legacy API business-state observation incomplete') + require(all(type(value) is int and value==0 for value in counts.values()),'Legacy API has business work or active transactions; completion cannot be inferred') + self.record['legacy_api_empty_state']=counts;self.save() def graceful_stop(self, name): # Save the obligation before systemd can remove an AutoRemove container. stopped=self.record.setdefault('stopped',{}) @@ -125,8 +139,11 @@ class Controller: matching=[event for event in matching if event.get('ID',event.get('id'))==member['container_id']] require(matching,'Original process exit evidence unavailable; hold retained') code=matching[-1].get('ContainerExitCode',matching[-1].get('containerExitCode')) - require(str(code)=='0','Original process did not exit cleanly; active work is not claimed completed') - stopped[name].update(confirmed=True,exit_code=0,confirmed_at=time.time());self.save() + idle_worker = name=='indeedhub-ffmpeg' and self.record.get('queue_pause_confirmed') is True and self.record.get('last_queue_counts',{}).get('active')==0 + empty_api = name=='indeedhub-api' and self.record.get('legacy_api_empty_state') is not None + require(str(code)=='0' or (str(code)=='143' and (idle_worker or empty_api)),'Original process did not exit cleanly; active work is not claimed completed') + classification=('idle-worker-terminated-after-queue-drain' if idle_worker else 'empty-business-store-legacy-api-terminated') if str(code)=='143' else 'clean-process-exit' + stopped[name].update(confirmed=True,exit_code=int(code),classification=classification,confirmed_at=time.time());self.save() def volume_sources(self): expected=['indeedhub-minio-data','indeedhub-postgres-data','indeedhub-redis-data','indeedhub-relay-data'] rows=json.loads(self.run(['podman','volume','inspect',*expected])) @@ -186,7 +203,7 @@ class Controller: if state['counts'].get('active',0)==0:break require(time.monotonic()