Prevent Duplicate Jobs with run-one

A cron job overruns and the next run starts anyway: run-one stops the second copy cold. It wraps a command so a duplicate exits immediately instead of piling up, and this guide covers that wrapping, choosing a retry policy, and reading what happened from its exit status. The examples use the run-one package installed on this machine, version 1.17-0ubuntu2.

Allow about 10 minutes. You need a shell account with run-one installed. The normal examples need no elevated privileges. Do not use run-this-one against a broad or partly untrusted command line: it deliberately kills matching processes owned by your user.

1. Confirm the installation

Check the package and the main executable:

$ dpkg-query -W -f='${Package} ${Version}\n' run-one
run-one 1.17-0ubuntu2
$ command -v run-one
/usr/bin/run-one

If the package is absent, stop here and install it through your normal Ubuntu package-management process. This guide does not change package state.

2. Run one copy of a command

run-one locks a unique command and argument string before starting it. If an identical invocation is already running, the new invocation does not wait and the wrapper returns status 1. A different argument string is treated as a different job, so choose the arguments that define the resource you are protecting.

Start a deliberately slow test job:

$ run-one sh -c 'sleep 20' &
[1] 12345

Repeat the identical invocation in another shell and inspect the status:

$ run-one sh -c 'sleep 20'
$ printf 'exit status: %s\n' "$?"
exit status: 1

The first process continues. The lock is associated with the command and arguments, not with a friendly job name. For example, run-one sh -c 'sleep 20' and run-one sh -c 'sleep 30' are different invocations and can run together.

For a real scheduled task, put the wrapper directly around the command:

run-one /usr/bin/rsync -azP /srv/source [email protected]:/srv/backup

Quote paths and arguments containing whitespace. In a cron entry, use absolute command paths and keep the complete invocation stable, otherwise a changed argument can create a second lock key.

3. Choose a restart policy

The package provides aliases for recurring work. run-one-constantly starts the command again whenever it exits, whether it succeeds or fails. keep-one-running is an alias for it:

$ run-one-constantly /usr/local/bin/example-worker
$ keep-one-running /usr/local/bin/example-worker

These loops are intentionally persistent. Stop a foreground loop with Ctrl-C; stop a background job by recording its process ID and sending it a signal, for example:

$ run-one-constantly /usr/local/bin/example-worker &
$ worker_pid=$!
$ kill "$worker_pid"
$ wait "$worker_pid"
$ unset worker_pid

That stops the wrapper and prevents another restart. It does not promise to undo work already performed by the child command.

4. Retry until a particular result

Use run-one-until-success when a non-zero exit means "try again". It exits when the child finally returns zero. Use run-one-until-failure when the loop should continue after success and stop at the first non-zero exit:

$ run-one-until-success /usr/local/bin/poll-and-process
$ printf 'final status: %s\n' "$?"
final status: 0

$ run-one-until-failure /usr/local/bin/health-check
$ printf 'final status: %s\n' "$?"
final status: 1

The installed script logs a failed attempt with logger and increases the delay between failures, capped at 60 seconds. A successful attempt resets that backoff. There is no maximum retry count, so a permanently broken command can run forever. Use a service manager or a separately supervised script when you need deadlines, resource limits, dependency ordering, or structured restart policy.

5. Use run-this-one only as a deliberate recovery tool

run-this-one first searches for matching processes owned by the current user with pgrep, kills them, waits for them to disappear, and then starts the requested command. It is useful when an old copy is wedged, but it is destructive to matching processes.

Before using it, inspect the exact process selection and make sure the command line cannot match unrelated work. Then run a narrow command:

$ pgrep -a -u "$USER" -f '^/usr/local/bin/example-worker --queue=reports$'
$ run-this-one /usr/local/bin/example-worker --queue=reports

Warning: no sudo is required for your own processes. Do not add it to broaden the target casually. A root invocation changes which processes are eligible and increases the consequences of a quoting mistake. If the old process owns locks or is writing data, stopping it can leave application-level recovery work behind.

Common traps and verification

Calling run-one without a command is an error and returns 1:

$ run-one
ERROR: no arguments specified
$ printf 'exit status: %s\n' "$?"
exit status: 1

A child failure is passed through by ordinary run-one. This makes it suitable for cron monitoring:

$ run-one sh -c 'exit 7'
$ printf 'child status: %s\n' "$?"
child status: 7

When a duplicate is refused, status 1 means "another identical invocation owns the lock", not necessarily that the underlying job failed. If monitoring treats every non-zero status as an incident, distinguish the duplicate case in your scheduler or logging wrapper.

The wrappers keep lock files in a per-user cache under a writable home directory, or under a temporary directory in /dev/shm when needed. Do not manually delete those files to stop a job. Stop the wrapper or child process and let the next invocation recreate what it needs.

Done means