Job Registration System
Problem
Section titled “Problem”Jobs in SHC come from multiple sources but have no unified registry:
- Standalone jobs (
.job.yamlfiles) — visible viaJobLoader.load_all(), show inshc job ls - Socket/plug jobs (inline in
*.socket.yaml,*.plug.yaml,*.vsocket.yaml) — invisible until they run - Backup jobs (inline in
backup.yaml) — invisible until they run - Hook jobs (referenced by name in
app.yamlhooks) — just references to.job.yamlnames
When a socket discovery job or backup pre_backup job runs, it creates a job_run record in the DB, but there’s no job record linking it to a known definition. The HUD, CLI, and any monitoring system can’t enumerate all possible jobs for a stack without scanning multiple YAML files at query time.
Solution
Section titled “Solution”Add a job table that acts as a registry/index of all known job definitions for a stack. This table is:
- Not backed up — it can be rebuilt from definition files
- Rebuilt on install/upgrade/restore — always reflects current definitions
- Single source of truth for
shc job ls, the HUD jobs tree, and any future job scheduling UI
Schema
Section titled “Schema”Table: job
Section titled “Table: job”| Column | Type | Constraints | Description |
|---|---|---|---|
id | Integer | PK, autoincrement | |
name | String(255) | indexed | Job name (e.g. backup_daily, create-database) |
tenant | String(255) | indexed | |
stack | String(255) | indexed | |
environment | String(255) | default "default" | |
source | String(50) | indexed | standalone, socket, plug, vsocket, backup |
source_name | String(255) | Which definition it came from (e.g. postgres, backup) | |
phase | String(50) | nullable | Phase within source (e.g. discovery, integration, pre_backup) |
context | String(20) | service, container, http | |
service | String(255) | nullable | Target service name (service/container context) |
image | String(512) | nullable | Container image (container context) |
command | Text | nullable | Command to execute |
timeout | Integer | default 300 | Execution timeout in seconds |
definition | Text (JSON) | Full job spec as JSON (for execution) | |
created_at | DateTime | When registered | |
updated_at | DateTime | When last updated (upgrade) |
Unique constraint: (tenant, stack, environment, name)
This means the same job name can’t appear twice in a stack, regardless of source. If a socket job and a .job.yaml have the same name, that’s a conflict caught at registration time.
Relationship to job_run
Section titled “Relationship to job_run”job_run.job_name + job_run.stack + job_run.tenant + job_run.environment is a logical FK to job. No physical FK constraint — job_run records survive if a job definition is removed (history preserved).
Job Sources and Extraction
Section titled “Job Sources and Extraction”1. Standalone (.job.yaml)
Section titled “1. Standalone (.job.yaml)”source: standalonesource_name: <filename without .job.yaml>phase: nullAlready parsed by JobLoader.load_all(). Each file produces one JobDefinition.
2. Socket (*.socket.yaml)
Section titled “2. Socket (*.socket.yaml)”source: socketsource_name: <socket name, e.g. "postgres">phase: discovery | integration | post_deployment | unintegrationAll phases are nested under a jobs: key in the YAML:
jobs.discovery→ phasediscoveryjobs.integration→ phaseintegrationjobs.post_deployment→ phasepost_deploymentjobs.unintegration→ phaseunintegration
Each job spec is a dict that can be parsed as JobDefinition. The name field may be missing — derive from context (e.g. <socket>-discovery-<index>).
3. Plug (*.plug.yaml)
Section titled “3. Plug (*.plug.yaml)”source: plugsource_name: <plug name>phase: discoveryField: jobs.discovery
4. VSocket (*.vsocket.yaml)
Section titled “4. VSocket (*.vsocket.yaml)”source: vsocketsource_name: <vsocket name>phase: discovery | integration | unintegrationFields: jobs.discovery, jobs.integration, jobs.unintegration
5. Backup (backup.yaml)
Section titled “5. Backup (backup.yaml)”source: backupsource_name: backupphase: pre_backup | post_backup | pre_restore | post_restore | restoreAll phases are nested under a jobs: key:
jobs.pre_backup→ phasepre_backupjobs.post_backup→ phasepost_backupjobs.pre_restore→ phasepre_restorejobs.post_restore→ phasepost_restorejobs.restore→ phaserestore(these areRestoreJobobjects, slightly different schema)
Registration Lifecycle
Section titled “Registration Lifecycle”On shc install
Section titled “On shc install”After the stack is deployed and definition files are on disk:
- Scan all sources (
.job.yaml,*.socket.yaml,*.plug.yaml,*.vsocket.yaml,backup.yaml) - Parse each job spec into a
JobDefinition(or equivalent for backup RestoreJobs) - INSERT all into the
jobtable
This happens in StackService.install_stack() after _compose_command_async succeeds, alongside _sync_schedules.
On shc update (upgrade)
Section titled “On shc update (upgrade)”- Scan all sources from the NEW definition files
- Compare against existing
jobtable rows for this stack - Added jobs → INSERT
- Removed jobs → DELETE
- Changed jobs → UPDATE (definition, context, image, etc.)
This is a full reconciliation, not an append. If a .job.yaml is renamed, the old job row is deleted and a new one is inserted.
On shc uninstall
Section titled “On shc uninstall”DELETE all job rows for this stack. job_run history is preserved (no FK cascade).
On shc integrate
Section titled “On shc integrate”When connecting a plug to a socket, the socket’s integration/discovery jobs become relevant to the consumer stack. These should be registered on the provider stack (where the socket lives), since that’s where they execute.
Socket/plug jobs are registered when the provider stack is installed. Integration doesn’t create new job registrations — it just runs the already-registered jobs.
On shc restore
Section titled “On shc restore”After restoring stack files, re-run the full registration scan (same as install). The job table is not in the backup, so it’s always rebuilt.
Registration Function
Section titled “Registration Function”// modules/job/service.go (RegisterJobs)
// RegisterJobs scans all definition sources and registers jobs in the DB.// Returns the number of jobs registered.// Deletes stale jobs that no longer exist in definitions.func RegisterJobs( ctx context.Context, db *sql.DB, tenant string, stack string, environment string, // default: "default") (int, error)
// UnregisterJobs removes all registered jobs for a stack.// Returns count removed.func UnregisterJobs( ctx context.Context, db *sql.DB, tenant string, stack string, environment string, // default: "default") (int, error)Impact on Existing Code
Section titled “Impact on Existing Code”JobLoader
Section titled “JobLoader”Stays as-is for .job.yaml scanning. The registration function uses it internally. At execution time, jobs can be loaded from the job table instead of the filesystem.
JobService.list_jobs()
Section titled “JobService.list_jobs()”Changes from JobLoader.load_all() to SELECT * FROM job WHERE stack=... AND tenant=.... This now returns ALL jobs, not just standalone ones.
shc job ls
Section titled “shc job ls”Automatically shows all jobs (standalone + socket + backup + etc.) because it queries the job table. Add a --source filter flag to narrow by source.
HUD Collector
Section titled “HUD Collector”Changes from JobLoader.load_all() + DB run lookup to a single query joining job with latest job_run.
shc job run
Section titled “shc job run”For standalone jobs, works as before. For socket/backup jobs, the execution still uses the inline spec (stored in job.definition JSON column). The executor loads the definition from the job table instead of scanning YAML files.
shc job ps
Section titled “shc job ps”No changes — queries job_run table as before.
shc job history
Section titled “shc job history”No changes — queries job_run table as before.
CLI Changes
Section titled “CLI Changes”shc job ls <stack>
Section titled “shc job ls <stack>”NAME SOURCE PHASE CONTEXT SERVICEbackup_daily standalone - service postgresmigrate standalone - container -check-pg-ready socket discovery service postgrescreate-database socket integration container -quiesce backup pre_backup service postgresresume backup post_backup service postgresimport-dump backup post_restore container -New flags:
--source <standalone|socket|plug|vsocket|backup>— filter by source--phase <discovery|integration|...>— filter by phase
HUD Changes
Section titled “HUD Changes”The jobs tree uses the job table. Each job shows source as a dim tag:
└ ▾ jobs 7 ├ ▾ backup_daily service ✓ │ ├ ✓ #42 2m ago exit:0 │ └ ✕ #40 2h ago exit:1 ├ ○ check-pg-ready service socket:discovery ├ ✓ create-database container socket:integration ├ ○ quiesce service backup:pre_backup └ ✓ resume service backup:post_backupMigration
Section titled “Migration”Single Atlas migration (migrations/<timestamp>_job_table.sql) to create the job table. No data migration needed — the table starts empty and gets populated on next install/upgrade.
For existing installations: add a one-time register_jobs() call during server startup if the job table is empty but stacks exist. This backfills the registry for stacks that were installed before this feature.
Not in Scope
Section titled “Not in Scope”- Job scheduling UI — cron scheduling stays in
app.yamlandschedulestable - Job definition editing via API — definitions are file-based, not mutable via API
- Cross-stack job references — jobs belong to one stack