Skip to content

Commit 3e1a2c1

Browse files
committed
fix: enforce CON-1751 runtime deadlines with focused regressions
1 parent 2a499f5 commit 3e1a2c1

12 files changed

Lines changed: 116 additions & 581 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
### Bug Fixes
1010

11-
* Wait for task cleanup after interruption, preserve cancellation and task errors, recover in-flight pod creation, and retry transient task deletion failures without cancelling detached work.
11+
* Task calls complete bounded interruption cleanup and carry absolute runtime-enforced lifetime deadlines. Failed deletion retains the pod ID for recovery.
1212
* Network volume creation intersects catalog storage support with every sharing resource's hardware stock and datacenter constraints. Unsupported explicit pins fail before creation; existing volumes retain their authoritative IDs and datacenters.
1313
* Deployment artifacts apply project ignore rules and mandatory secret-file exclusions while preserving vendored dependency assets.
1414
* Synchronous calls wait across polling timeouts on Python 3.10 and propagate operation timeouts.

‎docs/cli/references/projects.md‎

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,24 @@ Create a `.runpodignore` file in the root of your project to ignore files and fo
1414

1515
### Flash deployment artifacts
1616

17-
`rp flash deploy` and `rp flash deploy --build-only` apply git-style patterns to
18-
source files, using `pathspec`. Precedence, from lowest to highest, is:
17+
`rp flash deploy` and `rp flash deploy --build-only` apply Git-style ignore
18+
patterns in this order, with later rules taking precedence:
1919

20-
1. Default local-file exclusions: virtual environments, Python caches, test
21-
directories and test modules, `node_modules`, `.DS_Store`, and `*.tar.gz`.
22-
2. `.gitignore` files within the project, with deeper files overriding parent
23-
rules for their subtree.
20+
1. Default exclusions for local environments, caches, tests, and build archives.
21+
2. Project `.gitignore` files, with deeper files overriding parents in their subtree.
2422
3. The project-root `.runpodignore`.
2523

26-
Ancestor and global Git ignore files are not read. Within each ignore file, the
27-
last matching rule wins. `/` anchors a pattern to that ignore file's directory;
28-
trailing `/` matches directories; `**`, comments, escaped characters and `!`
29-
negation follow Git ignore syntax. Excluded directories are not traversed, so
30-
re-include the parent before its files. For example, to deploy a fixture from
31-
the otherwise excluded `tests` directory:
32-
33-
```gitignore
34-
!tests/
35-
tests/*
36-
!tests/fixture.json
37-
```
38-
39-
Some safeguards cannot be overridden by negation: `.git`, `.runpod`, `.flash`,
40-
the root `env/` and `runpod_manifest.json` paths, and credential-like source
41-
paths. Credential safeguards include `.env` and its variants, `*.env` and its
42-
variants, `*.pem`, `*.key`, SSH private-key names, `.ssh`, `.aws`, `.azure`,
43-
`.kube`, `.docker/config.json`, `.netrc`, `.npmrc`, `.pypirc`, `.git-credentials`,
44-
`.boto`, `credentials`/`credentials.*`, `secrets`/`secrets.*`, and
45-
`service-account*.json`/`service_account*.json`. Use worker environment variables
46-
or a secret store for credentials instead of packaging them with source.
47-
48-
These are filename safeguards, not secret scanning: credentials embedded in
49-
ordinary source or differently named files can still be uploaded. Review the
50-
build-only artifact before deployment.
51-
52-
The generated manifest and vendored `env/` are added separately and cannot be
53-
replaced by source files. Source ignore rules, including the PEM/key safeguards,
54-
do not apply to vendored dependencies, so dependency CA bundles are preserved.
55-
The output artifact itself and the dependency build directory are never copied
56-
back into source. Source and dependency symlinks are omitted, including
57-
project-contained links and linked ignore files; directory links are not
58-
traversed. Hardlinked regular files are stored as independent regular members,
59-
so the archive requires no link extraction support.
24+
Ancestor and global Git ignores are not read. Negation (`!`) can re-include
25+
ordinary files, but excluded parent directories must also be re-included.
26+
27+
Negation cannot include `.git`, `.runpod`, `.flash`, the root `env/` or
28+
`runpod_manifest.json`, or credential-like source paths such as `.env` variants,
29+
PEM/key files, private SSH keys, cloud credential directories, and credentials,
30+
secrets, or service-account files. These are filename safeguards, not secret
31+
scanning; review the build-only artifact and supply credentials through worker
32+
environment variables or a secret store.
33+
34+
The manifest and vendored `env/` are added separately. Source ignore rules do
35+
not strip dependency CA bundles. Symlinks are omitted from source and
36+
dependencies; the output artifact and dependency build directory are never
37+
copied back into source.

‎examples/apps/README.md‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,8 @@ including timeout, task failure, or cancellation. use `await job.cancel()` to
2525
abandon a spawned task explicitly. leaving the client without waiting or cancelling
2626
does not cancel intentional detached work.
2727

28-
cancellation gives cleanup up to 30 seconds to recover an in-flight create response
29-
and delete the pod; synchronous calls allow up to 35 seconds for the coroutine to
30-
acknowledge cleanup after ctrl-c. repeated interrupts do not interrupt deletion.
31-
transient delete failures are retried up to three times. cleanup errors do not
32-
replace an existing task error or cancellation, and failed deletion retains
33-
`job.pod_id` for recovery. cleanup that exceeds the grace period continues only
34-
while the client's event loop remains alive. check the pod in the console after a
35-
cleanup warning; client process exit alone does not stop billing.
28+
cancellation waits for bounded cleanup and retries transient deletion failures.
29+
failed deletion retains `job.pod_id`; check the console after a cleanup warning.
30+
each task carries an absolute one-hour deadline that the runtime checks even during
31+
active work. polling and container restarts do not extend it. pod deletion still
32+
requires a working control-plane API and pod-scoped credentials.

‎runpod/apps/targets.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,6 @@ class PodTarget(InvocationTarget):
806806
runner on the pod (see runpod.apps.tasks).
807807
"""
808808

809-
# tasks default to a long window; the pod's terminateAfter is the backstop
810809
TASK_TIMEOUT_SECONDS = 3600.0
811810

812811
def __init__(

‎runpod/apps/tasks.py‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
3. POST the FunctionRequest to /execute (remote) or /submit (spawn)
88
4. collect the response, terminate the pod
99
10-
`terminateAfter` is set at deploy time as a server-side safety net so a
11-
crashed client cannot leak a running pod indefinitely.
10+
the runtime watchdog requests pod deletion at the absolute task deadline,
11+
including when the client has exited or the function is still running.
1212
"""
1313

1414
import asyncio
@@ -17,7 +17,7 @@
1717
import secrets
1818
import sys
1919
import time
20-
from datetime import datetime, timedelta, timezone
20+
from datetime import timedelta
2121
from typing import Any, Dict, List, Optional
2222

2323
import aiohttp
@@ -31,7 +31,6 @@
3131

3232
TASK_PORT = 8080
3333

34-
# requested server-side deadline for abandoned tasks
3534
DEFAULT_MAX_LIFETIME = timedelta(hours=1)
3635

3736
READY_POLL_INTERVAL = 2.0
@@ -133,14 +132,14 @@ def _pod_input(spec: ResourceSpec, token: str, task_name: str) -> Dict[str, Any]
133132
start the runtime package via dockerArgs.
134133
"""
135134
spec.validate()
136-
terminate_after = (datetime.now(timezone.utc) + DEFAULT_MAX_LIFETIME).isoformat()
137135

138136
from .secret import render_env
139137

140138
env = {
141139
"RUNPOD_TASK_TOKEN": token,
142140
"RUNPOD_TASK_PORT": str(TASK_PORT),
143141
**render_env(spec.env),
142+
"RUNPOD_TASK_DEADLINE": str(time.time() + DEFAULT_MAX_LIFETIME.total_seconds()),
144143
}
145144

146145
from .images import image_for_spec, local_python_version
@@ -150,7 +149,6 @@ def _pod_input(spec: ResourceSpec, token: str, task_name: str) -> Dict[str, Any]
150149
"imageName": image_for_spec(spec, python_version=local_python_version()),
151150
"ports": f"{TASK_PORT}/http",
152151
"containerDiskInGb": spec.container_disk_gb or (10 if spec.is_cpu else 30),
153-
"terminateAfter": terminate_after,
154152
"supportPublicIp": True,
155153
}
156154

‎tests/test_apps/test_api_client.py‎

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -514,37 +514,14 @@ async def test_any_supported_tier_is_eligible(self):
514514
{"id": "US-IL-1", "networkVolumeTypes": []},
515515
{"id": "EU-RO-1", "networkVolumeTypes": ["STANDARD"]},
516516
{"id": "US-KS-2", "networkVolumeTypes": ["PREMIUM"]},
517-
{"id": "US-MO-2", "networkVolumeTypes": ["STANDARD", "PREMIUM"]},
518517
]
519518
}
520-
with patch(
521-
"runpod.apps.api.run_rest_request_async",
522-
AsyncMock(return_value=catalog),
523-
) as rest:
524-
supported = await AppsApiClient(
525-
api_key="test-key"
526-
).network_volume_datacenters()
527-
assert supported == {"EU-RO-1", "US-KS-2", "US-MO-2"}
528-
rest.assert_awaited_once_with(
529-
"GET", "/v2/catalog/datacenters", api_key="test-key"
530-
)
531-
532-
@pytest.mark.parametrize(
533-
"catalog",
534-
[
535-
{},
536-
{"dataCenters": None},
537-
{"dataCenters": [{"id": "EU-RO-1"}]},
538-
{"dataCenters": [{"id": "EU-RO-1", "networkVolumeTypes": "STANDARD"}]},
539-
],
540-
)
541-
async def test_missing_capability_is_not_assumed_available(self, catalog):
542519
with patch(
543520
"runpod.apps.api.run_rest_request_async",
544521
AsyncMock(return_value=catalog),
545522
):
546-
with pytest.raises(QueryError):
547-
await AppsApiClient().network_volume_datacenters()
523+
supported = await AppsApiClient().network_volume_datacenters()
524+
assert supported == {"EU-RO-1", "US-KS-2"}
548525

549526

550527
class TestVolumesRegistrySecrets:

0 commit comments

Comments
 (0)