postjob 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/postjob/cli/db.rb +3 -0
- data/lib/postjob/cli/job.rb +15 -7
- data/lib/postjob/cli/ps.rb +31 -4
- data/lib/postjob/cli/run.rb +4 -2
- data/lib/postjob/migrations/002_statuses.rb +18 -0
- data/lib/postjob/migrations/003_postjobs.sql +41 -0
- data/lib/postjob/migrations/003a_processing.sql +3 -0
- data/lib/postjob/migrations/003b_processing_columns.sql +31 -0
- data/lib/postjob/migrations/004_tokens.sql +9 -0
- data/lib/postjob/migrations/005_helpers.sql +28 -0
- data/lib/postjob/migrations/006_enqueue.sql +44 -0
- data/lib/postjob/migrations/006a_processing.sql +48 -0
- data/lib/postjob/migrations/007_job_results.sql +157 -0
- data/lib/postjob/migrations/008_checkout_runnable.sql +78 -0
- data/lib/postjob/migrations/008a_childjobs.sql +82 -0
- data/lib/postjob/migrations/009_tokens.sql +25 -0
- data/lib/postjob/migrations.rb +30 -77
- data/lib/postjob/queue/encoder.rb +1 -1
- data/lib/postjob/queue/notifications.rb +6 -21
- data/lib/postjob/queue/search.rb +4 -4
- data/lib/postjob/queue.rb +47 -209
- data/lib/postjob/runner.rb +22 -13
- data/lib/postjob.rb +21 -19
- data/spec/postjob/enqueue_spec.rb +26 -14
- data/spec/postjob/job_control/error_status_spec.rb +2 -2
- data/spec/postjob/job_control/manual_spec.rb +4 -6
- data/spec/postjob/job_control/max_attempts_spec.rb +3 -1
- data/spec/postjob/process_job_spec.rb +3 -2
- data/spec/postjob/queue/encoder_spec.rb +4 -0
- data/spec/postjob/run_spec.rb +1 -1
- data/spec/postjob/step_spec.rb +2 -2
- data/spec/postjob/{sub_workflow_spec.rb → workflows/child_workflow_spec.rb} +2 -2
- data/spec/spec_helper.rb +1 -1
- data/spec/support/configure_database.rb +1 -0
- data/spec/support/test_helper.rb +4 -4
- metadata +19 -7
@@ -0,0 +1,82 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.childjobs(v_parent_id BIGINT)
|
2
|
+
RETURNS SETOF {SCHEMA_NAME}.postjobs AS $$
|
3
|
+
BEGIN
|
4
|
+
RETURN QUERY
|
5
|
+
SELECT {TABLE_NAME}.* FROM {TABLE_NAME}
|
6
|
+
WHERE parent_id=v_parent_id
|
7
|
+
ORDER BY id;
|
8
|
+
END;
|
9
|
+
$$ LANGUAGE plpgsql;
|
10
|
+
|
11
|
+
CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.next_unresolved_childjob(v_parent_id BIGINT)
|
12
|
+
RETURNS SETOF {SCHEMA_NAME}.postjobs AS $$
|
13
|
+
BEGIN
|
14
|
+
RETURN QUERY
|
15
|
+
SELECT {TABLE_NAME}.* FROM {TABLE_NAME}
|
16
|
+
WHERE status NOT IN ('ok', 'failed') AND parent_id=v_parent_id
|
17
|
+
ORDER BY next_run_at;
|
18
|
+
END;
|
19
|
+
$$ LANGUAGE plpgsql;
|
20
|
+
|
21
|
+
CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.find_or_create_childjob(
|
22
|
+
v_queue VARCHAR,
|
23
|
+
v_workflow VARCHAR,
|
24
|
+
v_workflow_method VARCHAR,
|
25
|
+
v_workflow_version VARCHAR,
|
26
|
+
v_args JSONB,
|
27
|
+
v_parent_id BIGINT,
|
28
|
+
v_tags JSONB,
|
29
|
+
v_max_attempts INTEGER,
|
30
|
+
v_timeout DOUBLE PRECISION)
|
31
|
+
RETURNS SETOF {SCHEMA_NAME}.postjobs AS $$
|
32
|
+
DECLARE
|
33
|
+
child_id BIGINT;
|
34
|
+
parent {SCHEMA_NAME}.postjobs;
|
35
|
+
BEGIN
|
36
|
+
IF v_parent_id IS NULL THEN
|
37
|
+
RAISE 'Invalid parent job id NULL';
|
38
|
+
END IF;
|
39
|
+
|
40
|
+
IF v_parent_id IS NOT NULL THEN
|
41
|
+
SELECT INTO parent * FROM {TABLE_NAME} WHERE id=v_parent_id;
|
42
|
+
IF parent.id IS NULL THEN
|
43
|
+
RAISE 'No such job: %', v_parent_id;
|
44
|
+
END IF;
|
45
|
+
END IF;
|
46
|
+
|
47
|
+
-- check for existing child record
|
48
|
+
|
49
|
+
-- IF v_parent_id IS NOT NULL THEN
|
50
|
+
SELECT id INTO child_id FROM {TABLE_NAME}
|
51
|
+
WHERE parent_id=v_parent_id
|
52
|
+
AND workflow=v_workflow
|
53
|
+
AND workflow_method=v_workflow_method
|
54
|
+
AND args=v_args
|
55
|
+
;
|
56
|
+
|
57
|
+
IF child_id IS NOT NULL THEN
|
58
|
+
-- note that RETURN QUERY does not return the function here. It 'only'
|
59
|
+
-- adds the specified query to the result set.
|
60
|
+
RETURN QUERY
|
61
|
+
SELECT * FROM {TABLE_NAME} WHERE id=child_id
|
62
|
+
;
|
63
|
+
ELSE
|
64
|
+
IF v_tags IS NOT NULL THEN
|
65
|
+
RAISE WARNING 'Ignoring tags %', v_tags;
|
66
|
+
END IF;
|
67
|
+
|
68
|
+
RETURN QUERY
|
69
|
+
SELECT * FROM enqueue(
|
70
|
+
COALESCE(v_queue, parent.queue), -- queue VARCHAR,
|
71
|
+
v_workflow, -- workflow VARCHAR,
|
72
|
+
v_workflow_method, -- workflow_method VARCHAR,
|
73
|
+
NULL, -- workflow_version VARCHAR,
|
74
|
+
v_args, -- args JSONB,
|
75
|
+
v_parent_id, -- parent_id BIGINT,
|
76
|
+
parent.tags, -- tags JSONB,
|
77
|
+
v_max_attempts, -- max_attempts INTEGER,
|
78
|
+
v_timeout);
|
79
|
+
END IF;
|
80
|
+
-- END IF;
|
81
|
+
END;
|
82
|
+
$$ LANGUAGE plpgsql;
|
@@ -0,0 +1,25 @@
|
|
1
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
2
|
+
|
3
|
+
CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.find_or_create_token(job_id BIGINT) RETURNS VARCHAR AS $$
|
4
|
+
DECLARE
|
5
|
+
v_token UUID;
|
6
|
+
BEGIN
|
7
|
+
SELECT token INTO v_token FROM postjob.tokens WHERE postjob_id=job_id;
|
8
|
+
|
9
|
+
if v_token IS NULL THEN
|
10
|
+
SELECT gen_random_uuid() INTO v_token;
|
11
|
+
INSERT INTO postjob.tokens(postjob_id, token) VALUES(job_id, v_token);
|
12
|
+
END IF;
|
13
|
+
RETURN v_token::varchar;
|
14
|
+
END;
|
15
|
+
$$ LANGUAGE plpgsql;
|
16
|
+
|
17
|
+
CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.postjobs_by_token(v_token UUID)
|
18
|
+
RETURNS SETOF {SCHEMA_NAME}.postjobs AS $$
|
19
|
+
BEGIN
|
20
|
+
RETURN QUERY
|
21
|
+
SELECT {TABLE_NAME}.* FROM {TABLE_NAME}
|
22
|
+
INNER JOIN {SCHEMA_NAME}.tokens ON {SCHEMA_NAME}.tokens.postjob_id={TABLE_NAME}.id
|
23
|
+
WHERE {SCHEMA_NAME}.tokens.token=v_token;
|
24
|
+
END;
|
25
|
+
$$ LANGUAGE plpgsql;
|
data/lib/postjob/migrations.rb
CHANGED
@@ -1,97 +1,50 @@
|
|
1
|
+
# rubocop:disable Security/Eval
|
2
|
+
|
1
3
|
module Postjob
|
2
4
|
module Migrations
|
3
5
|
extend self
|
4
6
|
|
5
7
|
SQL = ::Simple::SQL
|
6
|
-
SCHEMA_NAME = Postjob::Queue::SCHEMA_NAME
|
8
|
+
SCHEMA_NAME = ::Postjob::Queue::SCHEMA_NAME
|
9
|
+
CHANNEL = ::Postjob::Queue::Notifications::CHANNEL
|
10
|
+
|
11
|
+
# Note that the SCHEMA_NAME should not be the default name, since unmigrate!
|
12
|
+
# below drops that schema, and we don't want to drop the default schema.
|
13
|
+
expect! SCHEMA_NAME != "public"
|
14
|
+
|
15
|
+
TABLE_NAME = "#{SCHEMA_NAME}.postjobs"
|
7
16
|
|
8
17
|
def unmigrate!
|
9
|
-
|
10
|
-
SQL.exec <<~SQL
|
11
|
-
DROP SCHEMA IF EXISTS #{SCHEMA_NAME} CASCADE;
|
12
|
-
SQL
|
13
|
-
end
|
18
|
+
SQL.exec "DROP SCHEMA IF EXISTS #{SCHEMA_NAME} CASCADE"
|
14
19
|
end
|
15
20
|
|
16
|
-
PG_TYPES = <<~SQL
|
17
|
-
SELECT pg_namespace.nspname AS schema, pg_type.typname AS name
|
18
|
-
FROM pg_type
|
19
|
-
LEFT JOIN pg_namespace on pg_namespace.oid=pg_type.typnamespace
|
20
|
-
SQL
|
21
|
-
|
22
21
|
def migrate!
|
23
|
-
SQL.exec
|
24
|
-
CREATE SCHEMA IF NOT EXISTS #{SCHEMA_NAME};
|
25
|
-
SQL
|
22
|
+
SQL.exec "CREATE SCHEMA IF NOT EXISTS #{SCHEMA_NAME}"
|
26
23
|
|
27
|
-
|
28
|
-
|
29
|
-
CREATE TYPE #{SCHEMA_NAME}.statuses AS ENUM (
|
30
|
-
'ready', -- process can run
|
31
|
-
'sleep', -- process has external dependencies to wait for.
|
32
|
-
'failed', -- process failed, with nonrecoverable error
|
33
|
-
'err', -- process errored (with recoverable error)
|
34
|
-
'timeout', -- process timed out
|
35
|
-
'ok' -- process succeeded
|
36
|
-
);
|
37
|
-
SQL
|
24
|
+
Dir.glob(__FILE__.gsub(/\.rb$/, "/**/*.{sql,rb}")).sort.each do |file|
|
25
|
+
run_migration file
|
38
26
|
end
|
27
|
+
end
|
39
28
|
|
40
|
-
|
41
|
-
CREATE TABLE IF NOT EXISTS #{SCHEMA_NAME}.postjobs (
|
42
|
-
-- id values, readonly once created
|
43
|
-
id BIGSERIAL PRIMARY KEY, -- process id
|
44
|
-
parent_id BIGINT REFERENCES #{SCHEMA_NAME}.postjobs ON DELETE CASCADE, -- parent process id
|
45
|
-
full_id VARCHAR, -- full process id
|
46
|
-
root_id BIGINT, -- root process id
|
47
|
-
|
48
|
-
created_at timestamp NOT NULL DEFAULT (now() at time zone 'utc'), -- creation timestamp
|
49
|
-
updated_at timestamp NOT NULL DEFAULT (now() at time zone 'utc'), -- update timestamp
|
50
|
-
|
51
|
-
queue VARCHAR, -- queue name. (readonly)
|
52
|
-
workflow VARCHAR NOT NULL, -- e.g. "MyJobModule" (readonly)
|
53
|
-
workflow_method VARCHAR NOT NULL DEFAULT 'run', -- e.g. "run" (readonly)
|
54
|
-
workflow_version VARCHAR NOT NULL DEFAULT '', -- e.g. "1.0"
|
55
|
-
args JSONB, -- args
|
56
|
-
|
57
|
-
-- process state ----------------------------------------------------
|
58
|
-
|
59
|
-
status #{SCHEMA_NAME}.statuses DEFAULT 'ready',
|
60
|
-
next_run_at timestamp DEFAULT (now() at time zone 'utc'), -- when possible to run next?
|
61
|
-
timing_out_at timestamp, -- job times out after this timestamp
|
62
|
-
failed_attempts INTEGER NOT NULL DEFAULT 0, -- failed how often?
|
63
|
-
max_attempts INTEGER NOT NULL DEFAULT 1, -- maximum attempts before failing
|
64
|
-
|
65
|
-
-- process result ---------------------------------------------------
|
66
|
-
|
67
|
-
results JSONB,
|
68
|
-
error VARCHAR,
|
69
|
-
error_message VARCHAR,
|
70
|
-
error_backtrace JSONB,
|
29
|
+
private
|
71
30
|
|
72
|
-
|
73
|
-
|
74
|
-
tags JSONB
|
75
|
-
);
|
31
|
+
def run_migration(file)
|
32
|
+
Postjob.logger.info "Postjob: migrating #{file}"
|
76
33
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
SQL
|
34
|
+
case file
|
35
|
+
when /\.rb$/ then run_migration_ruby(file)
|
36
|
+
when /\.sql$/ then run_migration_sql(file)
|
37
|
+
end
|
38
|
+
end
|
83
39
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
postjob_id BIGINT REFERENCES #{SCHEMA_NAME}.postjobs ON DELETE CASCADE,
|
88
|
-
token UUID NOT NULL,
|
89
|
-
created_at timestamp NOT NULL DEFAULT (now() at time zone 'utc')
|
90
|
-
);
|
40
|
+
def run_migration_ruby(file)
|
41
|
+
eval File.read(file)
|
42
|
+
end
|
91
43
|
|
92
|
-
|
93
|
-
|
94
|
-
|
44
|
+
def run_migration_sql(file)
|
45
|
+
sql = File.read(file)
|
46
|
+
sql.gsub!(/\{([^\}]+)\}/) { |_| const_get($1) }
|
47
|
+
SQL.exec sql
|
95
48
|
end
|
96
49
|
end
|
97
50
|
end
|
@@ -3,14 +3,9 @@
|
|
3
3
|
module Postjob::Queue::Notifications
|
4
4
|
extend self
|
5
5
|
|
6
|
-
SQL
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
def notify_listeners
|
11
|
-
SQL.ask "NOTIFY #{CHANNEL}"
|
12
|
-
end
|
13
|
-
|
6
|
+
SQL = ::Postjob::Queue::SQL
|
7
|
+
CHANNEL = "postjob_notifications"
|
8
|
+
SCHEMA_NAME = ::Postjob::Queue::SCHEMA_NAME
|
14
9
|
MAX_WAIT_TIME = 120
|
15
10
|
|
16
11
|
def wait_for_new_job
|
@@ -23,8 +18,8 @@ module Postjob::Queue::Notifications
|
|
23
18
|
wait_time = time_to_next_job
|
24
19
|
return if wait_time && wait_time <= 0
|
25
20
|
|
26
|
-
wait_time
|
27
|
-
Postjob.logger.debug "postjob: waiting for notification for up to #{wait_time
|
21
|
+
wait_time = MAX_WAIT_TIME if !wait_time || wait_time > MAX_WAIT_TIME
|
22
|
+
Postjob.logger.debug "postjob: waiting for notification for up to #{wait_time} seconds"
|
28
23
|
Simple::SQL.wait_for_notify(wait_time)
|
29
24
|
|
30
25
|
# flush notifications. It is possible that a huge number of notifications
|
@@ -49,16 +44,6 @@ module Postjob::Queue::Notifications
|
|
49
44
|
# returns the maximum number of seconds to wait until the
|
50
45
|
# next runnable or timeoutable job comes up.
|
51
46
|
def time_to_next_job
|
52
|
-
Simple::SQL.ask
|
53
|
-
SELECT EXTRACT(EPOCH FROM (MIN(next_event_at) - (now() at time zone 'utc'))) FROM (
|
54
|
-
SELECT MIN(timing_out_at) AS next_event_at
|
55
|
-
FROM #{TABLE_NAME}
|
56
|
-
WHERE status IN ('ready', 'sleep', 'err')
|
57
|
-
UNION
|
58
|
-
SELECT MIN(next_run_at) AS next_event_at
|
59
|
-
FROM #{TABLE_NAME}
|
60
|
-
WHERE status = 'ready' AND (workflow || workflow_version = ANY ($1))
|
61
|
-
) sq
|
62
|
-
SQL
|
47
|
+
Simple::SQL.ask "SELECT * FROM #{SCHEMA_NAME}.time_to_next_job($1)", ::Postjob::Registry.workflows_with_versions
|
63
48
|
end
|
64
49
|
end
|
data/lib/postjob/queue/search.rb
CHANGED
@@ -6,14 +6,14 @@ end
|
|
6
6
|
module Postjob::Queue::Search
|
7
7
|
extend self
|
8
8
|
|
9
|
-
def one(id, filter: {}, into:
|
9
|
+
def one(id, filter: {}, into: nil)
|
10
10
|
query = query(page: 0, per: 1, filter: filter, id: id)
|
11
|
-
Simple::SQL.
|
11
|
+
Simple::SQL.record(query, into: into)
|
12
12
|
end
|
13
13
|
|
14
|
-
def all(page: 0, per: 100, filter: {}, into:
|
14
|
+
def all(page: 0, per: 100, filter: {}, into: nil)
|
15
15
|
query = query(page: page, per: per, filter: filter)
|
16
|
-
Simple::SQL.
|
16
|
+
Simple::SQL.records(query, into: into)
|
17
17
|
end
|
18
18
|
|
19
19
|
private
|
data/lib/postjob/queue.rb
CHANGED
@@ -1,8 +1,4 @@
|
|
1
|
-
# rubocop:disable Layout/
|
2
|
-
# rubocop:disable Style/UnneededInterpolation
|
3
|
-
# rubocop:disable Metrics/ModuleLength
|
4
|
-
# rubocop:disable Metrics/LineLength
|
5
|
-
# rubocop:disable Lint/EndAlignment
|
1
|
+
# rubocop:disable Layout/AlignParameters
|
6
2
|
# rubocop:disable Metrics/MethodLength
|
7
3
|
# rubocop:disable Metrics/ParameterLists
|
8
4
|
|
@@ -25,12 +21,6 @@ require_relative "queue/search"
|
|
25
21
|
module Postjob::Queue
|
26
22
|
Job = ::Postjob::Job
|
27
23
|
|
28
|
-
DEFAULT_OPTIONS = {
|
29
|
-
version: "",
|
30
|
-
queue: "q",
|
31
|
-
max_attempts: 5
|
32
|
-
}
|
33
|
-
|
34
24
|
# enqueues a new job with the given arguments
|
35
25
|
#
|
36
26
|
# Parameters:
|
@@ -55,197 +45,71 @@ module Postjob::Queue
|
|
55
45
|
|
56
46
|
workflow, workflow_method = parse_workflow(workflow)
|
57
47
|
|
58
|
-
|
59
|
-
|
60
|
-
SQL
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
SQL.ask <<~SQL, id, timeout
|
75
|
-
UPDATE #{TABLE_NAME}
|
76
|
-
SET timing_out_at = (now() at time zone 'utc') + $2 * interval '1 second'
|
77
|
-
WHERE id=$1
|
78
|
-
SQL
|
79
|
-
end
|
80
|
-
|
81
|
-
root_id, parent_full_id = if parent_id
|
82
|
-
SQL.ask "SELECT root_id, full_id FROM #{TABLE_NAME} WHERE id=$1", parent_id
|
83
|
-
end
|
84
|
-
|
85
|
-
root_id ||= id
|
86
|
-
full_id = parent_full_id ? "#{parent_full_id}.#{id}" : "#{id}"
|
87
|
-
|
88
|
-
SQL.ask "UPDATE #{TABLE_NAME} SET full_id=$2, root_id=$3 WHERE id=$1",
|
89
|
-
id, full_id, root_id
|
90
|
-
|
91
|
-
Notifications.notify_listeners
|
92
|
-
|
93
|
-
SQL.ask "SELECT * FROM #{TABLE_NAME} WHERE id=$1", id, into: Job
|
94
|
-
end
|
48
|
+
# The use of a `SELECT * FROM function()` here is due to
|
49
|
+
#
|
50
|
+
# a) a limitation in Simple::SQL which would not be able to unpack a
|
51
|
+
# "SELECT function()" usefully when the return value is a record;
|
52
|
+
# b) and/or my inability to write better SQL functions;
|
53
|
+
SQL.record "SELECT * FROM #{SCHEMA_NAME}.enqueue($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
54
|
+
options[:queue],
|
55
|
+
workflow,
|
56
|
+
workflow_method,
|
57
|
+
options[:version],
|
58
|
+
Encoder.encode(args),
|
59
|
+
options[:parent_id],
|
60
|
+
Encoder.encode(options[:tags]),
|
61
|
+
options[:max_attempts],
|
62
|
+
options[:timeout],
|
63
|
+
into: Job
|
95
64
|
end
|
96
65
|
|
97
66
|
def set_job_result(job, value, version:)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
SQL.ask <<~SQL, job.id, results
|
102
|
-
UPDATE #{TABLE_NAME}
|
103
|
-
SET results=$2, status='ok', next_run_at=NULL, error=NULL, error_message=NULL, error_backtrace=NULL
|
104
|
-
WHERE id=$1
|
105
|
-
SQL
|
106
|
-
|
107
|
-
wakeup(job.parent_id)
|
67
|
+
value = Encoder.encode([value]) unless value.nil?
|
68
|
+
SQL.ask "SELECT #{SCHEMA_NAME}.set_job_result($1, $2, $3)", job.id, value, version
|
108
69
|
end
|
109
70
|
|
110
71
|
def set_job_pending(job, version:)
|
111
|
-
|
112
|
-
SQL.ask <<~SQL, job.id
|
113
|
-
UPDATE #{TABLE_NAME}
|
114
|
-
SET status='sleep', next_run_at=NULL
|
115
|
-
WHERE id=$1
|
116
|
-
SQL
|
72
|
+
SQL.ask "SELECT #{SCHEMA_NAME}.set_job_pending($1, $2)", job.id, version
|
117
73
|
end
|
118
74
|
|
119
|
-
private
|
120
|
-
|
121
|
-
def remaining_attempts(job)
|
122
|
-
SQL.ask <<~SQL, job.id
|
123
|
-
SELECT max_attempts - failed_attempts
|
124
|
-
FROM #{TABLE_NAME}
|
125
|
-
WHERE id=$1
|
126
|
-
SQL
|
127
|
-
end
|
128
|
-
|
129
|
-
def next_status_and_next_run_of_failed_job(job, status)
|
130
|
-
# If this is a recoverable error and if we have another run possible we'll
|
131
|
-
# set next_run_at, and the status to "sleep", otherwise next_run_at will be
|
132
|
-
# NULL and the status would be "failed"
|
133
|
-
#
|
134
|
-
# To check if we have another run we check (max_attempts - failed_attempts).
|
135
|
-
#
|
136
|
-
# This is only necessary with a status of :err. Note that we need to
|
137
|
-
# subtract 1, since this check runs *after* the current run was done,
|
138
|
-
# but before it was written to the database.
|
139
|
-
if status == :err && remaining_attempts(job) > 1
|
140
|
-
[ "err", next_run_at_fragment ]
|
141
|
-
elsif status == :timeout
|
142
|
-
[ "timeout", "NULL" ]
|
143
|
-
else
|
144
|
-
[ "failed", "NULL" ]
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
public
|
149
|
-
|
150
75
|
def set_job_error(job, error, error_message, error_backtrace = nil, status:, version:)
|
151
|
-
|
152
|
-
|
153
|
-
new_status, next_run_at = next_status_and_next_run_of_failed_job job, status
|
154
|
-
|
155
|
-
unless error_backtrace.nil?
|
156
|
-
error_backtrace = error_backtrace.map { |path| make_relative_path(path) }
|
157
|
-
error_backtrace = Encoder.encode(error_backtrace)
|
158
|
-
end
|
159
|
-
|
160
|
-
SQL.ask <<~SQL, job.id, new_status, error, error_message, error_backtrace
|
161
|
-
UPDATE #{TABLE_NAME}
|
162
|
-
SET
|
163
|
-
status=$2, error=$3, error_message=$4, error_backtrace=$5,
|
164
|
-
failed_attempts=failed_attempts+1, next_run_at=#{next_run_at}
|
165
|
-
WHERE id=$1
|
166
|
-
SQL
|
167
|
-
|
168
|
-
wakeup(job.parent_id)
|
169
|
-
end
|
170
|
-
|
171
|
-
private
|
172
|
-
|
173
|
-
def make_relative_path(path)
|
174
|
-
@here ||= "#{Dir.getwd}/"
|
175
|
-
path.start_with?(@here) ? path[@here.length..-1] : path
|
176
|
-
end
|
177
|
-
|
178
|
-
def update_job(job, version:)
|
179
|
-
return unless version
|
180
|
-
|
181
|
-
SQL.ask <<~SQL, job.id, version
|
182
|
-
UPDATE #{TABLE_NAME}
|
183
|
-
SET workflow_version=$2, updated_at=(now() at time zone 'utc')
|
184
|
-
WHERE id=$1
|
185
|
-
SQL
|
186
|
-
end
|
187
|
-
|
188
|
-
#
|
189
|
-
# The timeout until a job can be run next is calculated by running the
|
190
|
-
# +next_run_at_fragment+ sql in the database.
|
191
|
-
#
|
192
|
-
def next_run_at_fragment
|
193
|
-
# The basetime to use with the NEXT_RUN_AT_FRAGMENT below.
|
194
|
-
next_run_at_basetime = Postjob.fast_mode ? 0.01 : 10
|
195
|
-
"(now() at time zone 'utc') + #{next_run_at_basetime} * pow(1.5, failed_attempts) * interval '1 second'"
|
196
|
-
end
|
197
|
-
|
198
|
-
def wakeup(id)
|
199
|
-
return unless id
|
76
|
+
expect! status => [ :failed, :err, :timeout ]
|
200
77
|
|
201
|
-
SQL.ask
|
202
|
-
|
203
|
-
SET status='ready', next_run_at=(now() at time zone 'utc'), updated_at=(now() at time zone 'utc')
|
204
|
-
WHERE id=$1 AND status='sleep'
|
205
|
-
SQL
|
206
|
-
|
207
|
-
Notifications.notify_listeners
|
78
|
+
SQL.ask "SELECT #{SCHEMA_NAME}.set_job_error($1, $2, $3, $4, $5, $6, $7)",
|
79
|
+
job.id, error, error_message, Encoder.encode(error_backtrace), status, version, Postjob.fast_mode
|
208
80
|
end
|
209
81
|
|
210
|
-
public
|
211
|
-
|
212
82
|
def childjobs(parent)
|
213
83
|
expect! parent => Job
|
214
|
-
|
215
|
-
SQL.all <<~SQL, parent.id, into: Job
|
216
|
-
SELECT * FROM #{TABLE_NAME}
|
217
|
-
WHERE parent_id=$1
|
218
|
-
ORDER BY id
|
219
|
-
SQL
|
84
|
+
SQL.records "SELECT * FROM #{SCHEMA_NAME}.childjobs($1)", parent.id, into: Job
|
220
85
|
end
|
221
86
|
|
222
87
|
def next_unresolved_childjob(parent)
|
223
88
|
expect! parent => Job
|
224
|
-
|
225
|
-
SQL.ask <<~SQL, parent.id, into: Job
|
226
|
-
SELECT * FROM #{TABLE_NAME}
|
227
|
-
WHERE parent_id=$1 AND status NOT IN ('ok', 'failed')
|
228
|
-
ORDER BY next_run_at
|
229
|
-
LIMIT 1
|
230
|
-
SQL
|
89
|
+
SQL.records "SELECT * FROM #{SCHEMA_NAME}.next_unresolved_childjob($1)", parent.id, into: Job
|
231
90
|
end
|
232
91
|
|
233
|
-
def find_or_create_childjob(parent, workflow, args, timeout:, max_attempts:)
|
92
|
+
def find_or_create_childjob(parent, workflow, args, timeout:, max_attempts:, queue: nil)
|
234
93
|
expect! parent => Job, workflow => String, args => Array
|
235
94
|
|
236
95
|
workflow, workflow_method = parse_workflow(workflow)
|
237
96
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
97
|
+
# The use of a `SELECT * FROM function()` here is due to
|
98
|
+
#
|
99
|
+
# a) a limitation in Simple::SQL which would not be able to unpack a
|
100
|
+
# "SELECT function()" usefully when the return value is a record;
|
101
|
+
# b) and/or my inability to write better SQL functions;
|
102
|
+
return SQL.record "SELECT * FROM #{SCHEMA_NAME}.find_or_create_childjob($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
103
|
+
queue,
|
104
|
+
workflow,
|
105
|
+
workflow_method,
|
106
|
+
nil, # version
|
107
|
+
Encoder.encode(args),
|
108
|
+
parent.id,
|
109
|
+
nil, # tags will be read from parent
|
110
|
+
max_attempts,
|
111
|
+
timeout,
|
112
|
+
into: Job
|
249
113
|
end
|
250
114
|
|
251
115
|
def set_workflow_status(job, status)
|
@@ -275,42 +139,16 @@ module Postjob::Queue
|
|
275
139
|
|
276
140
|
public
|
277
141
|
|
278
|
-
def
|
279
|
-
|
280
|
-
|
281
|
-
*,
|
282
|
-
timing_out_at <= (now() at time zone 'utc') AS timed_out
|
283
|
-
FROM #{TABLE_NAME}
|
284
|
-
WHERE
|
285
|
-
(next_run_at <= (now() at time zone 'utc') AND status IN ('ready', 'err') AND workflow || workflow_version = ANY ($1))
|
286
|
-
OR
|
287
|
-
(timing_out_at <= (now() at time zone 'utc') AND status IN ('ready', 'err', 'sleep'))
|
288
|
-
ORDER BY (LEAST(next_run_at, timing_out_at))
|
289
|
-
FOR UPDATE SKIP LOCKED
|
290
|
-
LIMIT 1
|
291
|
-
SQL
|
292
|
-
|
293
|
-
SQL.transaction do
|
294
|
-
job = SQL.ask sql, Postjob::Registry.workflows_with_versions, into: Job
|
295
|
-
yield job if job
|
296
|
-
job
|
297
|
-
end
|
142
|
+
def checkout(workflows_with_versions)
|
143
|
+
SQL.record "SELECT * FROM #{SCHEMA_NAME}.checkout($1, $2)",
|
144
|
+
workflows_with_versions, Postjob.fast_mode, into: Job
|
298
145
|
end
|
299
146
|
|
300
147
|
def find_or_create_token(job)
|
301
|
-
|
302
|
-
return token if token
|
303
|
-
|
304
|
-
token = SecureRandom.uuid
|
305
|
-
SQL.ask "INSERT INTO postjob.tokens(postjob_id, token) VALUES($1, $2)", job.id, token
|
306
|
-
token
|
148
|
+
SQL.ask "SELECT #{SCHEMA_NAME}.find_or_create_token($1)", job.id
|
307
149
|
end
|
308
150
|
|
309
151
|
def find_job_by_token(token)
|
310
|
-
SQL.
|
311
|
-
SELECT postjob.postjobs.* FROM postjob.postjobs
|
312
|
-
INNER JOIN postjob.tokens ON postjob.tokens.postjob_id=postjob.postjobs.id
|
313
|
-
WHERE postjob.tokens.token=$1
|
314
|
-
SQL
|
152
|
+
SQL.record "SELECT * FROM #{SCHEMA_NAME}.postjobs_by_token($1)", token, into: Job
|
315
153
|
end
|
316
154
|
end
|