Background jobs
The jobs capability adds a persistent job queue and a background worker. A signed-in user picks a file; the app reads it once with the user’s session and keeps a private copy in the package’s data folder. The worker processes the copy after the user signs out or closes the browser. The shipped worker computes the SHA-256 digest of the file. Results stay inside the package: a job never writes to the user’s folders. For that, see Delegate.
Jobs run as the package account. They need no administrator setup. The app stores no cookie, session ID, token or password for a job. Every user has a separate set of jobs.
Add it
Section titled “Add it”New project:
synopkgland init -t native-go --with jobs my-jobsExisting project:
synopkgland add jobsBoth set this in dsm-package.yaml:
capabilities: jobs: truejobs: true is the same as jobs: { worker: sha256-v1 }. No other worker name is accepted.
| Combination | Result |
|---|---|
jobs with daemon | CAPABILITY_COMBINATION_INVALID, unless a delegate with lifetime: until-revoked is declared |
jobs with a binary backend | CAPABILITY_UNSUPPORTED |
jobs on armv7 | Built; not yet verified on a NAS |
delegate | Turns jobs on automatically |
The generated Vue UI adds a Background jobs page. A plain UI has no jobs page, but the routes exist.
Use it
Section titled “Use it”Submit, watch, cancel
Section titled “Submit, watch, cancel”The background page lets the user pick a file, submit it, watch the state, cancel, and open the result. With your own code, call these methods through ctx.call. All need a signed-in user.
| Method | Params | Result |
|---|---|---|
bg.status | {} | Limits, queue counts and whether the worker runs |
bg.browse | { folder?, offset } | A page of folders and files the user can see. Works without the files capability |
bg.jobs.submit | { key, folder, relative } | { job, duplicate } |
bg.jobs.list | {} | { jobs }, newest first |
bg.jobs.get | { id } | { job } |
bg.jobs.cancel | { id } | { job } |
bg.jobs.result | { id } | { job, result }, or not_ready when the job has no result |
const key = crypto.randomUUID()const { job } = await ctx.call<{ job: { id: string; state: string } }>('bg.jobs.submit', { key, folder: '/home', relative: 'report.txt',})const { result } = await ctx.call<{ result: { sha256: string; bytes: number } }>('bg.jobs.result', { id: job.id })keyidentifies the request: 1 to 64 letters, digits, dots, underscores and hyphens. Sending the same key again returns the same job withduplicate: true, so a retry is safe. Keys are separate per owner.folderis a File Station folder path andrelativeis a file below it.- The result is JSON, not a file:
{ jobId, worker, workerRevision, algorithm: "sha256", sha256, bytes, inputDigest, name }. - A job moves through
queued,runningand ends assucceeded,failed,canceledorinterrupted. A job that was running when the worker stopped isinterruptedand is not replayed. - Closing the browser or signing out does not cancel a job. Only
bg.jobs.canceldoes.
Schedules
Section titled “Schedules”After an import, the owner can repeat the private copy on a schedule. The app pins the bytes and the digest. It never reads the NAS file again, so a later change to the source file has no effect.
| Method | Params | Who |
|---|---|---|
bg.schedules.list | {} | Owners see their own, administrators see all |
bg.schedules.create | { key, jobId, intervalSeconds, consent: true } | Owner of the source job |
bg.schedules.disable, bg.schedules.delete | { id } | Owner or administrator |
bg.schedules.enable | { id, consent: true } | Administrators only |
intervalSecondsis exactly3600(hourly),86400(daily) or604800(weekly).consentmust betrue. Without it the call fails withbad_request.- The source job (
jobId) must be your own and must havesucceeded. Another user’s job returnsnot_found. A job that is not finished, a scheduled run, or a job whose copy is gone returnsinput-unavailable. Import the file again. - The pinned copy counts toward your 1 MiB and the package’s 16 MiB limits.
- A tick that is more than 5 seconds late is recorded as missed. Missed runs are never made up. A schedule is not an exact-time or exactly-once guarantee.
- Scheduled runs leave at least 4 queue slots free for manual jobs.
Who sees which job
Section titled “Who sees which job”An owner is a DSM user name together with the numeric UID that DSM resolves for it.
- Ordinary users list, read, cancel and get results for their own jobs only. A job ID that does not exist and a job that belongs to someone else both return
not_found. - Administrators see and cancel all jobs.
bg.jobs.purge({ owner: { user, uid } }) removes one owner’s jobs and is administrators only. - If DSM cannot resolve the user’s UID, the call fails with
identity_unavailable. For administrators this affects only submit and schedule create. - An account that is deleted and recreated with the same name but a different UID cannot reach the earlier jobs. If both the name and the UID are reused, jobs are not told apart.
- Changing permissions on the source file later does not recall copies or results that already exist.
Limits
Section titled “Limits”| Item | Limit |
|---|---|
| Jobs running at the same time | 1 |
| Queued jobs | 16 |
| Input file | 512 KiB |
| Input file on DSM 7.1 | DSM 7.1 sends the file without Content-Disposition. A file of at most 4 KiB whose whole content is a DSM reply (a JSON object with a success field) is refused as that reply |
| Total private copies and results | 16 MiB |
| Time for one job | 60 seconds (timeout) |
| Pending jobs per owner (administrators included) | 2 |
| Private data per owner (administrators included) | 1 MiB. Each pending or kept result also counts 512 bytes |
| Finished jobs kept | 64 |
| Schedules per package | 8 |
| Schedule intervals | 3600, 86400 or 604800 seconds |
Two full-size 512 KiB inputs are more than one owner may hold. The data lives in the package’s app data folder. It stays when the package stops and on a normal uninstall. See Data and removal.
Troubleshooting
Section titled “Troubleshooting”| Code | Cause | Fix |
|---|---|---|
session_required | No signed-in user, or the DSM session needed to read the file is missing | Sign in again. With standalone login the call needs the DSM id cookie |
dsm_session_required | Standalone login is on and submit or browse needs the DSM id cookie | Sign in to DSM on the same host name |
limit-exceeded | The file is over 512 KiB | Choose a smaller file |
queue-full | 16 jobs are queued | Wait and retry |
quota-exceeded | The package’s private storage (16 MiB) is full | Wait for jobs to finish, or ask an administrator to purge |
owner-queue-full | You already have 2 pending jobs | Wait or cancel one |
owner-quota-exceeded | You hold 1 MiB of private data | Wait for results to be purged |
schedule-full | The package has 8 schedules | Delete one |
input-unavailable | The kept copy for a schedule is gone | Import the file again |
conflict | The same key was used for a different input, job or interval, or you cancelled a finished job | Use a new key; check the state |
not_ready | The job has no result yet | Wait for succeeded |
not_found | No such job, or it belongs to another user | Refresh the list |
identity_unavailable | DSM could not resolve the UID of the user | Retry; check the DSM account |
jobs_busy | The job store was locked for more than 5 seconds | Retry |
admin_required | An administrators-only method such as bg.jobs.purge | Use an administrator account |
A failed job has an errorCode:
errorCode | Cause |
|---|---|
timeout | The job ran longer than 60 seconds |
input-changed | The private copy does not match its stored digest |
worker-error | The worker failed |
worker-stopped | The worker stopped while the job ran. State is interrupted |
worker-changed | The job was queued under a different build of the worker. State is interrupted |
admission-interrupted | A hand-off to a delegate was in progress when the worker restarted. State is interrupted |
Codes are described in Errors.