All notable changes to task-spooler-PLUS.
--requeue <jobid>— manually requeue a running or paused job. Callss_relegate_job()(same as automatic timeout) to pause the job, set negative wall_time, mark PAUSE, then moves it to the end of the queue. The job will be automatically retried after the cooldown period.s_relegate_job()— extracted fromcheck_timeout(); pauses a RUNNING job, setswall_time = -|wall_time| - 86400, marksstate = PAUSE. Shared between automatic timeout and manual--requeue.- Orphan QUEUED auto-cleanup —
s_cleanup_orphan_queued()scansactive_jobsfor QUEUED jobs withclient_socket <= 0(no client connected) and removes them vias_delete_job(). Triggered once ~30 minutes after server start fromserver_loop()using astatictimer variable — no thread, no pipe, no extra socket needed. movebottom_DB()— new SQLite helper (sqlite.c) sets a job'sorder_idtomax(order_id) + 1, effectively moving it to the end of the queue. Used by timeout and cleanup paths to keep DB order consistent with runtime order.- Timeout order_id sync —
s_check_timeout()now callsmovebottom_DB()after moving a timed-out job to the back ofactive_jobs, soSELECT ... ORDER BY order_idmatches the runtime queue after restart.
- State label "timeout" → "requeue" — PAUSE jobs with negative wall_time
(timeout-wait) are now displayed as
requeueints -loutput, reflecting that they will be automatically retried after the cooldown.
s_delete_job()DB leak —s_delete_job()(called fromclean_after_client_disappeared()andremove_connection()) now callsdelete_DB(jobid, "Jobs")beforedestroy_job(). Previously, QUEUED jobs removed after RECONNECT + second disconnect were deleted from memory but not from SQLite "Jobs" table, causing them to reappear on the next restart.get_order_id()always returns 0 for new jobs —get_order_id()usedsqlite3_execwith a callback, which could not distinguish "no row found" (new job not yet inserted) from "order_idis zero". This causededit_DB()to skip themax_order_id + 1fallback, assigningorder_id = 0to all new jobs.ORDER BY order_idthen returned jobs in arbitrary order after restart. Fixed by rewriting withsqlite3_prepare_v2/sqlite3_stepso that the absence of a row correctly sets an error code.s_hold_job()pause conversion for timeout-wait jobs — when a job was in "timeout wait" state (wall_time < 0, paused due to insufficient slots on retry),s_hold_job()did not clear the negative wall_time, leaving the job stuck. Now converts to a proper user pause by makingwall_timepositive and syncing to DB.s_cont_job()DB wall_time sync — when retry fails due to insufficient slots, the negative wall_time is now also written to the DB viaupdate_field_int64().
- Health check for RUNNING jobs —
s_check_running_health()runs every 10s in the server loop. Jobs RUNNING for ≥5 minutes are checked: if output log is empty AND no child processes, the job transitions toABNORMALstate and its slots are freed. Health status is in-memory only (health_statefield onstruct Job: 0=UNCHECKED, 1=NORMAL, 2=ABNORMAL). ABNORMALjob state — newenum Jobstatevalue for jobs found stuck by the health checker. State is visible ints -loutput. Slot quotas are released. Does not freeze cgroup —ts -kworks as usual.--add-wtimenegative values —parse_time()no longer rejects negative durations. Use--add-wtime -30mto reduce a job's wall-time.
- Dead job auto-cleanup —
s_update_slots_usage()now scans for dead PIDs (RUNNING/PAUSE/ABNORMAL) and moves them to finished viajob_finished(). Triggers on everyts -land job submission. - PAUSE job restoration on restart —
s_add_job()detects RUNNING jobs withpause_time > 0in the DB and restores them as PAUSE. If the PID is dead, the job is cleaned up instead of restored as a zombie. parse_time()type safety — output parameter changed fromtime_t*toint64_t*(matchingcommand_line.wall_timetype). Added ±1000-day boundary check.s_add_wtime()overflow protection — explicit INT64_MAX check before addition prevents wall-time integer overflow.
- Async CPU bind defrag: self-contained I/O thread — the defrag thread receives a complete copy of all needed data (jobid, pid, state, is_sleep, pre-formatted cpuset/mems strings) instead of referencing main-thread
struct Job*orstruct CpuAlloc*pointers. The thread does all cgroup I/O via rawopen/write/close— never calls public cgroup functions. This eliminates any possible data race between the main thread and the I/O thread. - Wait, don't bail —
cpu_bind_defrag_start()now callscgroup_io_wait_if_busy()before spawning a new defrag thread, so concurrent defrag requests block briefly instead of being dropped. All gating checks (cpu_bind_alloc_is_locked, retrigger flags) removed from jobs.c, server_user.c, and server.c — operations are unconditional, with wait inside cgroup I/O functions. - Removed
in_defrag_io_threadTLS flag — no longer needed since the defrag thread never enters the public cgroup functions.
--n-retry N— retry failed jobs up to N times insiderun_childif the command exits with error within 15 seconds (exec failures 126/127 excluded)
ts -Acrash —s_listwith NULL user now correctly shows all users' jobs instead of crashing ins_user_status
- **
cpu_bind_defrag()re-enabled** — auto-defrag after each job finishes, with--no-bind-defrag` server flag to disable (root only, for MPI workloads)
- Jobid init refactored — robust startup with duplicate prevention
init_jobids_DB(): validates seed against existing Jobs/Finished tables, bumps if neededget_jobids_DB()→ valid (>0) → use directly; invalid → full scan + fallback toTS_FIRST_JOBID/ 1000set_jobids_DB(): lightweight runtime write (no scan)GlobaltableINSERT OR IGNOREto avoid UNIQUE constraint on restart[set_jobids_DB]error output now includes return code
gen_topology.py: summary shows recommended strategy alongside generated files
- JSON output (
-M json) expanded to match-iinfo fields (times, CPU bind, dependencies, etc.) -M json -J <id>filters JSON output to a single job (reuses existing--jobid/-J)
- Mail subsystem —
mail.c,mail.h,mymail.sh, ssmtp/sendmail integration removed-mCLI option removed; use--on-finishhook for notificationsTS_MAIL_FROM,TS_MAIL_TIMEenv vars removedTS_ONFINISHretained as default on-finish command (overridden by--on-finish)
- Legacy
TS_ONFINISHhook (hook_on_finishwrapper) removed;--on-finishis the only callback path
run_on_finish: simplified fromfork+execl("/bin/sh",...)tosystem()
cpu_bind_defrag()refactored — two-phase NUMA-aware defrag:- Phase 3: reassign jobs on their original
primary_nodefirst (NUMA affinity preserved) - Phase 4: cross-node merge for remaining jobs
- Callback pattern removed; directly calls
cgroups_freeze_job/cgroups_set_cpuset/cgroups_thaw_job
- Phase 3: reassign jobs on their original
cpu_bind_alloc_init()now takesjobid;cpu_owner[]tracks real job IDs throughout allocationcpu_bind_post_alloc()called in both initial allocation and defrag paths (sort + validate)gen_topology.py— generates.hfiles for ALL non-trivial strategies at once- New
#define MAX_GROUPS_PER_NODEin every output - Skips trivial strategies (count == cores, or count == PUs with
--ht; exceptby_core) - Skips duplicate strategies (identical group topologies)
- New
cgroups_set_cpuset: create cgroup v2 directory before writingcpuset.cpuscpu_bind_defrag: memory leak on early return (all quality=0)cpu_bind_defrag: frozen jobs lost due tojobs[n]overwrite bug
cpu_bind_pause_fn/cpu_bind_update_fn/cpu_bind_resume_fntypedefs (no longer used)defrag_pause_job/defrag_update_cpuset/defrag_resume_jobcallbacks injobs.c
cpu_bind_defrag()auto-triggered after each job finishesgen_topology.pyrewritten: useshwloc-calcCLI instead of parsinglstopoXML- All topology queries follow top-down hierarchy (
type:i → Core → PU) - Precomputed
core_pu_mapandcore_node_mapat startup, shared across all strategies build_groupsskipshwloc-calcwhen count == total_cores (by_core or 1:1 cache mapping)- Auto-select targets average cores/group closest to 4 (tie-break:
by_l2 → by_core → by_l1 → by_l3 → by_numa) - MAX_GROUPS raised from 32 to 128
- All topology queries follow top-down hierarchy (
main.c: missing newline inTS_MAXCONNhelp output
upgrade_db.py— no longer needed
- Unified option naming: all long options use dashes (
--get-label,--add-wtime, etc.) - Server startup output:
key : valueformat, PID shown,[ENV_VAR]retained - Help text format: consistent
[param]style, comma-separated short options - Removed
pip install hwlocreference (useslstopodirectly)
ts-guardian.sh/ts-guardian.conf— non-system process freezer daemonlogin-check.sh— user session audit tool
- CLAUDE.md: restored missing Cgroups support section, corrected user.txt example
- CPU binding allocator (
TS_CPU_BINDcompile switch)- NUMA-aware topology-based CPU allocation
- cgroups cpuset v1/v2 integration
- HT exclusion (default) or inclusion (
--ht) - Restart recovery from cgroup filesystem
gen_topology.py— auto-detect system topology vialstopo--no-bind— disable CPU binding per-job or server-wide
upgrade_db.py— SQLite schema upgrade tool for older databases- Human-readable memory in
ts -i(auto KB/MB/GB)
cgroups_create_job()usesnum_slotsdirectly instead ofnum_allocated- cpuset cgroup created before freezer cgroup (correct init order)
ts -ishowsCPU: freefor jobs without binding- Memory display in
ts -inow auto-scales (KB/MB/GB) - PROTOCOL_VERSION bumped to 732 (
no_cpu_bindingfield in NEWJOB)
--atscheduled execution: delay jobs with+5m,14:00,2025-06-01T14:00- Client sends schedule_time, server persists in SQLite
ts -lshowsWaitstate andRun at ...output for scheduled jobsts -ishowsSchedule:line with time and remaining duration
- Client auto-reconnect on crash: queued and running jobs survive server restart
- Client reconnects every 5s using RECONNECT protocol
s_add_job()preserves QUEUED/RUNNING state (no fork, no DELINK)
- Server
select()uses 1s timeout for periodic schedule checks - List display hides
(-nan%)for jobs with zero runtime ts -ihides work/pause/elapsed time when value is zero
--on-finishper-job callback: run a command after job finishes with full job info via placeholders- Placeholders:
{jobid},{output},{exitcode},{pid},{label},{command},{realtime},{usertime},{systime},{pausetime},{start_time},{enque_time},{end_time},{slots} {output},{label},{command}auto-quoted for shell safetyENDJOB_OKprotocol: server sends final timing data (pause duration, start/enqueue/end time) after job finishes- Client-side execution (user context, not root)
- Placeholders:
- Client auto-reconnect: on server crash, client retries connection automatically
RECONNECT/RECONNECT_OKprotocol for seamless re-attachment- First retry immediately, then every 60s
- Works for both running and finished jobs
- Orphan child cleanup: server kills orphaned subprocesses when client disconnects during RUNNING/PAUSE state
- Server-side
--relinkmechanism: replaced by client auto-reconnect- Removed
RELINKstate,s_check_relink(),run_relink(),ptrace_pid(),wait_for_pid()
- Removed
- Sound notification: removed
SOUNDifdef,paplay/PulseAudio defaults
- Time calculation moved before job callbacks to provide timing data to
--on-finish
- Cgroups v2 support: unified hierarchy (
cpu.max+cgroup.freeze), compile-time switch (make CGROUP_V2=1) - User management via
vec_t:struct Userwith dynamic array, username-only config (UID resolved bygetpwnam) - PID lookup:
--find-by-pidfinds which job owns a process (including descendants via/proc/*/children) - Per-job boot time: stores boot epoch per-job in SQLite
end_time_mscolumn for correct wall-clock display after reboot REMOVEJOB_NOKprotocol: proper error response when remove-job fails- Refresh safety:
ts -Xonly allows adding users, rejects removals or slot changes - Single-instance guard:
/procscan on--daemonprevents two root servers - SQLite WAL mode:
PRAGMA journal_mode=WAL+busy_timeout=5000, checkpoint on graceful exit - Tools:
tools/clear_finished.py(clear/drop Finished table),tools/migrate_uid.py(migrate ts_UID to Linux UID) - man page:
ts.1manual page - Test script:
manual_test.shcovering all operations withsleepcommands
- User config format:
UID name slots→name slots(no manual UID needed) struct Job.ts_UID→struct User *userpointer- All server functions take
struct User *instead ofint ts_UID sprintf→snprintfin sqlite.c global bufferuser_locker:intindex →struct User *pointer- Cgroups refactor: shared public interface dispatches to v1 or v2 internally
- PAUSE job
-r/-k: thaw before cleanup/kill so signals can be delivered cgroups_v1_cleanup_cpu: EBUSY retry + thaw-before-kill orderingnew_finished_job: useinsert_or_replace_DBto avoid UNIQUE constraint on duplicate finishread_DB: recover user from/proc/<pid>/statusfor active Jobs table rowsread_jobid_DB/read_DB: removed redundantsqlite3_execbeforesqlite3_prepare
- readme updates
- bumped version
- erroneous free fix
- removed redundancy
- plain list output
- various memory fixes
- local installation support
- free malloc'ed memory
- more time units in
-idisplay - fixed Makefile
- git version embedding
- setenv/getenv/unsetenv support
- configurable log filename and directory
- changed gzip flag from
-gto-z
- adaptive command display width
- more responsive command display
- updated readme and man page
- killed servers without terminating jobs
- auto-changelog script
- handled invalid dependencies
- fixed memory leaks
- multiple dependency support
- cmake support
- improved help
- dependency exit code check (
-W) - updated readme
- major bug fixes
- show full command
- shortened label/command display
- uninstall script
- various reformats
- kill all running
- dependency fix
- improved argument parsing
- fixed time unit
- get last queued job ID
- count running jobs
- initial release (forked from upstream task-spooler)