Let a stuck backup be stopped instead of stranding the stack (0.67.0)
A backup holds the stack lock for up to twelve hours — a large volume
legitimately takes that long — and stops the stack's containers for a
consistent snapshot. One that died without unwinding therefore left the
stack stopped and refusing every start, stop, restart and deploy with
"Stack 'npmplus' is busy: backup in progress", with nothing to click
until the TTL ran out.
Backups and restores now register themselves in stack_job_service while
they run, with their phase and the helper containers they stream
through. Cancelling sets a flag and removes those containers, which is
what actually ends the long read: the backup then unwinds through its
own finally — the half-written archive goes, the containers it stopped
are started again, the lock is released. Checks sit between two files
and two volumes, never inside one. A restore is not cancellable at all;
it is halfway through writing a stack's data back.
POST /api/stacks/{id}/unlock does that and waits for it. A backup lock
with no job behind it is a leftover, so it is released on one click; a
deploy lock takes an explicit force, because compose keeps running past
its request. GET /api/stacks/{id}/lock and the stack detail response say
what holds the stack and since when, and the stack page turns that into
"Stop backup" / "Release lock".
Startup now clears every lock rather than only expired ones: after a
restart nothing of ours is in flight, the same reasoning the leftover
helper containers already follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
052a85067b
commit
270ddda51d
19 files changed
+1018
-22
No files matched your search
@@ -18,6 +18,7 @@ import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
|
||||
import { LogViewer } from "@/components/stacks/LogViewer";
|
||||
import { ContainerCard } from "@/components/stacks/ContainerCard";
|
||||
import { ActionStatusList } from "@/components/stacks/ActionStatusBanner";
|
||||
import { StackLockBanner } from "@/components/stacks/StackLockBanner";
|
||||
import { AutoUpdatePanel } from "@/components/stacks/AutoUpdatePanel";
|
||||
import { SecretsPanel } from "@/components/stacks/SecretsPanel";
|
||||
import { GitPanel } from "@/components/stacks/GitPanel";
|
||||
@@ -43,8 +44,10 @@ export function StackDetail() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["stack", id],
|
||||
queryFn: () => stacksApi.get(id),
|
||||
// Container state arrives over /ws/events; this is the fallback.
|
||||
refetchInterval: 30000,
|
||||
// Container state arrives over /ws/events; this is the fallback. A stack
|
||||
// held by an operation is watched closely instead — the banner's "stop
|
||||
// the backup" has to turn back into the normal page once it worked.
|
||||
refetchInterval: (query) => (query.state.data?.lock ? 3000 : 30000),
|
||||
});
|
||||
|
||||
if (isLoading || !data) return <Spinner />;
|
||||
@@ -99,6 +102,8 @@ export function StackDetail() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{data.lock && isAdmin && <StackLockBanner stackId={id} lock={data.lock} />}
|
||||
|
||||
<ActionStatusList statuses={actions.statuses} onDismiss={actions.dismissStatus} />
|
||||
|
||||
{/* Tabs */}
|
||||
|
||||
Reference in new issue
Block a user