Skip to content

fix(app): tear down serverless endpoints on flash app delete - #371

Open
justinwlin wants to merge 3 commits into
mainfrom
fix/367-app-delete-removes-endpoint
Open

fix(app): tear down serverless endpoints on flash app delete#371
justinwlin wants to merge 3 commits into
mainfrom
fix/367-app-delete-removes-endpoint

Conversation

@justinwlin

@justinwlin justinwlin commented Aug 25, 2026

Copy link
Copy Markdown

Problem

Fixes #367 · Internal: CON-1232

flash app delete removed the app record, printed an unqualified success message, and exited 0 — while the serverless endpoints it deployed kept running and stayed billable. The orphans are invisible to flash app list (the app record is genuinely gone) and only show up in runpodctl or the console, so CI teardown loops accumulate them silently.

Fix

Delete the endpoints as part of app delete, before the app record is removed. Endpoints are discovered server-side from each flash environment, so this works without local resource tracking (e.g. in CI).

If any endpoint cannot be removed, the app record is kept, the command exits 1, and each survivor is named with its remediation:

endpoint jayf2t4qi40v9r not removed; delete it with runpodctl serverless delete jayf2t4qi40v9r

Verified

Check Result
pytest tests/unit/cli/test_apps.py tests/unit/core/resources/test_app.py 61 passed
pytest tests/unit/cli/ tests/unit/resources/test_app.py tests/unit/core/resources/test_app.py 630 passed
ruff check + ruff format --check, touched files clean
mypy, touched files no new errors
mypy note

12 pre-existing errors in core/resources/app.py; typecheck is opt-in via quality-check-strict.

What changed

  • core/resources/app.py — new FlashApp.delete_endpoints(). Lists the app's environments server-side, deletes every registered endpoint via the existing GraphQL deleteEndpoint helper, returns (removed, failed). Already-deleted endpoints count as removed (mirrors ServerlessResource._do_undeploy), so retry after a partial teardown is safe.
  • cli/commands/apps.pydelete_flash_app runs teardown first, prints each deleted endpoint, and on partial failure blocks the app deletion with named endpoints, remediation, and a non-zero exit.
  • Tests — extended delete coverage in tests/unit/cli/test_apps.py (endpoints removed; endpoint failure blocks app delete and prints remediation) and added TestFlashAppDeleteEndpoints in tests/unit/core/resources/test_app.py (multi-env teardown, failure reporting, idempotent retry, id-less endpoint guard).

Live testing

2026-08-25, real Runpod account. Fixture: app con370-live, endpoint 4kuwiutqisjt2f, deployed via #370's build from runpod/serverless-hello-world:0.4.1.

$ flash app delete con370-live
✓ deleted endpoint con370-live-1248  4kuwiutqisjt2f
✓ deleted app con370-live
  • GET /v1/endpoints/4kuwiutqisjt2f404 endpoint not found. Actually deleted, not just unregistered — the exact silent-orphan case from the issue.
  • Account diff before/after: the 10 pre-existing endpoints were identical. Nothing else touched.
  • Repeat delete exits 1 with FlashAppNotFoundError.

Out of scope

  • Network volumes registered to environments. The issue is about billable serverless endpoints.
  • Local tracking. Endpoints deleted here are untracked from any .flash/resources.pkl; flash undeploy --cleanup-stale already handles stale local state.
  • FlashAppNotFoundError renders as a full traceback. Verified pre-existing on a build without this PR, not a regression. Cheap follow-up: catch it in the CLI and print app 'x' not found.

flash app delete removed the app record but left the deployed serverless
endpoints running and billable. The orphans were invisible to flash app
list (record gone) and accumulated silently in CI.

Discover endpoints server-side from each environment and delete them
before removing the app record. On any endpoint failure, keep the app
record, name the endpoint, print the runpodctl remediation, and exit
non-zero. Endpoints already deleted are treated as removed so the
command is safe to retry after a partial teardown.
test_deploy_all_background spawned an untracked daemon thread that could
lose the scheduling race and run the real ResourceManager after the mock
patch and fixtures unwound. Its MagicMock(spec=ServerlessResource)
resources were then cached for cleanup, and _save_resources failed to
cloudpickle spec'd mocks ("args[0] from __newobj__ args has the wrong
class"), truncating the shared state file and breaking unrelated tests
downstream (REG008 PicklingError / "Ran out of input").

deploy_all_background now returns the spawned thread so callers can
await completion, and the test joins it while the get_or_deploy_resource
patch is still active, making the cross-test pollution structurally
impossible rather than a scheduling lottery.
- Extract shared _delete_endpoint_idempotent helper into
  core/api/runpod.py; ServerlessResource._do_undeploy and
  FlashApp.delete_endpoints both delegate to it
- Distinguish the apps.py failure message when endpoint teardown blocks
  app deletion ('could not remove all endpoints...; app was not deleted')
  from an actual app-record deletion failure
- Cover the id-less endpoint remediation branch in CLI tests, and the
  defensive delete_endpoints branches (None/missing environment detail,
  success=False-without-exception hard failure with no re-check)
- Pin the empty-list contract of deploy_all_background returning None
@justinwlin
justinwlin marked this pull request as ready for review August 25, 2026 19:29
@justinwlin
justinwlin requested review from KAJdev and deanq and removed request for deanq August 25, 2026 19:29

@KAJdev KAJdev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flash app delete reports success but leaves the serverless endpoint running and billable

2 participants