shim: make container teardown timeouts configurable - #2855
Open
Jonas Heinle (Kataglyphis) wants to merge 1 commit into
Open
shim: make container teardown timeouts configurable#2855Jonas Heinle (Kataglyphis) wants to merge 1 commit into
Jonas Heinle (Kataglyphis) wants to merge 1 commit into
Conversation
The shim gives a container 30s to shut down and another 30s to terminate
before it stops waiting, DeleteExec waits 30s for resource cleanup, and
the delete command waits 30s for a leftover compute system to terminate.
All of these limits are hardcoded.
Tearing a Windows Server (process isolated) container down is host-side
work whose cost scales with how much the container touched the
filesystem: the layer filter stack has to be detached and the container's
registry hives flushed back into its scratch. For filesystem-heavy
workloads such as source builds this takes minutes. We measured 117s for
a single OpenCV build container on Windows 11 26200 with ltsc2025 base
images (HcsShutDownComputeSystem returned promptly; the completion
notification arrived 117s later).
When the 30s limits expire mid-flush, the container is terminated while
its scratch is still being written. The scratch is then left in a state
the platform refuses to export: every subsequent finalize of that
snapshot fails with hcsshim::ExportLayer 0x3, and the damage survives
fresh snapshots and host reboots. All silo processes do exit, so nothing
in the container is at fault and no in-container mitigation helps -
verified by overriding WaitToKillServiceTimeout inside the payload and by
a full pre-exit teardown of non-essential services, both of which lost
the notification identically.
Make the limits configurable via environment variables, which the shim
inherits from containerd, following the naming of the existing
CONTAINERD_SHIM_RUNHCS_V1_WAIT_DEBUGGER:
CONTAINERD_SHIM_RUNHCS_V1_TEARDOWN_TIMEOUT each wait in hcsTask.close,
and the delete command's
wait for termination
CONTAINERD_SHIM_RUNHCS_V1_TASK_CLOSE_TIMEOUT the hcsTask.DeleteExec wait
The first two are coupled, and the coupling is why the second knob is not
simply independent: DeleteExec waits on the channel close() closes, so a
task close timeout below close()'s worst case of 2*teardown abandons a
teardown that is still progressing - precisely the outcome these knobs
exist to prevent. Raising only the teardown timeout would therefore
silently not help. When the task close timeout is not set explicitly and
the teardown timeout has been raised above the default, the task close
timeout is derived to cover close()'s worst case with room to spare.
delete.go waits on the same host-side work as hcsTask.close and runs
exactly when the shim died with a container still going down, so it takes
the same bound rather than a knob of its own.
Defaults are unchanged at 30s, so behaviour is identical unless a host
opts in. An empty, unparseable or non-positive value is treated as unset,
since resolution happens during package initialization, before logging is
available, and a bad value must not stop the shim from starting.
Also log how long a successful shutdown actually took. That duration is
what is needed to size the timeout for a given workload and it was not
otherwise observable - the absence of this number is a large part of why
the failure above was hard to attribute.
Deliberately left alone: the 30s timer in the SIGKILL path, which guards
the hosting UVM (ht.host != nil) rather than process isolated teardown;
cmd/runhcs, a separate binary that already allows 5 minutes; and
cmd/containerd-shim-lcow-v2, which is a different shim.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jonas Heinle <jonasheinle@googlemail.com>
Jonas Heinle (Kataglyphis)
force-pushed
the
feature/configurable-teardown-timeout
branch
from
August 6, 2026 10:50
1cd2944 to
6830a70
Compare
Jonas Heinle (Kataglyphis)
added a commit
to Kataglyphis/Kataglyphis-ContainerHub
that referenced
this pull request
Aug 6, 2026
…ement Filed microsoft/hcsshim#2855 as a draft from Kataglyphis/hcsshim:feature/configurable-teardown-timeout. Corrects a wrong claim in the README: hcsshim enforces a DCO check in addition to the Microsoft CLA. The repo README advertises only the CLA, so the PR went up with a failing DCO gate and needed an amend with -s. Both the requirement and the fix are now written down, and the in-tree patch carries the Signed-off-by trailer. Still open: the issue from ISSUE.md is not filed, the comment on Windows-Containers#547 is not posted, and the runtime measurement still comes from the constants-in-place build rather than the submitted env-var build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
|
@microsoft-github-policy-service agree |
Jonas Heinle (Kataglyphis)
marked this pull request as ready for review
August 7, 2026 08:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR Version: Heavy container builds need longer times than 30s to teardown.
This PR makes this time configurable.
For the reviewer
Please let me know if the PR needs some changes in order to be merged.
I am willing to adapt the code for following your guidelines.
What this changes
The shim hardcodes four 30 second limits around container teardown:
hcsTask.close()waits 30 s for a graceful shutdown, then 30 s for a terminate(
const tearDownTimeout,task_hcs.go).hcsTask.DeleteExec()waits 30 s for container resource cleanup(
const timeout, "waiting for task to be closed",task_hcs.go).deletecommand waits 30 s for a leftover compute system to finishterminating (
delete.go).This PR makes them configurable via environment variables, which the shim
inherits from containerd. The names follow the existing
CONTAINERD_SHIM_RUNHCS_V1_WAIT_DEBUGGER:CONTAINERD_SHIM_RUNHCS_V1_TEARDOWN_TIMEOUThcsTask.close, and thedeletecommand's wait30sCONTAINERD_SHIM_RUNHCS_V1_TASK_CLOSE_TIMEOUThcsTask.DeleteExec30s, or derivedDefaults are unchanged, so behaviour is identical unless a host opts in.
delete.gotakes the same bound rather than a knob of its own: it waits on thesame host-side work, and it runs precisely when the shim died with a container
still going down - so if anything a filesystem-heavy container is more likely
to be on the other end of that wait.
The two knobs are coupled on purpose
DeleteExecwaits on the channel thatclose()closes. A task close timeoutbelow
close()'s worst case of2*teardowntherefore abandons a teardown thatis still making progress - which is exactly the outcome these knobs exist to
prevent. Raising only the teardown timeout would silently not help.
So when the task close timeout is not set explicitly and the teardown timeout
has been raised above the default, the task close timeout is derived as
2*teardown + 30s. Setting it explicitly always wins. Leaving both unsetreproduces today's behaviour exactly.
An empty, unparseable or non-positive value is treated as unset: resolution
happens during package initialization, before logging exists, so a bad value
must not stop the shim from starting.
Also
The PR logs how long a successful shutdown actually took. That duration is what
an operator needs in order to size the timeout for their workload, and it is
currently not observable at all.
Deliberately out of scope
SIGKILLpath: it guards the hosting UVM(
ht.host != nil) and plays no part in process isolated teardown.cmd/runhcs- a separate binary, and it already allows 5 minutes.cmd/containerd-shim-lcow-v2- a different shim, though it carries the same30 s pattern in its
manager.goand may deserve the same treatment.Why
Tearing down a Windows Server (process isolated) container is host-side work
whose cost scales with how much the container touched the filesystem: the layer
filter stack has to be detached and the container's registry hives flushed back
into its scratch. Most workloads finish in well under a second. Filesystem-heavy
ones do not.
Measured on the reference host: 117 s for a single OpenCV build container.
HcsShutDownComputeSystemreturned in milliseconds; the completion notificationarrived 117 s later (01:16:08 → 01:18:05 in the containerd debug log).
With the hardcoded limits, the shim gives up at 30 s + 30 s and terminates the
container while its scratch is still being written. The result is not a lost
container - it is a permanently damaged snapshot: every subsequent finalize
of it fails with
and the failure survives fresh snapshots and host reboots, because the unflushed
hive deltas live inside
sandbox.vhdxand are never completed.This is not a container-side problem. All silo processes do exit. Two
independent in-container mitigations were tried and both failed identically:
WaitToKillServiceTimeout(the shim injects2147483647) down to5 s in the payload - exit 0 published,
HcsShutDownComputeSystemreturned inmilliseconds, both notifications still lost, same
0x3.sccache --stop-server, killingmsdtcandAggregatorHost,Stop-Serviceon 11 non-essential services, with an exitdump proving they were gone - identical loss, identical
0x3.Host-side forensics matched: no zombie silos (the compute systems were
eventually cleaned up - teardown arrives too late, not never), and 22 orphaned
bindfltfilter instances inDetachedstate on deadVhdHardDiskvolumes,i.e. filter-stack teardown demonstrably not completing.
Why a fixed timeout is the wrong shape here
Worth stating explicitly, because it is the difference from the Linux side of
the same problem space: on Linux the analogous grace periods (
SIGTERM→ wait →SIGKILL) bound the processes inside the container, not a host-side storageoperation. The writable layer is an overlayfs
upperdirthat is already on disk;there is no end-of-life serialization step, so
umountcosts the same whetherthe container touched three files or three million, and killing at the wrong
moment cannot corrupt the layer.
WCOW has no equivalent property: hive flush is a durability-critical
serialization whose duration is unbounded in the workload. A fixed timeout on
such an operation is either too short or is not really a timeout. Making it
configurable is the smallest change that lets affected hosts stop losing data.
Verification
Built and checked against
81e2e01with Go 1.26.5,windows/amd64:go build ./cmd/containerd-shim-runhcs-v1succeeds.gofmt -lclean;go vet ./cmd/containerd-shim-runhcs-v1/clean.golangci-lint run --config .golangci.yml ./cmd/containerd-shim-runhcs-v1/...with the CI-pinned v2.11: 0 issues.
go test ./cmd/containerd-shim-runhcs-v1/passes - the whole package, notjust the new test.
Test_resolveTeardownTimeouts(7 cases: defaults,derivation, explicit override, each knob alone, malformed/negative, zero)
passes, and asserts the coupling invariant directly.
Functionally verified on the reference host. Five consecutive fresh
--no-cacheOpenCV container builds finalized and exported directly, no0x3on any of them:
CONTAINERD_SHIM_RUNHCS_V1_TEARDOWN_TIMEOUT=45mon the containerd serviceBefore the change that host had never once produced a direct OpenCV export; it
needed a workaround that avoided finalizing the snapshot at all, and direct
attempts failed with
0x3deterministically across retries. Run 4 shows the fixdoes not depend on warm runtime state. Run 5 exercises exactly the code in this
PR, including the environment plumbing.
Since the binary swap, containerd's log contains no
timed out while waiting for container ...entry at all, where the same log carried four such pairs onthe night before.
Two honest limits on that evidence. First, run 5 is a single data point for
the env-var path; a teardown that happened to complete under 30 s would look
identical to a working timeout, and the prior determinism of the failure is what
makes that unlikely rather than any direct measurement. Second, and more
tellingly: the teardown duration for these runs could not be measured at all.
The shim's shutdown path produces nothing in containerd's log at the levels this
host captures. The only reason a 117 s number exists is that the failing case
logged an error. That is precisely the gap the added duration log closes, and it
is why sizing this timeout is currently guesswork for anyone hitting it.
Host / repro environment
ltsc2025f5d08d5)Win11 24H2+ with
ltsc2025images in process isolation is an officiallysupported combination per the Microsoft version-compatibility documentation, so
this is not a host/image build mismatch.
Prior art
Nothing in this repo names
tearDownTimeoutor asks for it to be configurable -searched before filing. The neighbours, and how this differs:
#1554 Call
container.Terminate() on shutdown timeouts - introduced exactly the fallback
path this PR touches ("we weren't trying to force kill the container via
Terminate after if we timed out waiting for it to complete"). The path was
added deliberately; the fixed 30 s limit on it has not been revisited since,
and configurability was not discussed in review.
graceful termination of servercore containers - precedent for adjusting WCOW
termination timing to dodge a fixed platform timeout.
for notification (open since 2021) - same symptom family, but on
Docker/WS2019/Kubernetes, with no layer-level aftermath reported.
freeze at exportLayer phase - a hang in
os.RemoveAllduring export; adifferent mechanism, not a permanently unexportable scratch.
Process Isolation ws2025 - container fails to shutdown gracefully - the same
underlying phenomenon (ltsc2025 process isolation, ~10 min shutdown, resources
left locked, reboot required), reported as a slow-shutdown annoyance and
closed unresolved. It does not mention the shim timeout or
ExportLayer 0x3.That report also saw it with a matched 26100/26100 build, which is why a
host/image build mismatch can be ruled out.
The contribution here is the causal chain none of them connect: slow ltsc2025
process-isolated teardown → fixed 30 s → terminate mid-flush → permanently
unexportable scratch - plus the first measurement of how long that teardown
actually takes.
Notes for reviewers
runhcsopts.Optionsfield to keepthe change small and avoid regenerating the proto. If a runtime option in
containerd's config, or an OCI annotation for per-container control, is
preferred, say so and I will rework it - the mechanism is not the point.
fits a dedicated build host, which is the case this comes from, but on a node
running mixed workloads there is no way to grant the long timeout only to the
containers that need it. An OCI annotation would give per-container control
and is the natural answer if that matters to you.
is to leave the two knobs independent and merely document that they are
coupled - but then raising only the obvious one silently does nothing, which
seemed like the worse failure mode. Happy to invert it if you disagree.
leave a snapshot unrecoverable at all. Making the limits configurable is the
smallest change that stops the data loss today.