initial
This commit is contained in:
commit
9a98ac50ff
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.idea
|
||||
/vendor
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2021 Evgeny Zinoviev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
81
README.md
Normal file
81
README.md
Normal file
@ -0,0 +1,81 @@
|
||||
# jobd-php-example
|
||||
|
||||
This repository contains example of PHP integration with jobd. The code is
|
||||
mainly an excerpt from a real existent PHP application, with some changes.
|
||||
|
||||
To launch this example and see how it works, you will need to set up a jobd-master
|
||||
instance along with two jobd worker instances.
|
||||
|
||||
It was written and tested on my local machine with PHP 7.3, so I'm sharing all
|
||||
my configs as is. Don't forget to replace values, such as IP addresses,
|
||||
usernames, passwords and so on, with yours, and generally adjust it to your needs.
|
||||
|
||||
## Configuration
|
||||
|
||||
### jobd
|
||||
|
||||
jobd configs are included in the repo: [`jobd-1.conf`](jobd-1.conf),
|
||||
[`jobd-2.conf`](jobd-2.conf), [`jobd-master.conf`](jobd-master.conf).
|
||||
|
||||
### MySQL
|
||||
|
||||
[`schema.sql`](schema.sql) contains schema of MySQL table used in the example.
|
||||
|
||||
### Runtime
|
||||
|
||||
For the sake of simplicity, runtime configuration (such as MySQL credentials)
|
||||
is stored in [`init.php`](src/init.php) as global constants. Adjust to your needs.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Make sure **MySQL server** is running.
|
||||
|
||||
2. Start **jobd-master** and two **jobd** instances:
|
||||
|
||||
```
|
||||
jobd-master --config jobd-master.conf
|
||||
jobd --config jobd-1.conf
|
||||
jobd --config jobd-2.conf
|
||||
```
|
||||
|
||||
3. Install dependencies with composer:
|
||||
```
|
||||
composer install
|
||||
```
|
||||
|
||||
4. Test configuration:
|
||||
```
|
||||
php src/main.php test
|
||||
```
|
||||
|
||||
This command will test MySQL and jobd connection.
|
||||
|
||||
You can also print the list of workers by executing:
|
||||
```
|
||||
jobctl --master list-workers
|
||||
```
|
||||
|
||||
5. Launch test jobs:
|
||||
```
|
||||
php src/main.php hello
|
||||
```
|
||||
|
||||
This will launch two [`Hello`](src/jobs/Hello.php) jobs, wait for results
|
||||
and print them.
|
||||
|
||||
6. Launch another test job. [This one](src/jobs/CreateFile.php) will run in
|
||||
background. It just creates a file with the name you give it. Not like
|
||||
it's anything useful, but it's for the demo.
|
||||
```
|
||||
php src/main.php createfile
|
||||
```
|
||||
|
||||
Note that if the path your specify is not absolute, it will be relative to
|
||||
the jobd's working directory, specified `launcher.cwd` config option.
|
||||
|
||||
If it fails, just look into the MySQL table, there must be some error.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
17
composer.json
Normal file
17
composer.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ch1p/jobd-php-example",
|
||||
"description": "example usage of jobd and it's PHP client",
|
||||
"minimum-stability": "stable",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evgeny Zinoviev",
|
||||
"email": "me@ch1p.io"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"ch1p/jobd-client": "^1.5",
|
||||
"ext-json": "*",
|
||||
"ext-mysqli": "*"
|
||||
}
|
||||
}
|
59
composer.lock
generated
Normal file
59
composer.lock
generated
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "96d78551c8c7b7a22cb20daf8025e024",
|
||||
"packages": [
|
||||
{
|
||||
"name": "ch1p/jobd-client",
|
||||
"version": "1.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/gch1p/php-jobd-client.git",
|
||||
"reference": "a3bb10feaea28fd54866dc9c4311a247a84a202c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/gch1p/php-jobd-client/zipball/a3bb10feaea28fd54866dc9c4311a247a84a202c",
|
||||
"reference": "a3bb10feaea28fd54866dc9c4311a247a84a202c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-sockets": "*",
|
||||
"php": ">=7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"jobd\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"keywords": [
|
||||
"job",
|
||||
"jobd",
|
||||
"queue"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/gch1p/php-jobd-client/issues",
|
||||
"source": "https://github.com/gch1p/php-jobd-client/tree/v1.5.2"
|
||||
},
|
||||
"time": "2021-03-15T22:02:40+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
35
jobd-1.conf
Normal file
35
jobd-1.conf
Normal file
@ -0,0 +1,35 @@
|
||||
host = 0.0.0.0
|
||||
port = 7079
|
||||
; password =
|
||||
name = worker-1
|
||||
|
||||
master_host = 127.0.0.1
|
||||
master_port = 7081
|
||||
master_reconnect_timeout = 10
|
||||
|
||||
; Don't do this! Here i put it to /tmp only because it was
|
||||
; for a test. In a real world you should use something more
|
||||
; appropriate, like /var/log
|
||||
log_file = /tmp/jobd-1.log
|
||||
log_level_file = warn
|
||||
log_level_console = warn
|
||||
|
||||
mysql_host = 10.211.55.6
|
||||
mysql_port = 3306
|
||||
mysql_user = jobd
|
||||
mysql_password = password
|
||||
mysql_database = jobd
|
||||
mysql_table = jobs2
|
||||
mysql_fetch_limit = 10
|
||||
|
||||
launcher = php /Users/ch1p/dev/jobd-php-example/src/launcher.php {id}
|
||||
launcher.cwd = /Users/ch1p/dev/jobd-php-example/src
|
||||
launcher.env.LC_ALL = en_US.UTF-8
|
||||
launcher.env.LANGUAGE = en_US.UTF-8
|
||||
launcher.env.LANG = en_US.UTF-8
|
||||
max_output_buffer = 16777216
|
||||
|
||||
[targets]
|
||||
1/high = 10
|
||||
1/low = 10
|
||||
any = 5
|
35
jobd-2.conf
Normal file
35
jobd-2.conf
Normal file
@ -0,0 +1,35 @@
|
||||
host = 0.0.0.0
|
||||
port = 7080
|
||||
; password =
|
||||
name = worker-2
|
||||
|
||||
master_host = 127.0.0.1
|
||||
master_port = 7081
|
||||
master_reconnect_timeout = 10
|
||||
|
||||
; Don't do this! Here i put it to /tmp only because it was
|
||||
; for a test. In a real world you should use something more
|
||||
; appropriate, like /var/log
|
||||
log_file = /tmp/jobd-2.log
|
||||
log_level_file = warn
|
||||
log_level_console = warn
|
||||
|
||||
mysql_host = 10.211.55.6
|
||||
mysql_port = 3306
|
||||
mysql_user = jobd
|
||||
mysql_password = password
|
||||
mysql_database = jobd
|
||||
mysql_table = jobs2
|
||||
mysql_fetch_limit = 10
|
||||
|
||||
launcher = php /Users/ch1p/dev/jobd-php-example/src/launcher.php {id}
|
||||
launcher.cwd = /Users/ch1p/dev/jobd-php-example/src
|
||||
launcher.env.LC_ALL = en_US.UTF-8
|
||||
launcher.env.LANGUAGE = en_US.UTF-8
|
||||
launcher.env.LANG = en_US.UTF-8
|
||||
max_output_buffer = 16777216
|
||||
|
||||
[targets]
|
||||
2/high = 10
|
||||
2/low = 10
|
||||
any = 5
|
13
jobd-master.conf
Normal file
13
jobd-master.conf
Normal file
@ -0,0 +1,13 @@
|
||||
host = 0.0.0.0
|
||||
port = 7081
|
||||
; password =
|
||||
|
||||
ping_interval = 30
|
||||
poke_throttle_interval = 0.5
|
||||
|
||||
; Don't do this! Here i put it to /tmp only because it was
|
||||
; for a test. In a real world you should use something more
|
||||
; appropriate, like /var/log
|
||||
log_file = /tmp/jobd-master.log
|
||||
log_level_file = warn
|
||||
log_level_console = warn
|
17
schema.sql
Normal file
17
schema.sql
Normal file
@ -0,0 +1,17 @@
|
||||
CREATE TABLE `jobs2` (
|
||||
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`target` char(32) NOT NULL,
|
||||
`name` char(64) NOT NULL,
|
||||
`time_created` int(10) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`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,
|
||||
`input` mediumtext NOT NULL,
|
||||
`stdout` mediumtext NOT NULL DEFAULT '',
|
||||
`stderr` mediumtext NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `select_for_target_priority_idx` (`target`, `status`, `id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
51
src/classes/Job.php
Normal file
51
src/classes/Job.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
abstract class Job extends model {
|
||||
|
||||
// ENUM status
|
||||
const STATUS_WAITING = 'waiting';
|
||||
const STATUS_MANUAL = 'manual';
|
||||
const STATUS_ACCEPTED = 'accepted';
|
||||
const STATUS_IGNORED = 'ignored';
|
||||
const STATUS_RUNNING = 'running';
|
||||
const STATUS_DONE = 'done';
|
||||
|
||||
// ENUM result
|
||||
const RESULT_OK = 'ok';
|
||||
const RESULT_FAIL = 'fail';
|
||||
|
||||
const DB_TABLE = 'jobs';
|
||||
|
||||
protected static $Fields = [
|
||||
'id' => model::INTEGER,
|
||||
'target' => model::STRING,
|
||||
'name' => model::STRING,
|
||||
'time_created' => model::INTEGER,
|
||||
'time_started' => model::INTEGER,
|
||||
'time_finished' => model::INTEGER,
|
||||
'status' => model::STRING, // ENUM
|
||||
'result' => model::STRING, // ENUM
|
||||
'return_code' => model::INTEGER,
|
||||
'sig' => model::STRING,
|
||||
'stdout' => model::STRING,
|
||||
'stderr' => model::STRING,
|
||||
'input' => model::SERIALIZED,
|
||||
];
|
||||
|
||||
public $id;
|
||||
public $target;
|
||||
public $name;
|
||||
public $timeCreated;
|
||||
public $timeStarted;
|
||||
public $timeFinished;
|
||||
public $status;
|
||||
public $result;
|
||||
public $returnCode;
|
||||
public $sig;
|
||||
public $stdout;
|
||||
public $stderr;
|
||||
public $input;
|
||||
|
||||
abstract public function run();
|
||||
|
||||
}
|
96
src/classes/JobResult.php
Normal file
96
src/classes/JobResult.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
class JobResult {
|
||||
|
||||
/**
|
||||
* @var string $result
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @var int $returnCode
|
||||
*/
|
||||
protected $returnCode;
|
||||
|
||||
/**
|
||||
* @var string|null $signal
|
||||
*/
|
||||
protected $signal;
|
||||
|
||||
/**
|
||||
* @var string $stdout
|
||||
*/
|
||||
protected $stdout;
|
||||
|
||||
/**
|
||||
* @var string $stderr
|
||||
*/
|
||||
protected $stderr;
|
||||
|
||||
/**
|
||||
* @param string $result
|
||||
* @param int $return_code
|
||||
* @param string $stdout
|
||||
* @param string $stderr
|
||||
* @param null $signal
|
||||
* @return $this
|
||||
*/
|
||||
public function setResult(string $result,
|
||||
int $return_code,
|
||||
string $stdout,
|
||||
string $stderr,
|
||||
$signal = null): JobResult
|
||||
{
|
||||
$this->result = $result;
|
||||
$this->returnCode = $return_code;
|
||||
$this->stdout = $stdout;
|
||||
$this->stderr = $stderr;
|
||||
$this->signal = $signal;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $error
|
||||
* @return $this
|
||||
*/
|
||||
public function setError(string $error): JobResult
|
||||
{
|
||||
$this->result = Job::RESULT_FAIL;
|
||||
$this->stderr = $error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFailed(): bool
|
||||
{
|
||||
return $this->result == Job::RESULT_FAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getStdout(): string
|
||||
{
|
||||
return $this->stdout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getStdoutAsJSON() {
|
||||
$json = jsonDecode($this->stdout);
|
||||
return $json ? $json : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getError(): string {
|
||||
return $this->stderr ?? '';
|
||||
}
|
||||
|
||||
}
|
275
src/classes/jobs.php
Normal file
275
src/classes/jobs.php
Normal file
@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
class jobs
|
||||
{
|
||||
|
||||
/**
|
||||
* @var jobs_destructor $destructor_instance
|
||||
*/
|
||||
private static $destructor_instance;
|
||||
|
||||
/**
|
||||
* @var array<int, array> $new_jobs
|
||||
*/
|
||||
private static $new_jobs = [];
|
||||
|
||||
/**
|
||||
* Automatically poke master on exit.
|
||||
*/
|
||||
public static function destruct()
|
||||
{
|
||||
if (!empty(self::$new_jobs)) {
|
||||
$targets = [];
|
||||
foreach (self::$new_jobs as $new_job) {
|
||||
if ($new_job['status'] === Job::STATUS_WAITING)
|
||||
$targets[$new_job['target']] = true;
|
||||
}
|
||||
|
||||
if (!empty($targets)) {
|
||||
$targets = array_keys($targets);
|
||||
self::poke($targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create job.
|
||||
*
|
||||
* @param int|string $target
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
* @param string $status
|
||||
* @return int|string Job ID
|
||||
*/
|
||||
public static function add($target, string $name, array $data = [], string $status = Job::STATUS_WAITING): int
|
||||
{
|
||||
if (is_null(self::$destructor_instance))
|
||||
self::$destructor_instance = new jobs_destructor();
|
||||
|
||||
if (strpos($name, '\\') !== false) {
|
||||
$pos = strrpos($name, '\\');
|
||||
$name = substr($name, $pos + 1);
|
||||
}
|
||||
|
||||
$db = getMySQL();
|
||||
$db->insert(JOBD_TABLE, [
|
||||
'target' => $target,
|
||||
'name' => $name,
|
||||
'time_created' => time(),
|
||||
'input' => serialize($data),
|
||||
'status' => $status
|
||||
]);
|
||||
$id = $db->insertId();
|
||||
|
||||
self::$new_jobs[$id] = [
|
||||
'target' => $target,
|
||||
'status' => $status
|
||||
];
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create manual job.
|
||||
*
|
||||
* @param int|string $target
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
* @return int
|
||||
*/
|
||||
public static function manual($target, string $name, array $data = []): int
|
||||
{
|
||||
return self::add($target, $name, $data, Job::STATUS_MANUAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run jobs with given ids and status=Job::STATUS_MANUAL and wait for results.
|
||||
*
|
||||
* If only one job was given and it's failed, an Exception will be thrown!
|
||||
* If multiple jobs were given and some of them failed, an array of JobResults will be returned.
|
||||
*
|
||||
* @param int|int[] $job_ids
|
||||
* @return array<int, JobResult>|JobResult
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function run($job_ids)
|
||||
{
|
||||
if (!is_array($job_ids))
|
||||
$job_ids = [$job_ids];
|
||||
|
||||
$job_ids_orig = $job_ids;
|
||||
$job_ids = array_flip($job_ids);
|
||||
|
||||
$jobs = [];
|
||||
|
||||
// look for the given jobs in self::$new_jobs
|
||||
foreach (self::$new_jobs as $id => $new_job) {
|
||||
if ($new_job['status'] == Job::STATUS_MANUAL && isset($job_ids[$id])) {
|
||||
$jobs[] = ['id' => $id, 'target' => $new_job['target']];
|
||||
unset($job_ids[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
// if some (or all) jobs were not found in self::$new_jobs, get them from the database
|
||||
if (!empty($job_ids)) {
|
||||
$job_ids = array_keys($job_ids);
|
||||
|
||||
$db = getMySQL();
|
||||
$q = $db->query("SELECT id, target, status AS target FROM ".JOBD_TABLE." WHERE id IN (".implode(',', $job_ids).")");
|
||||
$job_ids = array_flip($job_ids);
|
||||
|
||||
while ($row = $db->fetch($q)) {
|
||||
// only manual jobs are allowed
|
||||
if ($row['status'] != Job::STATUS_MANUAL)
|
||||
throw new Exception("job id=${row['id']} has status = {$row['status']} != manual");
|
||||
|
||||
$jobs[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'target' => $row['target']
|
||||
];
|
||||
|
||||
unset($job_ids[$row['id']]);
|
||||
}
|
||||
|
||||
$q->free();
|
||||
|
||||
// we were given invalid ids, it seems. throw an exception and don't continue
|
||||
if (!empty($job_ids))
|
||||
throw new Exception("jobs with id ".implode(', ', array_keys($job_ids))." not found");
|
||||
}
|
||||
|
||||
// connect to master and send run-manual request
|
||||
$client = getJobdMaster();
|
||||
$response = $client->runManual($jobs);
|
||||
|
||||
// master request failed
|
||||
if (($error = $response->getError()) !== null)
|
||||
throw new Exception("jobd returned error: ".$error);
|
||||
|
||||
// at this point, jobd-master request succeeded
|
||||
// doesn't mean our jobs were successfully accepted and executed by workers,
|
||||
// but at least we have some results
|
||||
|
||||
/**
|
||||
* @var array<int, JobResult> $results
|
||||
*/
|
||||
$results = [];
|
||||
$data = $response->getData();
|
||||
|
||||
$client->close();
|
||||
|
||||
// collect results, successes and failures
|
||||
if (!empty($data['jobs'])) {
|
||||
foreach ($data['jobs'] as $job_id => $job_result_raw) {
|
||||
$job_result = (new JobResult())->setResult(
|
||||
$job_result_raw['result'],
|
||||
$job_result_raw['code'],
|
||||
$job_result_raw['stdout'],
|
||||
$job_result_raw['stderr'],
|
||||
$job_result_raw['signal']
|
||||
);
|
||||
$results[$job_id] = $job_result;
|
||||
}
|
||||
}
|
||||
if (!empty($data['errors'])) {
|
||||
foreach ($data['errors'] as $job_id => $job_result_raw) {
|
||||
$job_result = (new JobResult())->setError($job_result_raw);
|
||||
$results[$job_id] = $job_result;
|
||||
}
|
||||
}
|
||||
|
||||
// remove jobs from self::$new_jobs
|
||||
foreach ($job_ids_orig as $id) {
|
||||
if (isset(self::$new_jobs[$id]))
|
||||
unset(self::$new_jobs[$id]);
|
||||
}
|
||||
|
||||
// if the $job_ids arguments wasn't an array, return the JobResult instance
|
||||
if (count($job_ids_orig) === 1 && count($results) === 1) {
|
||||
$result = reset($results);
|
||||
if ($result->isFailed())
|
||||
throw new Exception($result->getError());
|
||||
return $result;
|
||||
}
|
||||
|
||||
// otherwise, return array of JobResult instances
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $targets
|
||||
*/
|
||||
public static function poke($targets)
|
||||
{
|
||||
|
||||
$client = getJobdMaster();
|
||||
|
||||
if (!is_array($targets))
|
||||
$targets = [$targets];
|
||||
|
||||
$client->poke($targets);
|
||||
$targets = array_flip(array_unique($targets));
|
||||
|
||||
// remove poked targets from self::$new_jobs to avoid meaninglessly duplicating this poke from the destructor
|
||||
if (!empty(self::$new_jobs)) {
|
||||
foreach (self::$new_jobs as $new_job_id => $new_job) {
|
||||
if ($new_job['status'] == Job::STATUS_WAITING && isset($targets[$new_job['target']]))
|
||||
unset(self::$new_jobs[$new_job_id]);
|
||||
}
|
||||
}
|
||||
|
||||
$client->close();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return array
|
||||
*/
|
||||
public static function get(int $id)
|
||||
{
|
||||
$db = getMySQL();
|
||||
$q = $db->query("SELECT * FROM ".JOBD_TABLE." WHERE id=?", $id);
|
||||
return $db->fetch($q);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete old succeeded jobs.
|
||||
*/
|
||||
public static function cleanup()
|
||||
{
|
||||
$db = getMySQL();
|
||||
$db->query("DELETE FROM ".JOBD_TABLE." WHERE status='done' AND result='ok' AND time_finished < ?",
|
||||
time() - 86400);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class job_target
|
||||
{
|
||||
|
||||
const any = "any";
|
||||
|
||||
public static function high(int $server): string
|
||||
{
|
||||
return "$server/high";
|
||||
}
|
||||
|
||||
public static function low(int $server): string
|
||||
{
|
||||
return "$server/low";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class jobs_destructor
|
||||
{
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
jobs::destruct();
|
||||
}
|
||||
|
||||
}
|
149
src/classes/model.php
Normal file
149
src/classes/model.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
abstract class model {
|
||||
|
||||
const DB_TABLE = null;
|
||||
const DB_KEY = 'id';
|
||||
|
||||
const STRING = 0;
|
||||
const INTEGER = 1;
|
||||
const FLOAT = 2;
|
||||
const ARRAY = 3;
|
||||
const BOOLEAN = 4;
|
||||
const JSON = 5;
|
||||
const SERIALIZED = 6;
|
||||
|
||||
protected static $Fields = [];
|
||||
|
||||
public static function create_instance(...$args) {
|
||||
$cl = get_called_class();
|
||||
return new $cl(...$args);
|
||||
}
|
||||
|
||||
public function __construct($raw) {
|
||||
foreach (static::$Fields as $name => $type)
|
||||
$this->{toCamelCase($name)} = self::cast_to_type($type, $raw[$name]);
|
||||
|
||||
if (is_null(static::DB_TABLE))
|
||||
trigger_error('class '.get_class($this).' doesn\'t have DB_TABLE defined');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fields
|
||||
*/
|
||||
public function edit($fields) {
|
||||
$db = getMySQL();
|
||||
|
||||
$save = [];
|
||||
foreach ($fields as $name => $value) {
|
||||
switch (static::$Fields[$name]) {
|
||||
case self::ARRAY:
|
||||
if (is_array($value)) {
|
||||
$fields[$name] = implode(',', $value);
|
||||
$save[$name] = $value;
|
||||
}
|
||||
break;
|
||||
|
||||
case self::INTEGER:
|
||||
$value = (int)$value;
|
||||
$fields[$name] = $value;
|
||||
$save[$name] = $value;
|
||||
break;
|
||||
|
||||
case self::FLOAT:
|
||||
$value = (float)$value;
|
||||
$fields[$name] = $value;
|
||||
$save[$name] = $value;
|
||||
break;
|
||||
|
||||
case self::BOOLEAN:
|
||||
$fields[$name] = $value ? 1 : 0;
|
||||
$save[$name] = $value;
|
||||
break;
|
||||
|
||||
case self::JSON:
|
||||
$fields[$name] = jsonEncode($value);
|
||||
$save[$name] = $value;
|
||||
break;
|
||||
|
||||
case self::SERIALIZED:
|
||||
$fields[$name] = serialize($value);
|
||||
$save[$name] = $value;
|
||||
break;
|
||||
|
||||
default:
|
||||
$value = (string)$value;
|
||||
$fields[$name] = $value;
|
||||
$save[$name] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$db->update(static::DB_TABLE, $fields, static::DB_KEY."=?", $this->get_id())) {
|
||||
//debugError(__METHOD__.': failed to update database');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($save as $name => $value)
|
||||
$this->{toCamelCase($name)} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function get_id() {
|
||||
return $this->{toCamelCase(static::DB_KEY)};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fields
|
||||
* @param array $custom_getters
|
||||
* @return array
|
||||
*/
|
||||
public function as_array(array $fields = [], array $custom_getters = []) {
|
||||
if (empty($fields))
|
||||
$fields = array_keys(static::$Fields);
|
||||
|
||||
$array = [];
|
||||
foreach ($fields as $field) {
|
||||
if (isset($custom_getters[$field]) && is_callable($custom_getters[$field])) {
|
||||
$array[$field] = $custom_getters[$field]();
|
||||
} else {
|
||||
$array[$field] = $this->{toCamelCase($field)};
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @param $value
|
||||
* @return array|bool|false|float|int|string
|
||||
*/
|
||||
protected static function cast_to_type($type, $value) {
|
||||
switch ($type) {
|
||||
case self::BOOLEAN:
|
||||
return (bool)$value;
|
||||
|
||||
case self::INTEGER:
|
||||
return (int)$value;
|
||||
|
||||
case self::FLOAT:
|
||||
return (float)$value;
|
||||
|
||||
case self::ARRAY:
|
||||
return array_filter(explode(',', $value));
|
||||
|
||||
case self::JSON:
|
||||
return jsonDecode($value);
|
||||
|
||||
case self::SERIALIZED:
|
||||
return unserialize($value);
|
||||
|
||||
default:
|
||||
return (string)$value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
166
src/classes/mysql.php
Normal file
166
src/classes/mysql.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
class mysql
|
||||
{
|
||||
|
||||
/** @var mysqli $link */
|
||||
private $link = null;
|
||||
|
||||
public function __construct(string $host, string $user, string $password, string $name)
|
||||
{
|
||||
$this->link = new mysqli();
|
||||
if (!$this->link->real_connect($host, $user, $password, $name)) {
|
||||
$this->link = null;
|
||||
throw new Exception('Could not connect to MySQL');
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->link)
|
||||
$this->link->close();
|
||||
}
|
||||
|
||||
public function __get($k)
|
||||
{
|
||||
if ($k == 'error') {
|
||||
return $this->link->error;
|
||||
}
|
||||
return $this->$k;
|
||||
}
|
||||
|
||||
public function query(string $sql)
|
||||
{
|
||||
if (func_num_args() > 1) {
|
||||
$mark_count = substr_count($sql, '?');
|
||||
$positions = array();
|
||||
$last_pos = -1;
|
||||
for ($i = 0; $i < $mark_count; $i++) {
|
||||
$last_pos = strpos($sql, '?', $last_pos + 1);
|
||||
$positions[] = $last_pos;
|
||||
}
|
||||
for ($i = $mark_count - 1; $i >= 0; $i--) {
|
||||
$arg_val = func_get_arg($i + 1);
|
||||
if (is_null($arg_val)) {
|
||||
$v = 'NULL';
|
||||
} else {
|
||||
$v = '\'' . $this->escape($arg_val) . '\'';
|
||||
}
|
||||
$sql = substr_replace($sql, $v, $positions[$i], 1);
|
||||
}
|
||||
}
|
||||
|
||||
$q = $this->link->query($sql);
|
||||
if (!$q) {
|
||||
$error = $this->link->error;
|
||||
trigger_error($error, E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
public function insert(string $table, array $fields)
|
||||
{
|
||||
return $this->performInsert('INSERT', $table, $fields);
|
||||
}
|
||||
|
||||
public function replace(string $table, array $fields)
|
||||
{
|
||||
return $this->performInsert('REPLACE', $table, $fields);
|
||||
}
|
||||
|
||||
protected function performInsert(string $command, string $table, array $fields)
|
||||
{
|
||||
$names = [];
|
||||
$values = [];
|
||||
$count = 0;
|
||||
foreach ($fields as $k => $v) {
|
||||
$names[] = $k;
|
||||
$values[] = $v;
|
||||
$count++;
|
||||
}
|
||||
|
||||
$sql = "{$command} INTO `{$table}` (`" . implode('`, `', $names) . "`) VALUES (" . implode(', ', array_fill(0, $count, '?')) . ")";
|
||||
array_unshift($values, $sql);
|
||||
|
||||
return call_user_func_array([$this, 'query'], $values);
|
||||
}
|
||||
|
||||
public function multipleInsert(string $table, array $rows)
|
||||
{
|
||||
return $this->performMultipleInsert('INSERT', $table, $rows);
|
||||
}
|
||||
|
||||
public function multipleReplace(string $table, array $rows)
|
||||
{
|
||||
return $this->performMultipleInsert('REPLACE', $table, $rows);
|
||||
}
|
||||
|
||||
protected function performMultipleInsert(string $command, string $table, array $rows)
|
||||
{
|
||||
$names = [];
|
||||
$sql_rows = [];
|
||||
foreach ($rows as $i => $fields) {
|
||||
$row_values = [];
|
||||
foreach ($fields as $field_name => $field_val) {
|
||||
if ($i == 0) {
|
||||
$names[] = $field_name;
|
||||
}
|
||||
$row_values[] = $this->escape($field_val);
|
||||
}
|
||||
$sql_rows[] = "('" . implode("', '", $row_values) . "')";
|
||||
}
|
||||
|
||||
$sql = "{$command} INTO `{$table}` (`" . implode('`, `', $names) . "`) VALUES " . implode(', ', $sql_rows);
|
||||
return $this->query($sql);
|
||||
}
|
||||
|
||||
public function update(string $table, arrow $rows, string ...$cond)
|
||||
{
|
||||
$fields = [];
|
||||
$args = [];
|
||||
foreach ($rows as $row_name => $row_value) {
|
||||
$fields[] = "`{$row_name}`=?";
|
||||
$args[] = $row_value;
|
||||
}
|
||||
$sql = "UPDATE `$table` SET " . implode(', ', $fields);
|
||||
if (!empty($cond)) {
|
||||
$sql .= " WHERE " . $cond[0];
|
||||
if (count($cond) > 1)
|
||||
$args = array_merge($args, array_slice($cond, 1));
|
||||
}
|
||||
return $this->query($sql, ...$args);
|
||||
}
|
||||
|
||||
public function fetch(mysqli_result $q)
|
||||
{
|
||||
$row = $q->fetch_assoc();
|
||||
if (!$row) {
|
||||
$q->free();
|
||||
return false;
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function result(mysqli_result $q, int $field = 0)
|
||||
{
|
||||
return $q ? $q->fetch_row()[$field] : false;
|
||||
}
|
||||
|
||||
public function insertId()
|
||||
{
|
||||
return $this->link->insert_id;
|
||||
}
|
||||
|
||||
public function numRows(mysqli_result $query): int
|
||||
{
|
||||
return $query->num_rows;
|
||||
}
|
||||
|
||||
public function escape(string $s): string
|
||||
{
|
||||
return $this->link->real_escape_string($s);
|
||||
}
|
||||
|
||||
}
|
5
src/cron/jobs-cleanup.php
Normal file
5
src/cron/jobs-cleanup.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../init.php';
|
||||
|
||||
jobs::cleanup();
|
47
src/functions.php
Normal file
47
src/functions.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
function jsonEncode($obj) {
|
||||
return json_encode($obj, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
function jsonDecode($json) {
|
||||
return json_decode($json, true);
|
||||
}
|
||||
|
||||
function toCamelCase(string $input, string $separator = '_'): string {
|
||||
return lcfirst(str_replace($separator, '', ucwords($input, $separator)));
|
||||
}
|
||||
|
||||
|
||||
/* Connection helpers */
|
||||
|
||||
function getMySQL(): mysql {
|
||||
static $link = null;
|
||||
if (is_null($link))
|
||||
$link = new mysql(MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB);
|
||||
return $link;
|
||||
}
|
||||
|
||||
function getJobdMaster(): jobd\MasterClient {
|
||||
return new jobd\MasterClient(JOBD_PORT, JOBD_HOST, JOBD_PASSWORD);
|
||||
}
|
||||
|
||||
|
||||
/* Command line helpers */
|
||||
|
||||
function green(string $s): string {
|
||||
return "\033[32m$s\033[0m";
|
||||
}
|
||||
|
||||
function yellow(string $s): string {
|
||||
return "\033[33m$s\033[0m";
|
||||
}
|
||||
|
||||
function red(string $s): string {
|
||||
return "\033[31m$s\033[0m";
|
||||
}
|
||||
|
||||
function input(string $prompt): string {
|
||||
echo $prompt;
|
||||
return substr(fgets(STDIN), 0, -1);
|
||||
}
|
32
src/init.php
Normal file
32
src/init.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
define('MYSQL_HOST', '10.211.55.6');
|
||||
define('MYSQL_USER', 'jobd');
|
||||
define('MYSQL_PASSWORD', 'password');
|
||||
define('MYSQL_DB', 'jobd');
|
||||
|
||||
define('JOBD_TABLE', 'jobs2');
|
||||
define('JOBD_HOST', '127.0.0.1');
|
||||
define('JOBD_PORT', jobd\Client::MASTER_PORT);
|
||||
define('JOBD_PASSWORD', '');
|
||||
|
||||
spl_autoload_register(function($class) {
|
||||
if (strpos($class, '\\') !== false) {
|
||||
$class = str_replace('\\', '/', $class);
|
||||
$root = __DIR__;
|
||||
} else {
|
||||
$root = __DIR__.'/classes';
|
||||
}
|
||||
|
||||
$path = $root.'/'.$class.'.php';
|
||||
|
||||
if (is_file($path))
|
||||
require_once $path;
|
||||
});
|
||||
|
||||
include __DIR__.'/functions.php';
|
15
src/jobs/CreateFile.php
Normal file
15
src/jobs/CreateFile.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace jobs;
|
||||
|
||||
class CreateFile extends \Job
|
||||
{
|
||||
|
||||
public function run()
|
||||
{
|
||||
$file = $this->input['file'];
|
||||
if (!touch($file))
|
||||
throw new \Exception("failed to touch file '".$file."'");
|
||||
}
|
||||
|
||||
}
|
15
src/jobs/Hello.php
Normal file
15
src/jobs/Hello.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace jobs;
|
||||
|
||||
class Hello extends \Job
|
||||
{
|
||||
|
||||
public function run()
|
||||
{
|
||||
$greetings = "Hello, ".($this->input['name'] ?? 'noname').".\n";
|
||||
$greetings .= "I'm writing you from ".__METHOD__.", my PID is ".getmypid()." and I'm executing job #".$this->id.".";
|
||||
echo jsonEncode(['response' => $greetings]);
|
||||
}
|
||||
|
||||
}
|
31
src/launcher.php
Normal file
31
src/launcher.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/init.php';
|
||||
|
||||
set_time_limit(0);
|
||||
$job = null;
|
||||
|
||||
register_shutdown_function(function() {
|
||||
global $job;
|
||||
if ($job !== true)
|
||||
exit(1);
|
||||
});
|
||||
|
||||
$job_id = $argv[1] ?? null;
|
||||
|
||||
$job_raw = jobs::get($job_id);
|
||||
if (!$job_raw)
|
||||
throw new InvalidArgumentException("job $job_id not found");
|
||||
|
||||
$class_name = "jobs\\{$job_raw['name']}";
|
||||
$job = new $class_name($job_raw);
|
||||
if ($job->status != Job::STATUS_RUNNING)
|
||||
throw new RuntimeException("job status is {$job->status}");
|
||||
|
||||
try {
|
||||
if ($job->run() !== false)
|
||||
$job = true;
|
||||
} catch (Exception $e) {
|
||||
fprintf(STDERR, $e.'');
|
||||
exit(1);
|
||||
}
|
93
src/main.php
Normal file
93
src/main.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/init.php';
|
||||
|
||||
if ($argc < 2) {
|
||||
echo <<<EOF
|
||||
Usage: {$argv[0]} COMMAND
|
||||
|
||||
Commands:
|
||||
test
|
||||
hello
|
||||
createfile
|
||||
|
||||
EOF;
|
||||
exit;
|
||||
}
|
||||
|
||||
$cmd = $argv[1];
|
||||
$func = "cmd_{$cmd}";
|
||||
if (!function_exists($func)) {
|
||||
echo red("command '".$cmd."' is not implement")."\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
call_user_func($func);
|
||||
|
||||
|
||||
/** Commands */
|
||||
|
||||
function cmd_test() {
|
||||
// MySQL
|
||||
try {
|
||||
$db = getMySQL();
|
||||
$jobs_count = $db->result($db->query("SELECT COUNT(*) FROM ".JOBD_TABLE));
|
||||
} catch (Exception $e) {
|
||||
echo red("MySQL connection failed")."\n";
|
||||
exit(1);
|
||||
}
|
||||
echo green("MySQL OK")."\n";
|
||||
|
||||
// jobd
|
||||
try {
|
||||
$jobd = getJobdMaster();
|
||||
$status = $jobd->status(true);
|
||||
$workers_count = count($status->getData()['workers']);
|
||||
if ($workers_count == 2) {
|
||||
echo green("jobd-master and jobd OK");
|
||||
} else {
|
||||
$message = "jobd-master OK, but ";
|
||||
$message .= $workers_count == 1 ? "only 1 worker is connected" : "no workers are connected";
|
||||
echo yellow($message);
|
||||
}
|
||||
echo "\n";
|
||||
} catch (Exception $e) {
|
||||
echo red("jobd-master connection failed: ".$e->getMessage())."\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function cmd_hello() {
|
||||
$myname = input('Enter your name: ');
|
||||
try {
|
||||
$job_ids = [];
|
||||
$job_server_map = [];
|
||||
|
||||
for ($server = 1; $server <= 2; $server++) {
|
||||
$id = jobs::manual(job_target::high($server), jobs\Hello::class, ['name' => $myname]);
|
||||
$job_server_map[$id] = $server;
|
||||
$job_ids[] = $id;
|
||||
}
|
||||
|
||||
$results = jobs::run($job_ids);
|
||||
foreach ($results as $job_id => $job_result) {
|
||||
$server = $job_server_map[$job_id];
|
||||
echo "> server {$server}:\n";
|
||||
if ($job_result->isFailed()) {
|
||||
echo red("failed")."\n";
|
||||
} else {
|
||||
echo green($job_result->getStdoutAsJSON()['response'])."\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo red("error: ".$e->getMessage())."\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function cmd_createfile() {
|
||||
$file = input('Enter file name: ');
|
||||
jobs::add(job_target::any, jobs\CreateFile::class, ['file' => $file]);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user