Summary
HandleRepoSubmit (projects.go:31) performs the entire deployment pipeline synchronously before returning an HTTP response. Any Cloudflare proxy, load balancer, or browser upstream will time out long before the deploy completes — the user sees a failure, retries, and the system spawns a duplicate container even though the first deploy succeeded.
Timeline — single deploy request
| Phase |
Duration |
| git clone + stack detection |
~5–15s |
| docker build |
~30–90s (Node can be longer) |
temp container + time.Sleep(5s) |
~10s |
Azure NSG PollUntilDone |
10–30s |
WaitForResponse (polls deploy-responses.json) |
up to 60s |
Total: easily 2–3 minutes. Cloudflare default gateway timeout: 100s.
Steps to reproduce
- Submit a repo that requires a non-trivial Node build (e.g. a Next.js app with many dependencies).
- Observe the browser or Cloudflare returning a 524/504 timeout.
- User retries. A second container is deployed; the first is still running.
deploy-responses.json now has entries from both runs; port reservations in Mongo are duplicated.
Root cause
All of the following run inline inside the HTTP handler, holding the connection open:
HandleRepoSubmit (projects.go:31)
├── git clone + stack detection
├── docker build ← unbounded; Node can take 90s+
├── docker run (temp)
├── time.Sleep(5s) ← hard-coded probe delay
├── port detection
├── Mongo reservation
├── Azure NSG PollUntilDone ← 10–30s, network I/O
├── docker run (final)
└── WaitForResponse() ← polls deploy-responses.json, 60s max
No deduplication or idempotency key exists on the deploy path. A retry issued after a proxy timeout will re-run every step regardless of whether the first run is still in progress.
Impact
- Any real-world Node/Python build exceeds Cloudflare's 100s gateway timeout.
- Retries create orphaned containers consuming ports and memory.
- Duplicate Mongo port reservations corrupt subsequent deploys on those ports.
- Users have no feedback between submission and timeout — perceived reliability is zero.
Proposed fix
- Decouple — accept the request, enqueue a job, return a
202 Accepted with a deploy_id immediately. Run the pipeline in a background goroutine or worker queue.
- Poll / stream status — expose a
GET /deploys/{deploy_id}/status endpoint (or SSE stream) the client can poll; remove WaitForResponse from the handler entirely.
- Idempotency — gate job creation on a
deploy_id or content hash so duplicate submissions within a window are deduplicated before any Docker work begins.
- Remove
time.Sleep(5s) — replace with an active health-check loop with a configurable timeout and backoff.
- Fence NSG calls —
PollUntilDone should run in the background job, not block the HTTP response path.
Acceptance criteria
POST /repos returns within 500ms regardless of build duration.
- Submitting the same repo twice within a deploy window results in exactly one container.
- Client can query deploy status and retrieve logs without holding an HTTP connection open.
Summary
HandleRepoSubmit(projects.go:31) performs the entire deployment pipeline synchronously before returning an HTTP response. Any Cloudflare proxy, load balancer, or browser upstream will time out long before the deploy completes — the user sees a failure, retries, and the system spawns a duplicate container even though the first deploy succeeded.Timeline — single deploy request
time.Sleep(5s)PollUntilDoneWaitForResponse(pollsdeploy-responses.json)Total: easily 2–3 minutes. Cloudflare default gateway timeout: 100s.
Steps to reproduce
deploy-responses.jsonnow has entries from both runs; port reservations in Mongo are duplicated.Root cause
All of the following run inline inside the HTTP handler, holding the connection open:
No deduplication or idempotency key exists on the deploy path. A retry issued after a proxy timeout will re-run every step regardless of whether the first run is still in progress.
Impact
Proposed fix
202 Acceptedwith adeploy_idimmediately. Run the pipeline in a background goroutine or worker queue.GET /deploys/{deploy_id}/statusendpoint (or SSE stream) the client can poll; removeWaitForResponsefrom the handler entirely.deploy_idor content hash so duplicate submissions within a window are deduplicated before any Docker work begins.time.Sleep(5s)— replace with an active health-check loop with a configurable timeout and backoff.PollUntilDoneshould run in the background job, not block the HTTP response path.Acceptance criteria
POST /reposreturns within 500ms regardless of build duration.