Payload
{
"ref": "refs/heads/main",
"before": "957f52dc0cb70a2262d6059107578c97e8ff4ae4",
"after": "afa2af707b77eca5bdec689488c2abcd3b92d534",
"repository": {
"id": 1182144886,
"node_id": "R_kgDORnYZdg",
"name": "dmcloud",
"full_name": "darkmatter/dmcloud",
"private": true,
"owner": {
"name": "darkmatter",
"email": "hello@dm.sh",
"login": "darkmatter",
"id": 17834193,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE3ODM0MTkz",
"avatar_url": "https://avatars.githubusercontent.com/u/17834193?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/darkmatter",
"html_url": "https://github.com/darkmatter",
"followers_url": "https://api.github.com/users/darkmatter/followers",
"following_url": "https://api.github.com/users/darkmatter/following{/other_user}",
"gists_url": "https://api.github.com/users/darkmatter/gists{/gist_id}",
"starred_url": "https://api.github.com/users/darkmatter/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/darkmatter/subscriptions",
"organizations_url": "https://api.github.com/users/darkmatter/orgs",
"repos_url": "https://api.github.com/users/darkmatter/repos",
"events_url": "https://api.github.com/users/darkmatter/events{/privacy}",
"received_events_url": "https://api.github.com/users/darkmatter/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"html_url": "https://github.com/darkmatter/dmcloud",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/darkmatter/dmcloud",
"forks_url": "https://api.github.com/repos/darkmatter/dmcloud/forks",
"keys_url": "https://api.github.com/repos/darkmatter/dmcloud/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/darkmatter/dmcloud/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/darkmatter/dmcloud/teams",
"hooks_url": "https://api.github.com/repos/darkmatter/dmcloud/hooks",
"issue_events_url": "https://api.github.com/repos/darkmatter/dmcloud/issues/events{/number}",
"events_url": "https://api.github.com/repos/darkmatter/dmcloud/events",
"assignees_url": "https://api.github.com/repos/darkmatter/dmcloud/assignees{/user}",
"branches_url": "https://api.github.com/repos/darkmatter/dmcloud/branches{/branch}",
"tags_url": "https://api.github.com/repos/darkmatter/dmcloud/tags",
"blobs_url": "https://api.github.com/repos/darkmatter/dmcloud/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/darkmatter/dmcloud/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/darkmatter/dmcloud/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/darkmatter/dmcloud/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/darkmatter/dmcloud/statuses/{sha}",
"languages_url": "https://api.github.com/repos/darkmatter/dmcloud/languages",
"stargazers_url": "https://api.github.com/repos/darkmatter/dmcloud/stargazers",
"contributors_url": "https://api.github.com/repos/darkmatter/dmcloud/contributors",
"subscribers_url": "https://api.github.com/repos/darkmatter/dmcloud/subscribers",
"subscription_url": "https://api.github.com/repos/darkmatter/dmcloud/subscription",
"commits_url": "https://api.github.com/repos/darkmatter/dmcloud/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/darkmatter/dmcloud/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/darkmatter/dmcloud/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/darkmatter/dmcloud/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/darkmatter/dmcloud/contents/{+path}",
"compare_url": "https://api.github.com/repos/darkmatter/dmcloud/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/darkmatter/dmcloud/merges",
"archive_url": "https://api.github.com/repos/darkmatter/dmcloud/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/darkmatter/dmcloud/downloads",
"issues_url": "https://api.github.com/repos/darkmatter/dmcloud/issues{/number}",
"pulls_url": "https://api.github.com/repos/darkmatter/dmcloud/pulls{/number}",
"milestones_url": "https://api.github.com/repos/darkmatter/dmcloud/milestones{/number}",
"notifications_url": "https://api.github.com/repos/darkmatter/dmcloud/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/darkmatter/dmcloud/labels{/name}",
"releases_url": "https://api.github.com/repos/darkmatter/dmcloud/releases{/id}",
"deployments_url": "https://api.github.com/repos/darkmatter/dmcloud/deployments",
"created_at": 1773551487,
"updated_at": "2026-05-10T05:09:21Z",
"pushed_at": 1780207339,
"git_url": "git://github.com/darkmatter/dmcloud.git",
"ssh_url": "git@github.com:darkmatter/dmcloud.git",
"clone_url": "https://github.com/darkmatter/dmcloud.git",
"svn_url": "https://github.com/darkmatter/dmcloud",
"homepage": null,
"size": 559,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Go",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"has_discussions": false,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": null,
"allow_forking": false,
"is_template": false,
"web_commit_signoff_required": false,
"has_pull_requests": true,
"pull_request_creation_policy": "all",
"topics": [],
"visibility": "private",
"forks": 0,
"open_issues": 0,
"watchers": 0,
"default_branch": "main",
"stargazers": 0,
"master_branch": "main",
"organization": "darkmatter",
"custom_properties": {}
},
"pusher": {
"name": "amacbride",
"email": "amacbride@users.noreply.github.com"
},
"forced": false,
"organization": {
"login": "darkmatter",
"id": 17834193,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE3ODM0MTkz",
"url": "https://api.github.com/orgs/darkmatter",
"repos_url": "https://api.github.com/orgs/darkmatter/repos",
"events_url": "https://api.github.com/orgs/darkmatter/events",
"hooks_url": "https://api.github.com/orgs/darkmatter/hooks",
"issues_url": "https://api.github.com/orgs/darkmatter/issues",
"members_url": "https://api.github.com/orgs/darkmatter/members{/member}",
"public_members_url": "https://api.github.com/orgs/darkmatter/public_members{/member}",
"avatar_url": "https://avatars.githubusercontent.com/u/17834193?v=4",
"description": ""
},
"enterprise": {
"id": 469843,
"slug": "darkmatter",
"name": "darkmatter",
"node_id": "E_kgDOAAcrUw",
"avatar_url": "https://avatars.githubusercontent.com/b/469843?v=4",
"description": "",
"website_url": "darkmatter.io",
"html_url": "https://github.com/enterprises/darkmatter",
"created_at": "2025-09-07T16:01:00Z",
"updated_at": "2026-05-09T15:34:55Z"
},
"sender": {
"login": "amacbride",
"id": 1263544,
"node_id": "MDQ6VXNlcjEyNjM1NDQ=",
"avatar_url": "https://avatars.githubusercontent.com/u/1263544?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/amacbride",
"html_url": "https://github.com/amacbride",
"followers_url": "https://api.github.com/users/amacbride/followers",
"following_url": "https://api.github.com/users/amacbride/following{/other_user}",
"gists_url": "https://api.github.com/users/amacbride/gists{/gist_id}",
"starred_url": "https://api.github.com/users/amacbride/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/amacbride/subscriptions",
"organizations_url": "https://api.github.com/users/amacbride/orgs",
"repos_url": "https://api.github.com/users/amacbride/repos",
"events_url": "https://api.github.com/users/amacbride/events{/privacy}",
"received_events_url": "https://api.github.com/users/amacbride/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"installation": {
"id": 131074261,
"node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTMxMDc0MjYx"
},
"created": false,
"deleted": false,
"base_ref": null,
"compare": "https://github.com/darkmatter/dmcloud/compare/957f52dc0cb7...afa2af707b77",
"commits": [
{
"id": "afa2af707b77eca5bdec689488c2abcd3b92d534",
"tree_id": "8c3d2fbe790b7f25dd3c400dbf0455455268eda4",
"distinct": true,
"message": "Phase 3: capacity reservations end-to-end (steps 1 through 3e)\n\nAdds time-windowed cluster reservations: book N GPUs of a given\nworker_type for a window, prepay (with discount) at create time, get\na per-reservation MinIO bucket for scratch storage, have your jobs\nroute exclusively to your reserved workers, get prorated refund on\ncancel.\n\nThis commit covers all of Phase 3 in one bundle since the pieces are\ntightly interdependent and there are no users yet to disrupt.\n\n═══════════════════════════════════════════════════════════════════\nStep 1 — model + APIs\n═══════════════════════════════════════════════════════════════════\n\n- Reservation model with worker_type (\"swarm\" | \"compute\"),\n GPUCount, StartTS/EndTS, status (pending|active|completed|\n cancelled), pricing breakdown (gpu + storage), storage fields\n (quota, bucket, endpoint, access/secret keys, region), pinning\n (PinnedHostIDs as JSONB UUIDArray), refund tracking.\n- SwarmWorker.ReservationID *uuid.UUID — pinning column for the\n swarm side. Compute pinning lives on Reservation.PinnedHostIDs\n since there's no SwarmWorker row for compute hosts.\n- UsageEvent.ReservationID — billing analytics field.\n- POST/GET/DELETE /api/v1/reservations. Capacity check uses\n Postgres tstzrange overlap with half-open semantics so back-to-\n back reservations at the same boundary don't false-conflict.\n- Cost = gpu_count × hours × rate × (1-discount). Storage is\n bundled in v1 (free, default 100 GB × gpu_count). Hooks for\n per-GB-hour overage are in place.\n- Refund: pending = full; active = prorated by remaining time;\n completed/cancelled = 400.\n\n═══════════════════════════════════════════════════════════════════\nStep 2 — worker control channel\n═══════════════════════════════════════════════════════════════════\n\npkg/nats/control.go, internal/swarm/worker.go,\ninternal/bootstrap/control.go:\n\n- New vanilla NATS subjects swarm.control.worker.<id> and\n compute.control.host.<id>, deliberately outside JetStream\n streams (verified by a guard test). At-most-once is right for\n \"switch your subscription right now\"; a stale instruction\n replayed minutes later would be wrong.\n- ControlMessage{Action, ReservationID, IssuedAtUnix}. Two\n actions: switch_to_reservation, rejoin_pool.\n- Swarm worker holds its task subscription handle; switchSubscription\n creates the new sub BEFORE draining the old so we never sit in\n neither queue group during the swap. Idempotent: switching to\n the same reservation is a no-op.\n- Compute bootstrap: reservationState struct on Agent updated by\n control messages; runComputeLoop reads the current state when\n it (re-)subscribes and picks the appropriate per-reservation\n pull consumer.\n\n═══════════════════════════════════════════════════════════════════\nStep 3a — UsageEvent.ReservationID + skip prepaid debit\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/billing.go meterTaskCompletion:\n- Looks up the executing worker's reservation_id; if set, AND the\n reservation user matches the job user AND status=active, stamps\n the event and SKIPS the credit debit. The user already prepaid\n that GPU-hour.\n- Defensive: cross-user mismatches still bill normally — a routing\n glitch can't shift cost onto someone else's prepay.\n\n═══════════════════════════════════════════════════════════════════\nStep 3b — activation/expiry reconcile loop\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/reservations.go startReservationLoop:\n\n- 30s ticker (immediate first fire on Start) sweeps the\n reservations table.\n- pending + start_ts <= now → activate: provision MinIO bucket\n (step 3e), pick gpu_count idle workers/hosts of the right type,\n pin them, publish switch_to_reservation. If insufficient\n capacity OR bucket creation fails, leaves pending; next tick\n retries.\n- active + end_ts <= now → expire: clears reservation_id on\n pinned workers, publishes rejoin_pool, freezes bucket, status →\n completed.\n- cancelled + still has pinned workers → release via the same path\n but writes status=cancelled; the control-plane cancel handler\n itself doesn't have a NATS handle, so the orchestrator picks up\n the work next tick.\n\nEach branch is idempotent — re-running the same tick is safe.\n\n═══════════════════════════════════════════════════════════════════\nStep 3c — submission routing\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/orchestrator.go, internal/swarm/compute.go:\n\n- swarmTaskSubjectFor / computeTaskSubjectFor: given a job, look\n up user → active reservation of the right worker_type → return\n the per-reservation subject; else open queue.\n- Standard inference dispatch in orchestrator.go publishes to\n that subject. Agent tasks (translate_doc, research_agent,\n agent) keep using the open agent queue — reservations are about\n GPU-bound inference, not agentic reasoning.\n- Compute campaign loop computes the subject once before the\n per-task publish.\n\n═══════════════════════════════════════════════════════════════════\nStep 3e — per-reservation MinIO buckets\n═══════════════════════════════════════════════════════════════════\n\npkg/storage/{bucket.go,minio.go}, cmd/dmcloud-control/main.go,\ndeploy/group_vars/all.yml, .env.example:\n\n- pkg/storage.BucketManager interface: CreateReservationBucket,\n FreezeBucket, DeleteBucket. Plus NoopManager for dev/test.\n- pkg/storage/minio.go: real backend on minio-go/v7 +\n madmin-go/v3. CreateReservationBucket provisions a bucket with\n hard quota, generates fresh credentials, attaches an rw policy\n scoped to that bucket only. FreezeBucket swaps the policy to\n ro. DeleteBucket purges objects + removes user + removes\n bucket.\n- Storage config block in control.yaml templated through Ansible.\n When admin_endpoint is empty (default), the orchestrator uses\n the noop manager — reservations still work, just without\n per-reservation scratch space.\n- Reservation model gets StorageAccessKey/SecretKey fields; the\n secret has json:\"-\" so it's stripped from default GETs. The\n reservation handler's GET returns an explicit storage block\n with the credentials when status=active so the user can point\n boto3 at the bucket.\n- Operational deployment of MinIO itself is out of scope here —\n documented inline in deploy/group_vars/all.yml. With our ~12-\n node cluster (~10-15 TB raw, ~5-7 TB usable after EC), the\n default 100 GB × gpu_count quota leaves plenty of headroom for\n several concurrent reservations.\n\n═══════════════════════════════════════════════════════════════════\nTests\n═══════════════════════════════════════════════════════════════════\n\n26 new tests across the bundle, all green:\n\ninternal/control/reservations_test.go (11):\n- Cost math: discount applied, storage bundled.\n- Refund math: pending full, active prorated, past-end zero.\n- Create: debits balance, persists fields; rejects insufficient\n credits (402), capacity conflict (409), invalid worker_type,\n too-short window, past start.\n- Cancel: pending = full refund, active = prorated, completed = 400.\n- List filters by user; Get cross-user = 403.\n\ninternal/swarm/reservations_test.go (10):\n- Activates pending past start_ts; insufficient capacity stays\n pending without partial activation; expiry clears pins; cancel-\n with-pins releases on next tick.\n- swarmTaskSubjectFor routes correctly by reservation status and\n worker_type; ignores compute reservation when looking up swarm.\n- BucketManager wiring: activation provisions bucket and stamps\n creds; bucket failure leaves reservation pending and unpinned;\n noop manager doesn't block activation; expiry calls FreezeBucket.\n- activeReservationForUser respects worker_type filter.\n\ninternal/swarm/billing_test.go (3):\n- Under-active-reservation: ReservationID stamped, balance unchanged.\n- Reservation status=completed: still debits.\n- Different-owner reservation: still debits (security).\n\npkg/nats/control_test.go (3):\n- Control subjects don't overlap JetStream streams.\n- ControlMessage JSON roundtrip.\n- ReservationQueueGroup doesn't collide with open-pool \"workers\".\n\ninternal/bootstrap/control_test.go (3):\n- reservationState transitions; computeTaskSubject open + reserved.\n\n═══════════════════════════════════════════════════════════════════\nDeploy\n═══════════════════════════════════════════════════════════════════\n\n1. ./deploy.sh control — rebuilds SPA + binary, pushes to bv-bastion.\n2. dmcloud-control -migrate — GORM AutoMigrate adds:\n reservations (whole table)\n usage_events.reservation_id\n reservations.pinned_host_ids\n reservations.storage_* fields\n3. psql ... -f migrations/006_reservations.sql — adds\n swarm_workers.reservation_id (idempotent ALTER).\n4. (optional) deploy MinIO on the cluster, then set the\n DMCLOUD_STORAGE_* env vars in .env, redeploy.\n\nWhat works post-deploy without MinIO: reservations book/cancel\ncorrectly with prorated refunds; activation pins workers; jobs\nroute to reserved workers; tasks under reservations skip per-task\ndebit. Workers run their work end-to-end.\n\nWhat lights up after MinIO is deployed: each reservation gets a\nfresh bucket + scoped credentials in the response, the user can\npoint boto3 at the bucket, the bucket flips to read-only at expiry.\n\nDependencies added:\n- github.com/minio/minio-go/v7 v7.0.77\n- github.com/minio/madmin-go/v3 v3.0.71",
"timestamp": "2026-05-31T02:01:49-04:00",
"url": "https://github.com/darkmatter/dmcloud/commit/afa2af707b77eca5bdec689488c2abcd3b92d534",
"author": {
"name": "Andrew MacBride",
"email": "amacbride@yahoo.com",
"date": "2026-05-31T02:01:49-04:00",
"username": "amacbride"
},
"committer": {
"name": "Andrew MacBride",
"email": "amacbride@yahoo.com",
"date": "2026-05-31T02:01:49-04:00",
"username": "amacbride"
},
"added": [
"internal/bootstrap/control.go",
"internal/bootstrap/control_test.go",
"internal/control/reservations.go",
"internal/control/reservations_test.go",
"internal/swarm/reservations.go",
"internal/swarm/reservations_test.go",
"migrations/006_reservations.sql",
"pkg/nats/control.go",
"pkg/nats/control_test.go",
"pkg/storage/bucket.go",
"pkg/storage/minio.go"
],
"removed": [],
"modified": [
".env.example",
"COMMIT_MSG.txt",
"cmd/dmcloud-control/main.go",
"deploy/group_vars/all.yml",
"deploy/roles/control/templates/control.yaml.j2",
"go.mod",
"go.sum",
"internal/bootstrap/agent.go",
"internal/bootstrap/compute.go",
"internal/control/api.go",
"internal/control/config.go",
"internal/control/testutil_test.go",
"internal/swarm/billing.go",
"internal/swarm/billing_test.go",
"internal/swarm/compute.go",
"internal/swarm/orchestrator.go",
"internal/swarm/service.go",
"internal/swarm/worker.go",
"pkg/models/db.go",
"pkg/models/models.go",
"pkg/models/swarm.go"
]
}
],
"head_commit": {
"id": "afa2af707b77eca5bdec689488c2abcd3b92d534",
"tree_id": "8c3d2fbe790b7f25dd3c400dbf0455455268eda4",
"distinct": true,
"message": "Phase 3: capacity reservations end-to-end (steps 1 through 3e)\n\nAdds time-windowed cluster reservations: book N GPUs of a given\nworker_type for a window, prepay (with discount) at create time, get\na per-reservation MinIO bucket for scratch storage, have your jobs\nroute exclusively to your reserved workers, get prorated refund on\ncancel.\n\nThis commit covers all of Phase 3 in one bundle since the pieces are\ntightly interdependent and there are no users yet to disrupt.\n\n═══════════════════════════════════════════════════════════════════\nStep 1 — model + APIs\n═══════════════════════════════════════════════════════════════════\n\n- Reservation model with worker_type (\"swarm\" | \"compute\"),\n GPUCount, StartTS/EndTS, status (pending|active|completed|\n cancelled), pricing breakdown (gpu + storage), storage fields\n (quota, bucket, endpoint, access/secret keys, region), pinning\n (PinnedHostIDs as JSONB UUIDArray), refund tracking.\n- SwarmWorker.ReservationID *uuid.UUID — pinning column for the\n swarm side. Compute pinning lives on Reservation.PinnedHostIDs\n since there's no SwarmWorker row for compute hosts.\n- UsageEvent.ReservationID — billing analytics field.\n- POST/GET/DELETE /api/v1/reservations. Capacity check uses\n Postgres tstzrange overlap with half-open semantics so back-to-\n back reservations at the same boundary don't false-conflict.\n- Cost = gpu_count × hours × rate × (1-discount). Storage is\n bundled in v1 (free, default 100 GB × gpu_count). Hooks for\n per-GB-hour overage are in place.\n- Refund: pending = full; active = prorated by remaining time;\n completed/cancelled = 400.\n\n═══════════════════════════════════════════════════════════════════\nStep 2 — worker control channel\n═══════════════════════════════════════════════════════════════════\n\npkg/nats/control.go, internal/swarm/worker.go,\ninternal/bootstrap/control.go:\n\n- New vanilla NATS subjects swarm.control.worker.<id> and\n compute.control.host.<id>, deliberately outside JetStream\n streams (verified by a guard test). At-most-once is right for\n \"switch your subscription right now\"; a stale instruction\n replayed minutes later would be wrong.\n- ControlMessage{Action, ReservationID, IssuedAtUnix}. Two\n actions: switch_to_reservation, rejoin_pool.\n- Swarm worker holds its task subscription handle; switchSubscription\n creates the new sub BEFORE draining the old so we never sit in\n neither queue group during the swap. Idempotent: switching to\n the same reservation is a no-op.\n- Compute bootstrap: reservationState struct on Agent updated by\n control messages; runComputeLoop reads the current state when\n it (re-)subscribes and picks the appropriate per-reservation\n pull consumer.\n\n═══════════════════════════════════════════════════════════════════\nStep 3a — UsageEvent.ReservationID + skip prepaid debit\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/billing.go meterTaskCompletion:\n- Looks up the executing worker's reservation_id; if set, AND the\n reservation user matches the job user AND status=active, stamps\n the event and SKIPS the credit debit. The user already prepaid\n that GPU-hour.\n- Defensive: cross-user mismatches still bill normally — a routing\n glitch can't shift cost onto someone else's prepay.\n\n═══════════════════════════════════════════════════════════════════\nStep 3b — activation/expiry reconcile loop\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/reservations.go startReservationLoop:\n\n- 30s ticker (immediate first fire on Start) sweeps the\n reservations table.\n- pending + start_ts <= now → activate: provision MinIO bucket\n (step 3e), pick gpu_count idle workers/hosts of the right type,\n pin them, publish switch_to_reservation. If insufficient\n capacity OR bucket creation fails, leaves pending; next tick\n retries.\n- active + end_ts <= now → expire: clears reservation_id on\n pinned workers, publishes rejoin_pool, freezes bucket, status →\n completed.\n- cancelled + still has pinned workers → release via the same path\n but writes status=cancelled; the control-plane cancel handler\n itself doesn't have a NATS handle, so the orchestrator picks up\n the work next tick.\n\nEach branch is idempotent — re-running the same tick is safe.\n\n═══════════════════════════════════════════════════════════════════\nStep 3c — submission routing\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/orchestrator.go, internal/swarm/compute.go:\n\n- swarmTaskSubjectFor / computeTaskSubjectFor: given a job, look\n up user → active reservation of the right worker_type → return\n the per-reservation subject; else open queue.\n- Standard inference dispatch in orchestrator.go publishes to\n that subject. Agent tasks (translate_doc, research_agent,\n agent) keep using the open agent queue — reservations are about\n GPU-bound inference, not agentic reasoning.\n- Compute campaign loop computes the subject once before the\n per-task publish.\n\n═══════════════════════════════════════════════════════════════════\nStep 3e — per-reservation MinIO buckets\n═══════════════════════════════════════════════════════════════════\n\npkg/storage/{bucket.go,minio.go}, cmd/dmcloud-control/main.go,\ndeploy/group_vars/all.yml, .env.example:\n\n- pkg/storage.BucketManager interface: CreateReservationBucket,\n FreezeBucket, DeleteBucket. Plus NoopManager for dev/test.\n- pkg/storage/minio.go: real backend on minio-go/v7 +\n madmin-go/v3. CreateReservationBucket provisions a bucket with\n hard quota, generates fresh credentials, attaches an rw policy\n scoped to that bucket only. FreezeBucket swaps the policy to\n ro. DeleteBucket purges objects + removes user + removes\n bucket.\n- Storage config block in control.yaml templated through Ansible.\n When admin_endpoint is empty (default), the orchestrator uses\n the noop manager — reservations still work, just without\n per-reservation scratch space.\n- Reservation model gets StorageAccessKey/SecretKey fields; the\n secret has json:\"-\" so it's stripped from default GETs. The\n reservation handler's GET returns an explicit storage block\n with the credentials when status=active so the user can point\n boto3 at the bucket.\n- Operational deployment of MinIO itself is out of scope here —\n documented inline in deploy/group_vars/all.yml. With our ~12-\n node cluster (~10-15 TB raw, ~5-7 TB usable after EC), the\n default 100 GB × gpu_count quota leaves plenty of headroom for\n several concurrent reservations.\n\n═══════════════════════════════════════════════════════════════════\nTests\n═══════════════════════════════════════════════════════════════════\n\n26 new tests across the bundle, all green:\n\ninternal/control/reservations_test.go (11):\n- Cost math: discount applied, storage bundled.\n- Refund math: pending full, active prorated, past-end zero.\n- Create: debits balance, persists fields; rejects insufficient\n credits (402), capacity conflict (409), invalid worker_type,\n too-short window, past start.\n- Cancel: pending = full refund, active = prorated, completed = 400.\n- List filters by user; Get cross-user = 403.\n\ninternal/swarm/reservations_test.go (10):\n- Activates pending past start_ts; insufficient capacity stays\n pending without partial activation; expiry clears pins; cancel-\n with-pins releases on next tick.\n- swarmTaskSubjectFor routes correctly by reservation status and\n worker_type; ignores compute reservation when looking up swarm.\n- BucketManager wiring: activation provisions bucket and stamps\n creds; bucket failure leaves reservation pending and unpinned;\n noop manager doesn't block activation; expiry calls FreezeBucket.\n- activeReservationForUser respects worker_type filter.\n\ninternal/swarm/billing_test.go (3):\n- Under-active-reservation: ReservationID stamped, balance unchanged.\n- Reservation status=completed: still debits.\n- Different-owner reservation: still debits (security).\n\npkg/nats/control_test.go (3):\n- Control subjects don't overlap JetStream streams.\n- ControlMessage JSON roundtrip.\n- ReservationQueueGroup doesn't collide with open-pool \"workers\".\n\ninternal/bootstrap/control_test.go (3):\n- reservationState transitions; computeTaskSubject open + reserved.\n\n═══════════════════════════════════════════════════════════════════\nDeploy\n═══════════════════════════════════════════════════════════════════\n\n1. ./deploy.sh control — rebuilds SPA + binary, pushes to bv-bastion.\n2. dmcloud-control -migrate — GORM AutoMigrate adds:\n reservations (whole table)\n usage_events.reservation_id\n reservations.pinned_host_ids\n reservations.storage_* fields\n3. psql ... -f migrations/006_reservations.sql — adds\n swarm_workers.reservation_id (idempotent ALTER).\n4. (optional) deploy MinIO on the cluster, then set the\n DMCLOUD_STORAGE_* env vars in .env, redeploy.\n\nWhat works post-deploy without MinIO: reservations book/cancel\ncorrectly with prorated refunds; activation pins workers; jobs\nroute to reserved workers; tasks under reservations skip per-task\ndebit. Workers run their work end-to-end.\n\nWhat lights up after MinIO is deployed: each reservation gets a\nfresh bucket + scoped credentials in the response, the user can\npoint boto3 at the bucket, the bucket flips to read-only at expiry.\n\nDependencies added:\n- github.com/minio/minio-go/v7 v7.0.77\n- github.com/minio/madmin-go/v3 v3.0.71",
"timestamp": "2026-05-31T02:01:49-04:00",
"url": "https://github.com/darkmatter/dmcloud/commit/afa2af707b77eca5bdec689488c2abcd3b92d534",
"author": {
"name": "Andrew MacBride",
"email": "amacbride@yahoo.com",
"date": "2026-05-31T02:01:49-04:00",
"username": "amacbride"
},
"committer": {
"name": "Andrew MacBride",
"email": "amacbride@yahoo.com",
"date": "2026-05-31T02:01:49-04:00",
"username": "amacbride"
},
"added": [
"internal/bootstrap/control.go",
"internal/bootstrap/control_test.go",
"internal/control/reservations.go",
"internal/control/reservations_test.go",
"internal/swarm/reservations.go",
"internal/swarm/reservations_test.go",
"migrations/006_reservations.sql",
"pkg/nats/control.go",
"pkg/nats/control_test.go",
"pkg/storage/bucket.go",
"pkg/storage/minio.go"
],
"removed": [],
"modified": [
".env.example",
"COMMIT_MSG.txt",
"cmd/dmcloud-control/main.go",
"deploy/group_vars/all.yml",
"deploy/roles/control/templates/control.yaml.j2",
"go.mod",
"go.sum",
"internal/bootstrap/agent.go",
"internal/bootstrap/compute.go",
"internal/control/api.go",
"internal/control/config.go",
"internal/control/testutil_test.go",
"internal/swarm/billing.go",
"internal/swarm/billing_test.go",
"internal/swarm/compute.go",
"internal/swarm/orchestrator.go",
"internal/swarm/service.go",
"internal/swarm/worker.go",
"pkg/models/db.go",
"pkg/models/models.go",
"pkg/models/swarm.go"
]
}
}