Skip to content

Commit c414636

Browse files
stubbiclaude
andauthored
feat(availability): configurable terminationGracePeriodSeconds + preStop for server drain (#106)
feat: configurable server termination grace + preStop drain hook The Paperclip server pod hardcoded terminationGracePeriodSeconds=30 with no preStop hook, so a deploy/rollout SIGKILLed in-flight agent runs 30s after SIGTERM -- far shorter than a typical multi-minute run. - api: add AvailabilitySpec.TerminationGracePeriodSeconds and a ServerDrain block (Enabled default true, TimeoutSeconds) to the Instance CRD. - podtemplate: source the server pod grace from the spec, defaulting HIGH (1800s, matching the 30-min PAPERCLIP_HEARTBEAT_REAP_STALE_MS run window) so a rollout never kills an active run. The grace is a ceiling, not a fixed wait -- an idle pod whose server exits promptly still terminates fast. - statefulset: add a gated preStop hook (sleep, default 15s, clamped to grace-1) on the server container that deregisters the terminating pod from the Service before SIGTERM, closing the endpoint-removal race. Shared by the Deployment workload path via BuildServerPodTemplate. - tests: default/configured grace, default/disabled/custom/zero/clamped preStop, and the Deployment path. Scope note: the preStop delivers the traffic-cutover half. Making in-flight runs FINISH also requires the server image to soft-drain on SIGTERM; the current fork handler hard-interrupts and requeues runs on SIGTERM, so the higher grace only takes full effect once that soft-drain lands. Regenerated CRDs/deepcopy/docs and synced the Helm chart CRD templates. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d459426 commit c414636

9 files changed

Lines changed: 428 additions & 1 deletion

File tree

api/v1alpha1/paperclipinstance_types.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,52 @@ type AvailabilitySpec struct {
12471247
// TopologySpreadConstraints specifies topology spread constraints.
12481248
// +optional
12491249
TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"`
1250+
1251+
// TerminationGracePeriodSeconds is the pod termination grace period for the
1252+
// Paperclip server pod. On shutdown (deploy/rollout/node drain) the kubelet
1253+
// sends SIGTERM and waits up to this many seconds before SIGKILL. The server
1254+
// uses this window to let in-flight agent runs finish (soft-drain) instead of
1255+
// being interrupted mid-run. When unset the operator defaults it high (see
1256+
// DefaultServerTerminationGracePeriodSeconds) so a rollout never kills an active
1257+
// run; the previous hardcoded 30s was far shorter than a typical multi-minute
1258+
// agent run. The grace period is a ceiling, not a fixed wait: the pod terminates
1259+
// as soon as the server exits, so a high value only delays SIGKILL for pods that
1260+
// still have work to drain.
1261+
// +kubebuilder:validation:Minimum=0
1262+
// +optional
1263+
TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"`
1264+
1265+
// ServerDrain configures graceful draining of in-flight agent runs when the
1266+
// Paperclip server pod is shutting down.
1267+
// +optional
1268+
ServerDrain *ServerDrainSpec `json:"serverDrain,omitempty"`
1269+
}
1270+
1271+
// ServerDrainSpec configures how the Paperclip server pod drains in-flight agent
1272+
// runs on shutdown. The server itself soft-drains on SIGTERM (it stops accepting
1273+
// new work and waits for active runs to finish within the pod's termination grace
1274+
// period). This spec adds an optional container preStop hook that holds the
1275+
// container in the "Terminating" state for a short, bounded window BEFORE SIGTERM
1276+
// is delivered, so the pod's endpoints are deregistered from the Service first.
1277+
// That prevents new requests from being routed to a pod that is about to drain,
1278+
// which would otherwise race the soft-drain.
1279+
type ServerDrainSpec struct {
1280+
// Enabled controls whether the server container gets a preStop drain hook.
1281+
// The soft-drain on SIGTERM is a property of the server image and is unaffected
1282+
// by this toggle; disabling only removes the pre-SIGTERM endpoint-deregistration
1283+
// delay. Defaults to true.
1284+
// +kubebuilder:default=true
1285+
// +optional
1286+
Enabled *bool `json:"enabled,omitempty"`
1287+
1288+
// TimeoutSeconds bounds how long the preStop hook sleeps before the kubelet
1289+
// delivers SIGTERM, giving Endpoints/EndpointSlice controllers time to remove
1290+
// this pod from the Service so in-flight-only traffic remains. Must be shorter
1291+
// than terminationGracePeriodSeconds (which also has to cover the SIGTERM
1292+
// soft-drain that follows). Defaults to DefaultServerDrainTimeoutSeconds.
1293+
// +kubebuilder:validation:Minimum=0
1294+
// +optional
1295+
TimeoutSeconds *int64 `json:"timeoutSeconds,omitempty"`
12501296
}
12511297

12521298
// PDBSpec configures a PodDisruptionBudget.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/paperclip-operator/templates/crds/paperclip.inc_instances.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,45 @@ spec:
16611661
format: int32
16621662
minimum: 1
16631663
type: integer
1664+
serverDrain:
1665+
description: |-
1666+
ServerDrain configures graceful draining of in-flight agent runs when the
1667+
Paperclip server pod is shutting down.
1668+
properties:
1669+
enabled:
1670+
default: true
1671+
description: |-
1672+
Enabled controls whether the server container gets a preStop drain hook.
1673+
The soft-drain on SIGTERM is a property of the server image and is unaffected
1674+
by this toggle; disabling only removes the pre-SIGTERM endpoint-deregistration
1675+
delay. Defaults to true.
1676+
type: boolean
1677+
timeoutSeconds:
1678+
description: |-
1679+
TimeoutSeconds bounds how long the preStop hook sleeps before the kubelet
1680+
delivers SIGTERM, giving Endpoints/EndpointSlice controllers time to remove
1681+
this pod from the Service so in-flight-only traffic remains. Must be shorter
1682+
than terminationGracePeriodSeconds (which also has to cover the SIGTERM
1683+
soft-drain that follows). Defaults to DefaultServerDrainTimeoutSeconds.
1684+
format: int64
1685+
minimum: 0
1686+
type: integer
1687+
type: object
1688+
terminationGracePeriodSeconds:
1689+
description: |-
1690+
TerminationGracePeriodSeconds is the pod termination grace period for the
1691+
Paperclip server pod. On shutdown (deploy/rollout/node drain) the kubelet
1692+
sends SIGTERM and waits up to this many seconds before SIGKILL. The server
1693+
uses this window to let in-flight agent runs finish (soft-drain) instead of
1694+
being interrupted mid-run. When unset the operator defaults it high (see
1695+
DefaultServerTerminationGracePeriodSeconds) so a rollout never kills an active
1696+
run; the previous hardcoded 30s was far shorter than a typical multi-minute
1697+
agent run. The grace period is a ceiling, not a fixed wait: the pod terminates
1698+
as soon as the server exits, so a high value only delays SIGKILL for pods that
1699+
still have work to drain.
1700+
format: int64
1701+
minimum: 0
1702+
type: integer
16641703
tolerations:
16651704
description: Tolerations specifies pod tolerations.
16661705
items:

config/crd/bases/paperclip.inc_instances.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,45 @@ spec:
16551655
format: int32
16561656
minimum: 1
16571657
type: integer
1658+
serverDrain:
1659+
description: |-
1660+
ServerDrain configures graceful draining of in-flight agent runs when the
1661+
Paperclip server pod is shutting down.
1662+
properties:
1663+
enabled:
1664+
default: true
1665+
description: |-
1666+
Enabled controls whether the server container gets a preStop drain hook.
1667+
The soft-drain on SIGTERM is a property of the server image and is unaffected
1668+
by this toggle; disabling only removes the pre-SIGTERM endpoint-deregistration
1669+
delay. Defaults to true.
1670+
type: boolean
1671+
timeoutSeconds:
1672+
description: |-
1673+
TimeoutSeconds bounds how long the preStop hook sleeps before the kubelet
1674+
delivers SIGTERM, giving Endpoints/EndpointSlice controllers time to remove
1675+
this pod from the Service so in-flight-only traffic remains. Must be shorter
1676+
than terminationGracePeriodSeconds (which also has to cover the SIGTERM
1677+
soft-drain that follows). Defaults to DefaultServerDrainTimeoutSeconds.
1678+
format: int64
1679+
minimum: 0
1680+
type: integer
1681+
type: object
1682+
terminationGracePeriodSeconds:
1683+
description: |-
1684+
TerminationGracePeriodSeconds is the pod termination grace period for the
1685+
Paperclip server pod. On shutdown (deploy/rollout/node drain) the kubelet
1686+
sends SIGTERM and waits up to this many seconds before SIGKILL. The server
1687+
uses this window to let in-flight agent runs finish (soft-drain) instead of
1688+
being interrupted mid-run. When unset the operator defaults it high (see
1689+
DefaultServerTerminationGracePeriodSeconds) so a rollout never kills an active
1690+
run; the previous hardcoded 30s was far shorter than a typical multi-minute
1691+
agent run. The grace period is a ceiling, not a fixed wait: the pod terminates
1692+
as soon as the server exits, so a high value only delays SIGKILL for pods that
1693+
still have work to drain.
1694+
format: int64
1695+
minimum: 0
1696+
type: integer
16581697
tolerations:
16591698
description: Tolerations specifies pod tolerations.
16601699
items:

docs/api-reference.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ _Appears in:_
216216
| `affinity` _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#affinity-v1-core)_ | Affinity specifies pod affinity rules. | | Optional: \{\} <br /> |
217217
| `priorityClassName` _string_ | PriorityClassName sets the scheduling PriorityClass on the product pod so<br />it can preempt lower-priority workloads instead of sitting Pending when the<br />node pool is full. Leave empty for the cluster default priority. | | Optional: \{\} <br /> |
218218
| `topologySpreadConstraints` _[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#topologyspreadconstraint-v1-core) array_ | TopologySpreadConstraints specifies topology spread constraints. | | Optional: \{\} <br /> |
219+
| `terminationGracePeriodSeconds` _integer_ | TerminationGracePeriodSeconds is the pod termination grace period for the<br />Paperclip server pod. On shutdown (deploy/rollout/node drain) the kubelet<br />sends SIGTERM and waits up to this many seconds before SIGKILL. The server<br />uses this window to let in-flight agent runs finish (soft-drain) instead of<br />being interrupted mid-run. When unset the operator defaults it high (see<br />DefaultServerTerminationGracePeriodSeconds) so a rollout never kills an active<br />run; the previous hardcoded 30s was far shorter than a typical multi-minute<br />agent run. The grace period is a ceiling, not a fixed wait: the pod terminates<br />as soon as the server exits, so a high value only delays SIGKILL for pods that<br />still have work to drain. | | Minimum: 0 <br />Optional: \{\} <br /> |
220+
| `serverDrain` _[ServerDrainSpec](#serverdrainspec)_ | ServerDrain configures graceful draining of in-flight agent runs when the<br />Paperclip server pod is shutting down. | | Optional: \{\} <br /> |
219221

220222

221223
#### BackupS3Spec
@@ -655,6 +657,7 @@ _Appears in:_
655657
| `backend` _string_ | Backend selects the per-run workload primitive. "job" runs each agent as a<br />fire-and-forget batch/v1 Job (log-scraped output); "sandbox-cr" creates a<br />long-lived agent-sandbox CR (agents.x-k8s.io) that the server execs into.<br />Maps to PAPERCLIP_K8S_BACKEND. | job | Enum: [job sandbox-cr] <br />Optional: \{\} <br /> |
656658
| `runtimeClassName` _string_ | RuntimeClassName is the RuntimeClass applied to agent pods (e.g. "gvisor")<br />for an extra kernel-isolation boundary. Maps to<br />PAPERCLIP_K8S_RUNTIME_CLASS_NAME. When set, the execution ClusterRole also<br />grants get/use on the named RuntimeClass. | | Optional: \{\} <br /> |
657659
| `egressMode` _string_ | EgressMode selects how per-tenant egress is enforced. "standard" uses plain<br />NetworkPolicy (CIDR-only, cannot match FQDNs); "cilium" uses a<br />CiliumNetworkPolicy for exact FQDN allow-listing (requires the Cilium CNI).<br />Maps to PAPERCLIP_K8S_EGRESS_MODE. | | Enum: [standard cilium] <br />Optional: \{\} <br /> |
660+
| `egressPolicy` _string_ | EgressPolicy selects the overall egress posture for tenant sandboxes.<br />"allowlist" (default) restricts egress to EgressAllowFQDNs/EgressAllowCIDRs.<br />"open-internet" allows public internet on ports 80/443 while blocking<br />private ranges, link-local metadata, and CGNAT. | allowlist | Enum: [allowlist open-internet] <br />Optional: \{\} <br /> |
658661
| `egressAllowFQDNs` _string array_ | EgressAllowFQDNs is the list of fully-qualified domain names tenant agent<br />pods may reach (e.g. the LLM gateway and required APIs). Enforced exactly<br />only under EgressMode "cilium". Maps to PAPERCLIP_K8S_EGRESS_ALLOW_FQDNS<br />(comma-separated). | | Optional: \{\} <br /> |
659662
| `egressAllowCIDRs` _string array_ | EgressAllowCIDRs is the list of CIDR blocks tenant agent pods may reach, in<br />addition to (or as the standard-mode substitute for) the FQDN allow-list.<br />Maps to PAPERCLIP_K8S_EGRESS_ALLOW_CIDRS (comma-separated). | | Optional: \{\} <br /> |
660663
| `namespacePrefix` _string_ | NamespacePrefix is prepended to each derived per-tenant namespace name,<br />letting multiple instances share a cluster without namespace collisions.<br />Maps to PAPERCLIP_K8S_NAMESPACE_PREFIX. | | Optional: \{\} <br /> |
@@ -1152,6 +1155,30 @@ _Appears in:_
11521155
| `allowedActions` _[SelfConfigAction](#selfconfigaction) array_ | AllowedActions restricts which action categories the agent can perform.<br />If empty and enabled is true, no actions are allowed (fail-safe). | | Enum: [plugins config envVars] <br />MaxItems: 3 <br />Optional: \{\} <br /> |
11531156

11541157

1158+
#### ServerDrainSpec
1159+
1160+
1161+
1162+
ServerDrainSpec configures how the Paperclip server pod drains in-flight agent
1163+
runs on shutdown. The server itself soft-drains on SIGTERM (it stops accepting
1164+
new work and waits for active runs to finish within the pod's termination grace
1165+
period). This spec adds an optional container preStop hook that holds the
1166+
container in the "Terminating" state for a short, bounded window BEFORE SIGTERM
1167+
is delivered, so the pod's endpoints are deregistered from the Service first.
1168+
That prevents new requests from being routed to a pod that is about to drain,
1169+
which would otherwise race the soft-drain.
1170+
1171+
1172+
1173+
_Appears in:_
1174+
- [AvailabilitySpec](#availabilityspec)
1175+
1176+
| Field | Description | Default | Validation |
1177+
| --- | --- | --- | --- |
1178+
| `enabled` _boolean_ | Enabled controls whether the server container gets a preStop drain hook.<br />The soft-drain on SIGTERM is a property of the server image and is unaffected<br />by this toggle; disabling only removes the pre-SIGTERM endpoint-deregistration<br />delay. Defaults to true. | true | Optional: \{\} <br /> |
1179+
| `timeoutSeconds` _integer_ | TimeoutSeconds bounds how long the preStop hook sleeps before the kubelet<br />delivers SIGTERM, giving Endpoints/EndpointSlice controllers time to remove<br />this pod from the Service so in-flight-only traffic remains. Must be shorter<br />than terminationGracePeriodSeconds (which also has to cover the SIGTERM<br />soft-drain that follows). Defaults to DefaultServerDrainTimeoutSeconds. | | Minimum: 0 <br />Optional: \{\} <br /> |
1180+
1181+
11551182
#### ServiceMonitorSpec
11561183

11571184

0 commit comments

Comments
 (0)