aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 9a4bc6f82f7e8478eefaa0417d87644563b685c5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# jobd

**jobd** is a simple job queue daemon with persistent queue storage, written in
Node.JS. It uses a MySQL table as a storage backend (for queue input and output).

Currently, MySQL is the only supported storage type, but other backends might be
easily supported.

It is by design that jobd never adds nor deletes jobs from storage. It only
reads (when a certain request arrives) and updates them (during execution, when
job status changes). Succeeded or failed, your jobs are never lost.

jobd consists of 2 parts:

1. **jobd** is a "worker" daemon that reads jobs from the database, enqueues and
   launches them. There may be multiple instances of jobd running on multiple
   hosts. Each jobd instance may have unlimited number of queues (called "targets"),
   each with its own concurrency limit.

2. **jobd-master** is a "master" or "cetral" daemon that simplifies control over
   many jobd instances. There should be only one instance of jobd-master running.
   jobd-master is not required for jobd workers to work (they can work without it),
   but it's very very useful.
   
In addition, there is a command line utility called **jobctl**.

Originally, jobd was created as a saner alternative to Gearman. It's been used
in production with a large PHP web application on multiple servers for quite some
time already, and proven to be stable and efficient.

## Table of Contents

- [How it works](#how-it-works)
- [Protocol](#protocol)
  - [Request Message](#request-message)
    - [jobd request types](#jobd-request-types)
    - [jobd-master request types](#jobd-master-request-types)
  - [Response Message](#response-message)
  - [Ping and Pong Messages](#ping-and-pong-messages)
- [Implementation example](#implementation-example)
- [Installation](#installation)
- [Usage](#usage)
  - [systemd](#systemd)
  - [supervisor](#supervisor)
  - [Other notes](#other-notes)
- [Configuration](#configuration)
  - [jobd](#jobd)
  - [jobd-master](#jobd-master)
  - [jobctl](#jobctl)
- [MySQL setup](#mysql-setup)
- [Clients](#clients)
  - [PHP](#php)
- [TODO](#todo)
- [License](#license)


## How it works

To be written.

## Protocol

By default, jobd and jobd-master listen on TCP ports 7080 and 7081 respectively,
ports can be changed in a config.

jobd has been created with an assumption that it'll be used in more-or-less
trusted environments (LAN, or, at least, servers within one data center) so no
encryption nor authentication mechanisms have been implemented. All traffic
between jobd and clients flow in plain text. You can protect a jobd instance with
a password though, so at least basic password-based authorization is supported.

Both daemons receive and send Messages. Each message is followed by `EOT` (`0x4`)
byte which indicates an end of a message. Clients may send and receive multiple
messages over a single connection. Usually, it's the client who must close the
connection, when it's not needed anymore. A server, however, may close the
connection in some situations (invalid password, server error, etc).

Messages are encoded as JSON arrays with at least one item, representing
the message type:
```
[TYPE]
```

If a message of specific type has some data, it's placed as a second item:
```
[TYPE, DATA]
```

Type of `TYPE` is integer. Supported types are:

- `0`: Request
- `1`: Response
- `2`: Ping
- `3`: Pong


### Request Message

`DATA` is a JSON object with following keys:

- **`no`** *(**required**, int)* — unique (per connection) request number. Clients
  can start counting request numbers from one (`1`) or from any other random number.
  Each subsequent request should increment this number by 1. Note that zero (`0`)
  is reserved.
- **`type`** *(**required**, string)* — request type. Supported request types for
  jobd and jobd-master are listed below.
- **`data`** *(object)* — request arguments (if needed): an object, whose keys
  and values represent argument names and values.
- **`password`** *(string)* — a password, for password-protected instances. Only
  needed for first request.
  
Example (w/o trailing `EOT`):
```
[0,{no:0,type:'poll',data:{'targets':['target_1','target_2']}}]
```

Here is the list of supported requests, using `type(arguments)` notation.

#### jobd request types

* **`poll(targets: string[])`** — get new tasks for specified `targets` from database.
  If `targets` argument is not specified, get tasks for all serving targets.

* **`pause(targets: string[])`** — pause execution of tasks of specified targets.
  If `targets` argument is not specified, pauses all targets.

* **`continue(targets: string[])`** — continue execution of tasks of specified targets.
  If `targets` argument is not specified, continues all targets.

* **`status()`** — returns status of internal queues and memory usage.

* **`run-manual(ids: int[])`** — enqueue and run jobs with specified IDs and
  `status` set to `manual`, and return results.

* **`set-target-concurrency(target: string, concurrency: int)`** — set concurrency
  of target `target`.

#### jobd-master request types

* **`register-worker(targets: string[])`** — used by a jobd instance to register
  itself with master. You don't need it.

* **`poke(targets: string[])`** — send `poll` requests to all registered workers
  that serve specified `targets`.

* **`pause(targets: string[])`** — send `pause(targets)` requests to workers
  serving specified `targets`. If `targets` argument is not specified, sends
  `pause()` to all workers.

* **`continue(targets: string[])`** — send `continue(targets)` requests to workers
  serving specified `targets`. If `targets` argument is not specified, sends
  `continue()` to all workers.

* **`status(poll_workers=false: bool)`** — returns list of registered workers and
  memory usage. If `poll_workers` is true, sends `status()` request to all registered
  workers and includes their responses.

* **`run-manual(jobs: {id: int, target: string}[])`** — send `run-manual`
  requests to registered jobd instances serving specified targets, aggregate an
  return results.
  
### Response Message

`DATA` is a JSON object with following keys:

- **`no`** *(**required**, int)*`no` of request this response is related to.
- **`data`** *(array *|* object *|* string *|* int)* — data, if request succeeded.
- **`error`** *(string)* — error message, if request failed.

Example (w/o trailing `EOT`):
```
[1,{no:0,data:'ok'}]
```

### Ping and Pong Messages

No `DATA`.

Example (w/o trailing `EOT`):
```
[2]
```

## Implementation example

To be written.

## Installation

First, you need Node.JS 14 or newer. See [here](https://nodejs.org/en/download/package-manager/)
now to install it using package manager.

Then install jobd using npm:

```
npm i -g jobd 
```


## Usage

### systemd

One of possible ways of launching jobd and jobd-master daemons is via systemd.
This repository contains basic examples of `jobd.service` and `jobd-master.service`
unit files. Note that jobs will be launched as the same user the jobd worker is
running, so you might want to change that.

Copy `.service` file(s) to `/etc/systemd/system`, then do:
```
systemctl daemon-reload
systemctl enable jobd
systemctl start jobd
# repeat last two steps for jobd-master, if needed
```

### supervisor

If you don't like systemd, supervisor might be an option. Create a configuration
file in `/etc/supervisor/conf.d` with following content:
```
[program:jobd]
command=/usr/bin/jobd --config /etc/jobd.conf
numprocs=1
directory=/
stdout_logfile=/var/log/jobd-stdout.log
autostart=true
autorestart=true
user=nobody
stopsignal=TERM
```

Then use `supervisorctl` to start or stop jobd.


### Other notes

Don't forget to filter access to jobd and jobd-master ports using your favorite
firewall.

## Configuration

Configuration files are written in ini format. All available options for both
daemons, as well as a command-line utility, are described below. You can copy
`jobd.conf.example` and `jobd-master.conf.example` and use them as a template
instead of writing configs from scratch.

### jobd

Default config path is `/etc/jobd.conf`. Use the `--config` option to use
a different path.

Without section:

- **`host`** *(required, string)* — jobd server hostname 
- **`port`** *(required, int)* — jobd server port
- **`password`** *(string)* — password for requests
- **`always_allow_localhost`** *(boolean, default: `false`)* — when set to `1`
  or `true`, allows accepting requests from clients connecting from localhost
  without password
- **`master_host`** *(string)* — master hostname
- **`master_port`** *(int)* — master port. If hostname or port is omitted, jobd
  will not connect to master.
- **`master_reconnect_timeout`** *(int, default: `10`)* — if connection to master
  failed, jobd will be constantly trying to reconnect. This option specifies a
  delay between connection attempts, in seconds.
- **`log_file`** *(string)* — path to a log file
- **`log_level_file`** *(string, default: `warn`)* — minimum level of logs that
  are written to the file.<br>
  Allowed values: `trace`, `debug`, `info`, `warn`, `error`
- **`log_level_console`** *(string, default: `warn`)* — minimum level of logs
  that go to stdout.
- **`mysql_host`** *(required, string)* — database host
- **`mysql_port`** *(required, int)* — database port
- **`mysql_user`** *(required, string)* — database user
- **`mysql_password`** *(required, string)* — database password
- **`mysql_database`** *(required, string)* — database name
- **`mysql_table`** *(required, string)* — table name
- **`mysql_fetch_limit`** *(int, default: `100`)* — a number of new jobs to fetch
  in every request
- **`launcher`** *(required, string)* — a template of shell command that will be launched
  for every job. `{id}` will be replaced with job id
- **`max_output_buffer`** *(int, default: `1048576`)*

Under the `[targets]` section, targets are specified. Each target is specified on
a separate line in the following format:
```
{target_name} = {n}
```
where:
- `{target_name}` *(string)* is target name
- `{n}` *(int)* is maximum count of simultaneously executing jobs for this target

### jobd-master

Default config path is `/etc/jobd-master.conf`. Use the `--config` option to use
a different path.

- **`host`** *(required, string)*
- **`port`** *(required, int)*
- **`password`** *(string)*
- **`always_allow_localhost`** *(boolean, default: `false`)*
- **`ping_interval`** *(int, default: `30`)* — specifies interval between workers
  pings.
- **`poke_throttle_interval`** *(int, default: `0.5`)*
- **`log_file`** *(string)*
- **`log_level_file`** *(string, default: `warn`)* 
- **`log_level_console`** *(string, default: `warn`)*

### jobctl

Default config path is `~/.jobctl.conf`.

- **`master`** (boolean) — same as `--master`.
- **`host`** *(string)* — same as `--host`.
- **`port`** *(int)* — same as `--port`.
- **`password`** *(string)*
- **`log_level`** *(string, default: `warn`)*

## MySQL setup

Minimal table scheme:

```
CREATE TABLE `jobs` (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `target` char(16) NOT NULL,
  `time_created` int(10) UNSIGNED NOT NULL,
  `time_started` int(10) UNSIGNED NOT NULL DEFAULT 0,
  `time_finished` int(10) UNSIGNED NOT NULL DEFAULT 0,
  `status` enum('waiting','manual','accepted','running','done','ignored') NOT NULL DEFAULT 'waiting',
  `result` enum('ok','fail') DEFAULT NULL,
  `return_code` tinyint(3) UNSIGNED DEFAULT NULL,
  `sig` char(10) DEFAULT NULL,
  `stdout` mediumtext DEFAULT NULL,
  `stderr` mediumtext DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `status_target_idx` (`status`, `target`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

In a real world, you need some additional fields such as `job_name` or `job_data`.

For optimization purposes, you can turn `target` into `ENUM`. Also, if 16 characters
for the `target` field is not enough for you, change it to fit your needs.


## Clients

### PHP

[php-jobd-client](https://github.com/gch1p/php-jobd-client) (official)

## TODO

- graceful shutdown

## License

BSD-2c