postjob 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/postjob/cli/db.rb +3 -0
  3. data/lib/postjob/cli/job.rb +15 -7
  4. data/lib/postjob/cli/ps.rb +31 -4
  5. data/lib/postjob/cli/run.rb +4 -2
  6. data/lib/postjob/migrations/002_statuses.rb +18 -0
  7. data/lib/postjob/migrations/003_postjobs.sql +41 -0
  8. data/lib/postjob/migrations/003a_processing.sql +3 -0
  9. data/lib/postjob/migrations/003b_processing_columns.sql +31 -0
  10. data/lib/postjob/migrations/004_tokens.sql +9 -0
  11. data/lib/postjob/migrations/005_helpers.sql +28 -0
  12. data/lib/postjob/migrations/006_enqueue.sql +44 -0
  13. data/lib/postjob/migrations/006a_processing.sql +48 -0
  14. data/lib/postjob/migrations/007_job_results.sql +157 -0
  15. data/lib/postjob/migrations/008_checkout_runnable.sql +78 -0
  16. data/lib/postjob/migrations/008a_childjobs.sql +82 -0
  17. data/lib/postjob/migrations/009_tokens.sql +25 -0
  18. data/lib/postjob/migrations.rb +30 -77
  19. data/lib/postjob/queue/encoder.rb +1 -1
  20. data/lib/postjob/queue/notifications.rb +6 -21
  21. data/lib/postjob/queue/search.rb +4 -4
  22. data/lib/postjob/queue.rb +47 -209
  23. data/lib/postjob/runner.rb +22 -13
  24. data/lib/postjob.rb +21 -19
  25. data/spec/postjob/enqueue_spec.rb +26 -14
  26. data/spec/postjob/job_control/error_status_spec.rb +2 -2
  27. data/spec/postjob/job_control/manual_spec.rb +4 -6
  28. data/spec/postjob/job_control/max_attempts_spec.rb +3 -1
  29. data/spec/postjob/process_job_spec.rb +3 -2
  30. data/spec/postjob/queue/encoder_spec.rb +4 -0
  31. data/spec/postjob/run_spec.rb +1 -1
  32. data/spec/postjob/step_spec.rb +2 -2
  33. data/spec/postjob/{sub_workflow_spec.rb → workflows/child_workflow_spec.rb} +2 -2
  34. data/spec/spec_helper.rb +1 -1
  35. data/spec/support/configure_database.rb +1 -0
  36. data/spec/support/test_helper.rb +4 -4
  37. metadata +19 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b1ab40be9d1e5d2a5dce344ed912d837bf3ae567
4
- data.tar.gz: 09ee97d4ce95a137525d8efee7e0f7c24a21cb95
3
+ metadata.gz: 586b34b133d67462d64f913cf678ae14ef7954b4
4
+ data.tar.gz: 4ecb46ab647b4337c56af775110d64cc16a36cb5
5
5
  SHA512:
6
- metadata.gz: 2930f208eb34cda94e2570a823f4d32132e6ba58c078a24d0f0803c7bbccf8068b51d3832779a1e1b7eb4c40c0034f1e951970e847bcd1f73806582f3031c735
7
- data.tar.gz: 15ee51d44d1341b538808e4cb875a1c98cb518c34e68b355ef678860de8d1737ee20197fe49dd627d84b4b8803b928714df9a73e60b0a0074548cc930db4d1db
6
+ metadata.gz: a0a946eed383baa9e3c23a0bacc0a52175d917f7f976d6dc6f10b1ffe7e87ab8d3da7add93145f426f2e637613c3a0417dfd5a11404e6ac227d57a765e26a08b
7
+ data.tar.gz: 132c173dc2da7630433511078c8eb7b75766fa00da4d3e52bb88e8a05c84254f1ae27490e7d3dc9ab762af962d6f2e813d42440d1f6e204cd1305e2e16225f48
@@ -48,6 +48,9 @@ module Postjob::CLI
48
48
  USE_ACTIVE_RECORD = false
49
49
 
50
50
  def connect_to_database!
51
+ return if @connected_to_database
52
+
53
+ @connected_to_database = true
51
54
  if USE_ACTIVE_RECORD
52
55
  require "active_record"
53
56
  abc = ::Simple::SQL::Config.read_database_yml
@@ -1,5 +1,3 @@
1
- # rubocop:disable Metrics/MethodLength
2
-
3
1
  module Postjob::CLI
4
2
  # Enqueues a workflow
5
3
  #
@@ -37,15 +35,11 @@ module Postjob::CLI
37
35
  Simple::SQL.ask <<~SQL, job_ids
38
36
  UPDATE postjob.postjobs
39
37
  SET
40
- status='ready', next_run_at=now(),
38
+ status='ready', next_run_at=(now() at time zone 'utc'),
41
39
  results=null, failed_attempts=0, error=NULL, error_message=NULL, error_backtrace=NULL
42
40
  WHERE id = ANY($1);
43
41
  SQL
44
42
 
45
- Simple::SQL.ask <<~SQL
46
- NOTIFY postjob_notifications
47
- SQL
48
-
49
43
  logger.warn "The following jobs have been reset: #{job_ids.join(', ')}"
50
44
  end
51
45
 
@@ -66,6 +60,20 @@ module Postjob::CLI
66
60
  SQL
67
61
  end
68
62
 
63
+ # runs a job as soon as possible
64
+ def job_force(job_id)
65
+ connect_to_database!
66
+
67
+ job_id = Integer(job_id)
68
+
69
+ Simple::SQL.ask <<~SQL, job_id
70
+ UPDATE postjob.postjobs
71
+ SET
72
+ next_run_at=now() at time zone 'utc'
73
+ WHERE id = $1 AND status IN ('ready', 'err', 'sleep')
74
+ SQL
75
+ end
76
+
69
77
  private
70
78
 
71
79
  # parses "foo:bar,baz:quibble" into { "foo" => "bar", "baz" => "quibble"}
@@ -1,3 +1,6 @@
1
+ # rubocop:disable Lint/HandleExceptions
2
+ # rubocop:disable Metrics/ModuleLength
3
+
1
4
  module Postjob::CLI
2
5
  private
3
6
 
@@ -24,7 +27,16 @@ module Postjob::CLI
24
27
  error,
25
28
  COALESCE((results->0)::varchar, error_message) AS result,
26
29
  next_run_at,
27
- (now() at time zone 'utc') - created_at AS age,
30
+ next_run_at - (now() at time zone 'utc') AS next_run_in,
31
+ to_char(EXTRACT(EPOCH FROM (now() at time zone 'utc') - created_at), '999999999.99') AS age,
32
+ CASE WHEN processing_started_at IS NOT NULL THEN
33
+ format(
34
+ '%s/%s',
35
+ to_char(EXTRACT(EPOCH FROM (now() at time zone 'utc') - processing_started_at), '999999999.99'),
36
+ processing_max_duration
37
+ )
38
+ END AS processing,
39
+ COALESCE(processing_client, '') || COALESCE('/' || processing_client_identifier, '') AS worker,
28
40
  tags
29
41
  FROM postjob.postjobs
30
42
  WHERE #{condition_fragment}
@@ -56,6 +68,11 @@ module Postjob::CLI
56
68
  expect! limit => /\A\d+\z/
57
69
  limit = Integer(limit)
58
70
 
71
+ connect_to_database!
72
+
73
+ # check for timed out and zombie processes
74
+ # ::Postjob::Queue.checkout(nil)
75
+
59
76
  unless ids.empty?
60
77
  ps_full(*ids, limit: limit, tags: tags)
61
78
  return
@@ -70,11 +87,20 @@ module Postjob::CLI
70
87
  print_sql limit: limit, query: query
71
88
  end
72
89
 
90
+ def ps_top(*ids, limit: "100", tags: nil)
91
+ loop do
92
+ system "clear"
93
+ ps(*ids, limit: limit, tags: tags)
94
+ sleep 1
95
+ end
96
+ rescue Interrupt
97
+ end
98
+
73
99
  # Show all information about this job
74
100
  def ps_show(id, *ids)
75
101
  ids = ([id] + ids).map { |s| Integer(s) }
76
102
 
77
- jobs = Simple::SQL.all <<~SQL, ids, into: Postjob::Job
103
+ jobs = Simple::SQL.records <<~SQL, ids, into: Postjob::Job
78
104
  SELECT * FROM postjob.postjobs WHERE id = ANY($1)
79
105
  SQL
80
106
 
@@ -84,6 +110,8 @@ module Postjob::CLI
84
110
  end
85
111
 
86
112
  def ps_full(*ids, limit: 100, tags: nil)
113
+ connect_to_database!
114
+
87
115
  conditions = []
88
116
  conditions << tags_condition(tags)
89
117
  conditions << ids_condition(ids)
@@ -110,8 +138,7 @@ module Postjob::CLI
110
138
  end
111
139
 
112
140
  def print_sql(limit:, query:)
113
- connect_to_database!
114
- records = Simple::SQL.all("#{query} LIMIT $1+1", limit, into: Hash)
141
+ records = Simple::SQL.records("#{query} LIMIT $1+1", limit)
115
142
 
116
143
  tp records[0, limit]
117
144
  if records.length > limit
@@ -20,8 +20,10 @@ module Postjob::CLI
20
20
 
21
21
  connect_to_database!
22
22
 
23
- processed = Postjob.run(count: count) do |job|
24
- logger.info "Processed job w/id #{job.id}" if job
23
+ logger.success "Starting runner with pid #{$$}"
24
+
25
+ processed = Postjob.run(count: count) do |job_id|
26
+ logger.info "Processed job w/id #{job_id}" if job_id
25
27
  STDERR.print "." unless quiet
26
28
  end
27
29
 
@@ -0,0 +1,18 @@
1
+ pg_types = <<~SQL
2
+ SELECT pg_namespace.nspname AS schema, pg_type.typname AS name
3
+ FROM pg_type
4
+ LEFT JOIN pg_namespace on pg_namespace.oid=pg_type.typnamespace
5
+ SQL
6
+
7
+ unless SQL.ask("SELECT 1 FROM (#{pg_types}) sq WHERE (schema,name) = ($1, $2)", SCHEMA_NAME, "statuses")
8
+ SQL.exec <<~SQL
9
+ CREATE TYPE #{SCHEMA_NAME}.statuses AS ENUM (
10
+ 'ready', -- process can run
11
+ 'sleep', -- process has external dependencies to wait for.
12
+ 'failed', -- process failed, with nonrecoverable error
13
+ 'err', -- process errored (with recoverable error)
14
+ 'timeout', -- process timed out
15
+ 'ok' -- process succeeded
16
+ );
17
+ SQL
18
+ end
@@ -0,0 +1,41 @@
1
+ CREATE TABLE IF NOT EXISTS {SCHEMA_NAME}.postjobs (
2
+ -- id values, readonly once created
3
+ id BIGSERIAL PRIMARY KEY, -- process id
4
+ parent_id BIGINT REFERENCES {SCHEMA_NAME}.postjobs ON DELETE CASCADE, -- parent process id
5
+ full_id VARCHAR, -- full process id
6
+ root_id BIGINT, -- root process id
7
+
8
+ created_at timestamp NOT NULL DEFAULT (now() at time zone 'utc'), -- creation timestamp
9
+ updated_at timestamp NOT NULL DEFAULT (now() at time zone 'utc'), -- update timestamp
10
+
11
+ queue VARCHAR, -- queue name. (readonly)
12
+ workflow VARCHAR NOT NULL, -- e.g. "MyJobModule" (readonly)
13
+ workflow_method VARCHAR NOT NULL DEFAULT 'run', -- e.g. "run" (readonly)
14
+ workflow_version VARCHAR NOT NULL DEFAULT '', -- e.g. "1.0"
15
+ args JSONB, -- args
16
+
17
+ -- process state ----------------------------------------------------
18
+
19
+ status {SCHEMA_NAME}.statuses DEFAULT 'ready',
20
+ next_run_at timestamp DEFAULT (now() at time zone 'utc'), -- when possible to run next?
21
+ timing_out_at timestamp, -- job times out after this timestamp
22
+ failed_attempts INTEGER NOT NULL DEFAULT 0, -- failed how often?
23
+ max_attempts INTEGER NOT NULL DEFAULT 1, -- maximum attempts before failing
24
+
25
+ -- process result ---------------------------------------------------
26
+
27
+ results JSONB,
28
+ error VARCHAR,
29
+ error_message VARCHAR,
30
+ error_backtrace JSONB,
31
+
32
+ -- custom fields
33
+ workflow_status VARCHAR,
34
+ tags JSONB
35
+ );
36
+
37
+ -- [TODO] check indices
38
+ CREATE INDEX IF NOT EXISTS postjobs_tags_idx
39
+ ON {SCHEMA_NAME}.postjobs USING GIN (tags jsonb_path_ops);
40
+ CREATE INDEX IF NOT EXISTS postjobs_parent_id_idx
41
+ ON {SCHEMA_NAME}.postjobs(parent_id);
@@ -0,0 +1,3 @@
1
+ -- Apparently this line must be kept in a file of its own.
2
+ ALTER TYPE {SCHEMA_NAME}.statuses
3
+ ADD value IF NOT EXISTS 'processing' AFTER 'ready';
@@ -0,0 +1,31 @@
1
+ DO $$
2
+ BEGIN
3
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN processing_client varchar;
4
+ EXCEPTION
5
+ WHEN duplicate_column THEN RAISE NOTICE 'column {SCHEMA_NAME}.postjobs.processing_client already exists';
6
+ END;
7
+ $$;
8
+
9
+ DO $$
10
+ BEGIN
11
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN processing_client_identifier varchar;
12
+ EXCEPTION
13
+ WHEN duplicate_column THEN RAISE NOTICE 'column {SCHEMA_NAME}.postjobs.processing_client_identifier already exists';
14
+ END;
15
+ $$;
16
+
17
+ DO $$
18
+ BEGIN
19
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN processing_started_at timestamp;
20
+ EXCEPTION
21
+ WHEN duplicate_column THEN RAISE NOTICE 'column {SCHEMA_NAME}.postjobs.processing_started_at already exists';
22
+ END;
23
+ $$;
24
+
25
+ DO $$
26
+ BEGIN
27
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN processing_max_duration float;
28
+ EXCEPTION
29
+ WHEN duplicate_column THEN RAISE NOTICE 'column {SCHEMA_NAME}.postjobs.processing_max_duration already exists';
30
+ END;
31
+ $$;
@@ -0,0 +1,9 @@
1
+ CREATE TABLE IF NOT EXISTS {SCHEMA_NAME}.tokens (
2
+ id BIGSERIAL PRIMARY KEY,
3
+ postjob_id BIGINT REFERENCES {SCHEMA_NAME}.postjobs ON DELETE CASCADE,
4
+ token UUID NOT NULL,
5
+ created_at timestamp NOT NULL DEFAULT (now() at time zone 'utc')
6
+ );
7
+
8
+ CREATE INDEX IF NOT EXISTS tokens_postjob_id_idx ON {SCHEMA_NAME}.tokens(postjob_id);
9
+ CREATE INDEX IF NOT EXISTS tokens_token_idx ON {SCHEMA_NAME}.tokens(token);
@@ -0,0 +1,28 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._wakeup_runners() RETURNS TRIGGER AS $$
2
+ BEGIN
3
+ NOTIFY {CHANNEL};
4
+ RETURN NEW;
5
+ END;
6
+ $$ LANGUAGE plpgsql;
7
+
8
+ BEGIN;
9
+ DROP TRIGGER IF EXISTS _wakeup_runners ON {TABLE_NAME};
10
+
11
+ CREATE TRIGGER _wakeup_runners AFTER INSERT OR UPDATE
12
+ ON {TABLE_NAME}
13
+ FOR EACH STATEMENT
14
+ EXECUTE PROCEDURE {SCHEMA_NAME}._wakeup_runners();
15
+ COMMIT;
16
+
17
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._wakeup_parent_job(job_id BIGINT) RETURNS VOID AS $$
18
+ BEGIN
19
+ UPDATE postjobs
20
+ SET
21
+ status='ready',
22
+ next_run_at=(now() at time zone 'utc'),
23
+ updated_at=(now() at time zone 'utc')
24
+ WHERE
25
+ status='sleep'
26
+ AND id=(SELECT parent_id FROM postjobs WHERE id=job_id);
27
+ END;
28
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,44 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.enqueue(
2
+ queue VARCHAR,
3
+ workflow VARCHAR,
4
+ workflow_method VARCHAR,
5
+ workflow_version VARCHAR,
6
+ args JSONB,
7
+ parent_id BIGINT,
8
+ tags JSONB,
9
+ max_attempts INTEGER,
10
+ timeout DOUBLE PRECISION)
11
+ RETURNS SETOF {SCHEMA_NAME}.postjobs
12
+ AS $$
13
+ DECLARE
14
+ job_id BIGINT;
15
+ BEGIN
16
+ -- set defaults ---------------------------------------------------
17
+ workflow_version := COALESCE(workflow_version, '');
18
+ queue := COALESCE(queue, 'q');
19
+ max_attempts := COALESCE(max_attempts, 5);
20
+
21
+ -- create postjobs entry ------------------------------------------
22
+ INSERT INTO {SCHEMA_NAME}.postjobs (
23
+ queue, workflow, workflow_method, workflow_version, args,
24
+ parent_id, tags, max_attempts,
25
+ timing_out_at
26
+ )
27
+ VALUES(
28
+ queue, workflow, workflow_method, workflow_version, args,
29
+ parent_id, tags, max_attempts,
30
+ (now() at time zone 'utc') + timeout * interval '1 second'
31
+ ) RETURNING {SCHEMA_NAME}.postjobs.id INTO job_id;
32
+
33
+ -- fill in root_id and full_id ------------------------------------
34
+ UPDATE postjobs
35
+ SET
36
+ root_id=COALESCE((SELECT root_id FROM postjobs s WHERE s.id=postjobs.parent_id), id),
37
+ full_id=COALESCE((SELECT full_id FROM postjobs s WHERE s.id=postjobs.parent_id) || '.' || id, id::varchar)
38
+ WHERE id=job_id;
39
+
40
+ -- return the job -------------------------------------------------
41
+ RETURN QUERY
42
+ SELECT * FROM postjobs WHERE id=job_id;
43
+ END;
44
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,48 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.set_client_identifier(client_identifier varchar) RETURNS VOID AS $$
2
+ BEGIN
3
+ PERFORM set_config('{SCHEMA_NAME}.client_identifier', client_identifier, false);
4
+ END;
5
+ $$ LANGUAGE plpgsql;
6
+
7
+ --SELECT {SCHEMA_NAME}.set_client_identifier('nope');
8
+
9
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._client_identifier() RETURNS varchar AS $$
10
+ BEGIN
11
+ RETURN current_setting('{SCHEMA_NAME}.client_identifier');
12
+ EXCEPTION
13
+ WHEN OTHERS THEN RETURN NULL;
14
+ END;
15
+ $$ LANGUAGE plpgsql;
16
+
17
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._set_job_processing(job_id BIGINT) RETURNS VOID AS $$
18
+ DECLARE
19
+ v_pid int;
20
+ BEGIN
21
+ v_pid := pg_backend_pid();
22
+
23
+ UPDATE postjobs
24
+ SET
25
+ status='processing',
26
+ processing_client=(SELECT client_addr || ':' || client_port FROM pg_stat_activity WHERE pid = v_pid),
27
+ processing_client_identifier={SCHEMA_NAME}._client_identifier(),
28
+ processing_started_at=now() at time zone 'utc',
29
+ processing_max_duration=30 * 60, -- default max duration of processing: 30 minutes.
30
+ error=NULL,
31
+ error_message=NULL,
32
+ error_backtrace=NULL,
33
+ next_run_at=NULL
34
+ WHERE id=job_id;
35
+ END;
36
+ $$ LANGUAGE plpgsql;
37
+
38
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._reset_job_processing(job_id BIGINT) RETURNS VOID AS $$
39
+ BEGIN
40
+ UPDATE postjobs
41
+ SET
42
+ processing_client=NULL,
43
+ processing_client_identifier=NULL,
44
+ processing_started_at=NULL,
45
+ processing_max_duration=NULL
46
+ WHERE id=job_id;
47
+ END;
48
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,157 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._update_job_version(job_id BIGINT, p_version VARCHAR) RETURNS VOID AS $$
2
+ BEGIN
3
+ IF p_version IS NOT NULL THEN
4
+ UPDATE postjobs
5
+ SET
6
+ workflow_version=p_version,
7
+ updated_at=(now() at time zone 'utc')
8
+ WHERE id=job_id;
9
+ END IF;
10
+ END;
11
+ $$ LANGUAGE plpgsql;
12
+
13
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.set_job_result(job_id BIGINT, p_results JSONB, p_version VARCHAR) RETURNS VOID AS $$
14
+ BEGIN
15
+ PERFORM _update_job_version(job_id, p_version);
16
+ PERFORM _reset_job_processing(job_id);
17
+
18
+ UPDATE postjobs
19
+ SET
20
+ results=p_results,
21
+ error=NULL,
22
+ error_message=NULL,
23
+ error_backtrace=NULL,
24
+ status='ok',
25
+ next_run_at=NULL
26
+ WHERE id=job_id;
27
+
28
+ PERFORM _wakeup_parent_job(job_id);
29
+ END;
30
+ $$ LANGUAGE plpgsql;
31
+
32
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.set_job_pending(job_id BIGINT, p_version VARCHAR) RETURNS VOID AS $$
33
+ BEGIN
34
+ PERFORM _update_job_version(job_id, p_version);
35
+ PERFORM _reset_job_processing(job_id);
36
+
37
+ UPDATE postjobs
38
+ SET status='sleep', next_run_at=NULL
39
+ WHERE id=job_id;
40
+ END;
41
+ $$ LANGUAGE plpgsql;
42
+
43
+ -- If this is a recoverable error and if we have another run possible
44
+ -- we'll set next_run_at, and the status to "err", otherwise
45
+ -- next_run_at will be NULL and the status would be "failed" or "timeout"
46
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._prepare_next_run(
47
+ job_id BIGINT,
48
+ p_status {SCHEMA_NAME}.statuses,
49
+ p_fast_mode BOOLEAN) RETURNS VOID AS $$
50
+ DECLARE
51
+ next_run_at_basetime DOUBLE PRECISION;
52
+ BEGIN
53
+ IF p_status NOT IN ('err', 'timeout', 'failed') THEN
54
+ RAISE 'Invalid status value %', p_status;
55
+ END IF;
56
+
57
+ -- If this is a recoverable error we check if we have any remaining attempts.
58
+ IF p_status = 'err' THEN
59
+ IF (SELECT max_attempts - failed_attempts FROM postjobs WHERE id=job_id) > 0 THEN
60
+ p_status = 'err';
61
+ ELSE
62
+ p_status = 'failed';
63
+ END IF;
64
+ END IF;
65
+
66
+ -- set status, clear next_run_at
67
+ UPDATE postjobs
68
+ SET
69
+ status=p_status,
70
+ next_run_at=NULL
71
+ WHERE id=job_id;
72
+
73
+ -- fill in next_run_at
74
+ IF p_status != 'failed' AND p_status != 'timeout' THEN
75
+ next_run_at_basetime := CASE WHEN p_fast_mode THEN 0.01 ELSE 10 END;
76
+
77
+ UPDATE postjobs
78
+ SET
79
+ next_run_at=(now() at time zone 'utc') + next_run_at_basetime * pow(1.5, failed_attempts) * interval '1 second'
80
+ WHERE id=job_id;
81
+ END IF;
82
+ END;
83
+ $$ LANGUAGE plpgsql;
84
+
85
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.set_job_error(
86
+ job_id BIGINT,
87
+ p_error VARCHAR,
88
+ p_error_message VARCHAR,
89
+ p_error_backtrace JSONB,
90
+ p_status {SCHEMA_NAME}.statuses,
91
+ p_version VARCHAR,
92
+ p_fast_mode BOOLEAN) RETURNS VOID AS $$
93
+ BEGIN
94
+ PERFORM _update_job_version(job_id, p_version);
95
+ PERFORM _reset_job_processing(job_id);
96
+
97
+ -- write error info
98
+ UPDATE postjobs
99
+ SET
100
+ error=p_error,
101
+ error_message=p_error_message,
102
+ error_backtrace=p_error_backtrace,
103
+ failed_attempts=failed_attempts+1,
104
+ next_run_at=NULL
105
+ WHERE id=job_id;
106
+
107
+ -- prepare next run, if any
108
+ PERFORM _prepare_next_run(job_id, p_status, p_fast_mode);
109
+ PERFORM _wakeup_parent_job(job_id);
110
+ END;
111
+ $$ LANGUAGE plpgsql;
112
+
113
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._set_job_timeout(
114
+ job_id BIGINT,
115
+ p_fast_mode BOOLEAN) RETURNS VOID AS $$
116
+ BEGIN
117
+ PERFORM _reset_job_processing(job_id);
118
+
119
+ -- write error info
120
+ UPDATE postjobs
121
+ SET
122
+ error='Timeout',
123
+ error_message='timeout',
124
+ error_backtrace=NULL,
125
+ failed_attempts=failed_attempts+1,
126
+ next_run_at=NULL
127
+ WHERE id=job_id;
128
+
129
+ -- prepare next run, if any
130
+ PERFORM _prepare_next_run(job_id, 'timeout', p_fast_mode);
131
+ PERFORM _wakeup_parent_job(job_id);
132
+ END;
133
+ $$ LANGUAGE plpgsql;
134
+
135
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._set_job_zombie(
136
+ job_id BIGINT,
137
+ p_fast_mode BOOLEAN) RETURNS VOID AS $$
138
+ BEGIN
139
+ PERFORM _reset_job_processing(job_id);
140
+
141
+ RAISE NOTICE 'job % is a zombie', job_id;
142
+
143
+ -- write error info
144
+ UPDATE postjobs
145
+ SET
146
+ error='Zombie',
147
+ error_message='zombie',
148
+ error_backtrace=NULL,
149
+ failed_attempts=failed_attempts+1,
150
+ next_run_at=NULL
151
+ WHERE id=job_id;
152
+
153
+ -- prepare next run, if any
154
+ PERFORM _prepare_next_run(job_id, 'err', p_fast_mode);
155
+ PERFORM _wakeup_parent_job(job_id);
156
+ END;
157
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,78 @@
1
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.time_to_next_job(workflows_with_versions varchar[])
2
+ RETURNS float
3
+ AS $$
4
+ DECLARE
5
+ p_processable_at timestamp;
6
+ BEGIN
7
+ SELECT MIN(processable_at) INTO p_processable_at FROM (
8
+ SELECT MIN(processing_started_at + processing_max_duration * interval '1 second') AS processable_at
9
+ FROM {TABLE_NAME}
10
+ WHERE status IN ('processing')
11
+ UNION
12
+ SELECT MIN(timing_out_at) AS processable_at
13
+ FROM {TABLE_NAME}
14
+ WHERE status IN ('ready', 'err', 'sleep')
15
+ UNION
16
+ SELECT MIN(next_run_at) AS processable_at
17
+ FROM {TABLE_NAME}
18
+ WHERE status IN ('ready', 'err')
19
+ AND workflow || workflow_version = ANY ($1)
20
+ ) sq;
21
+
22
+ RETURN EXTRACT(EPOCH FROM p_processable_at - (now() at time zone 'utc'));
23
+ END;
24
+ $$ LANGUAGE plpgsql;
25
+
26
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.checkout(workflows_with_versions varchar[], p_fast_mode BOOLEAN)
27
+ RETURNS SETOF {TABLE_NAME}
28
+ AS $$
29
+ DECLARE
30
+ job {TABLE_NAME};
31
+ BEGIN
32
+ LOOP
33
+ -- try to checkout a job. Each of the conditions here is matching
34
+ -- one of the CASE .. WHEN clauses below.
35
+ SELECT INTO job *
36
+ FROM {TABLE_NAME} s
37
+ WHERE
38
+ (
39
+ s.status IN ('processing')
40
+ AND (s.processing_started_at + s.processing_max_duration * interval '1 second') <= (now() at time zone 'utc')
41
+ )
42
+ OR
43
+ (
44
+ s.status IN ('ready', 'err', 'sleep')
45
+ AND s.timing_out_at <= (now() at time zone 'utc')
46
+ )
47
+ OR
48
+ (
49
+ s.status IN ('ready', 'err')
50
+ AND s.next_run_at <= (now() at time zone 'utc')
51
+ AND s.workflow || s.workflow_version = ANY ($1)
52
+ )
53
+ ORDER BY (LEAST(s.next_run_at, s.timing_out_at, (s.processing_started_at + s.processing_max_duration * interval '1 second')))
54
+ FOR UPDATE SKIP LOCKED
55
+ LIMIT 1;
56
+
57
+ CASE
58
+ WHEN job.id IS NULL THEN
59
+ -- couldn't find a job?
60
+ EXIT;
61
+ WHEN job.status='processing' AND
62
+ (job.processing_started_at + job.processing_max_duration * interval '1 second') <= (now() at time zone 'utc') THEN
63
+ -- a zombie: the worker probably died.
64
+ PERFORM {SCHEMA_NAME}._set_job_zombie(job.id, p_fast_mode);
65
+ CONTINUE;
66
+ WHEN job.status IN ('ready', 'err', 'sleep') AND job.timing_out_at <= (now() at time zone 'utc') THEN
67
+ -- job timed out? mark it as such, and try next one.
68
+ PERFORM {SCHEMA_NAME}._set_job_timeout(job.id, p_fast_mode);
69
+ CONTINUE;
70
+ ELSE
71
+ -- set job to processing
72
+ PERFORM _set_job_processing(job.id);
73
+ RETURN QUERY SELECT * FROM {TABLE_NAME} WHERE id=job.id;
74
+ EXIT;
75
+ END CASE;
76
+ END LOOP;
77
+ END;
78
+ $$ LANGUAGE plpgsql;