postjob 0.5.5 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +5 -5
  2. data/lib/postjob/cli/cron.rb +16 -0
  3. data/lib/postjob/cli/enqueue.rb +14 -0
  4. data/lib/postjob/cli/job.rb +15 -19
  5. data/lib/postjob/cli/ps.rb +8 -2
  6. data/lib/postjob/host.rb +41 -0
  7. data/lib/postjob/job.rb +23 -23
  8. data/lib/postjob/migrations/006_enqueue.sql +48 -4
  9. data/lib/postjob/migrations/006a_processing.sql +5 -2
  10. data/lib/postjob/migrations/007_job_results.sql +6 -5
  11. data/lib/postjob/migrations/008a_childjobs.sql +33 -38
  12. data/lib/postjob/migrations/010_settings.sql +1 -1
  13. data/lib/postjob/migrations/{008_checkout_runnable.sql → 013a_checkout_runnable.sql} +11 -7
  14. data/lib/postjob/migrations/017_zombie_check.sql +11 -6
  15. data/lib/postjob/migrations/021_cron_jobs.sql +65 -0
  16. data/lib/postjob/migrations/023_sticky_jobs.sql +16 -0
  17. data/lib/postjob/migrations.rb +3 -1
  18. data/lib/postjob/queue.rb +23 -4
  19. data/lib/postjob/record.rb +4 -10
  20. data/lib/postjob/registry.rb +117 -20
  21. data/lib/postjob/runner.rb +169 -166
  22. data/lib/postjob/worker_session.rb +9 -25
  23. data/lib/postjob/workflow.rb +26 -50
  24. data/lib/postjob.rb +97 -23
  25. data/lib/tools/heartbeat.rb +2 -2
  26. data/lib/tools/history.rb +1 -1
  27. data/spec/postjob/enqueue_spec.rb +3 -0
  28. data/spec/postjob/events/heartbeat_event_spec.rb +5 -67
  29. data/spec/postjob/events/zombie_event_spec.rb +61 -0
  30. data/spec/postjob/worker_session_spec.rb +1 -24
  31. data/spec/spec_helper.rb +1 -1
  32. data/spec/support/configure_active_record.rb +7 -16
  33. data/spec/support/configure_database.rb +11 -0
  34. data/spec/support/configure_simple_sql.rb +0 -16
  35. metadata +10 -5
  36. data/lib/tools/atomic_store.rb +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 4f4e54ce9a3522400d5f29d0ebfbb8552b98e72b
4
- data.tar.gz: 66154224d10c0fb2b5d72e350476ccb817c5634e
2
+ SHA256:
3
+ metadata.gz: 574e3a114fa762835314b2e72a2d12b73840569661d9ff8ab06f1ba08d95fb92
4
+ data.tar.gz: e922e1f1116423b4895a4725107f0456260fa6382edcb308e220fdf62622b528
5
5
  SHA512:
6
- metadata.gz: 6e48d531c2da7d304e351630faa9f535b380009efa5aac4fef2f60a3f6cf339d0a8bcf34e59467f435a993daff02210f3eec4509bf218daee31f10a5c3ad08db
7
- data.tar.gz: 2d588a518f7602595809a06c9b44d93e69b268f55bc1eec5a60419ec2218369bf18f5c5d69c379dbcf37a2423c0ca4c9064ed5474dd2f4ee3634f1384538f017
6
+ metadata.gz: 9d7a61a2298c1fc15efc9ce424111d1baf102326b4f73a8314b3e42085a3929443390df44f8e721ef9bac0d4d2273d57a21c8252a4e30a140c6c523ee3766939
7
+ data.tar.gz: 782b95ae3ab74ad021772b7eb4f3886b8c92835ef3484fa34f1c4ba84792db80e5448273a8318661004d4a36229f554674b87ee00e453882388a27a72d7403be
@@ -0,0 +1,16 @@
1
+ module Postjob::CLI
2
+ # Enqueues a cron workflow
3
+ def cron_enqueue(workflow, *args, interval:, queue: "ruby", tags: nil)
4
+ interval = Integer(interval)
5
+
6
+ connect_to_database!
7
+
8
+ Postjob.enqueue! workflow, *args, queue: queue, tags: parse_tags(tags), cron_interval: interval
9
+ end
10
+
11
+ # Disable an existing cron job
12
+ def cron_disable(workflow, *args)
13
+ connect_to_database!
14
+ Postjob::Queue.disable_cron_jobs(workflow, args)
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Postjob::CLI
2
+ # Enqueues a workflow
3
+ #
4
+ # Adds a workflow to the job table, with name <workflow> and the given
5
+ # arguments.
6
+ #
7
+ # Note that the workflow will receive the arguments as strings and must be
8
+ # prepared to handle these.
9
+ def enqueue(workflow, *args, queue: "ruby", tags: nil)
10
+ connect_to_database!
11
+
12
+ Postjob.enqueue! workflow, *args, queue: queue, tags: parse_tags(tags)
13
+ end
14
+ end
@@ -5,23 +5,9 @@ module Postjob::CLI
5
5
  Postjob.resolve(token: token, result: result)
6
6
  end
7
7
 
8
- # # Fail a job by token
9
- # def job_resolve(token, result)
10
- # connect_to_database!
11
- # Postjob.resolve(token: token, result: result)
12
- # end
13
-
14
- # Enqueues a workflow
15
- #
16
- # Adds a workflow to the job table, with name <workflow> and the given
17
- # arguments.
18
- #
19
- # Note that the workflow will receive the arguments as strings and must be
20
- # prepared to handle these.
21
8
  def job_enqueue(workflow, *args, queue: "ruby", tags: nil)
22
- connect_to_database!
23
-
24
- Postjob.enqueue! workflow, *args, queue: queue, tags: parse_tags(tags)
9
+ logger "The job:enqueue command is deprecated, pls use enqueue instead."
10
+ enqueue(workflow, *args, queue: queue, tags: tags)
25
11
  end
26
12
 
27
13
  # Reset failed jobs
@@ -101,8 +87,18 @@ module Postjob::CLI
101
87
  public
102
88
 
103
89
  def registry
104
- workflows = Postjob::Registry.workflows
105
- names = workflows.map(&:workflow_name)
106
- puts names.sort.join("\n")
90
+ workflows_with_versions = Postjob::Registry.workflows.keys.reject { |k| k[1] == "" }
91
+ workflows_with_versions = workflows_with_versions.sort_by { |name, _version| name }
92
+
93
+ data = workflows_with_versions.map do |name, version|
94
+ spec = Postjob::Registry.lookup! name: name, version: version
95
+ {
96
+ name: name,
97
+ version: version.inspect,
98
+ options: spec.options.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
99
+ }
100
+ end
101
+
102
+ tp data
107
103
  end
108
104
  end
@@ -24,9 +24,15 @@ module Postjob::CLI
24
24
  END AS status,
25
25
  error,
26
26
  COALESCE((results->0)::varchar, error_message) AS result,
27
+ max_attempts,
28
+ cron_interval AS cron,
29
+ CASE
30
+ WHEN is_sticky THEN COALESCE(sticky_host_id::varchar, 'yes')
31
+ ELSE 'no'
32
+ END AS sticky,
27
33
  next_run_at,
28
- next_run_at - (now() at time zone 'utc') AS next_run_in,
29
- to_char(EXTRACT(EPOCH FROM (now() at time zone 'utc') - postjobs.created_at), '999999999.99') AS age,
34
+ to_char(EXTRACT(EPOCH FROM (next_run_at - now() at time zone 'utc')), '999999999.99') AS next_run_in,
35
+ to_char(EXTRACT(EPOCH FROM (now() at time zone 'utc' - postjobs.created_at)), '999999999.99') AS age,
30
36
 
31
37
  tags
32
38
  FROM postjob.postjobs AS postjobs
@@ -0,0 +1,41 @@
1
+ require_relative "./record"
2
+
3
+ class Postjob::Host < Postjob::Record
4
+ class << self
5
+ def clear_storage
6
+ File.unlink(storage_path) if File.exist?(storage_path)
7
+ end
8
+
9
+ def host_id
10
+ @host_id ||= atomic_set_and_get(storage_path) { register_host }
11
+ end
12
+
13
+ private
14
+
15
+ def storage_path
16
+ env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
17
+ ".postjob.#{env}.host_id"
18
+ end
19
+
20
+ def atomic_set_and_get(path)
21
+ value = nil
22
+ File.open(path, File::RDWR | File::CREAT, 0644) do |f|
23
+ f.flock(File::LOCK_EX)
24
+
25
+ value = f.read
26
+ value = yield if value == "" || value.nil?
27
+ f.rewind
28
+ f.write(value)
29
+ f.flush
30
+ f.truncate(f.pos)
31
+ end
32
+ value
33
+ end
34
+
35
+ def register_host
36
+ attributes = {}
37
+ Postjob.logger.debug "registering host w/#{attributes.inspect}"
38
+ ::Postjob::Queue.host_register(attributes)
39
+ end
40
+ end
41
+ end
data/lib/postjob/job.rb CHANGED
@@ -9,29 +9,29 @@ class Postjob::Job < Postjob::Record
9
9
  Simple::SQL.ask(scope, into: Postjob::Job)
10
10
  end
11
11
 
12
- attribute :id
13
- attribute :parent_id
14
- attribute :full_id
15
- attribute :root_id
16
- attribute :created_at
17
- attribute :queue
18
- attribute :workflow
19
- attribute :workflow_method
20
- attribute :workflow_version
21
- attribute :args
22
- attribute :next_run_at
23
- attribute :timing_out_at
24
- attribute :failed_attempts
25
- attribute :max_attempts
26
- attribute :status
27
- attribute :results
28
- attribute :error
29
- attribute :error_message
30
- attribute :error_backtrace
31
- attribute :recipients
32
- attribute :timed_out
33
- attribute :tags
34
- attribute :last_worker_session_id
12
+ attr_reader :id
13
+ attr_reader :parent_id
14
+ attr_reader :full_id
15
+ attr_reader :root_id
16
+ attr_reader :created_at
17
+ attr_reader :queue
18
+ attr_reader :workflow
19
+ attr_reader :workflow_method
20
+ attr_reader :workflow_version
21
+ attr_reader :args
22
+ attr_reader :next_run_at
23
+ attr_reader :timing_out_at
24
+ attr_reader :failed_attempts
25
+ attr_reader :max_attempts
26
+ attr_reader :status
27
+ attr_reader :results
28
+ attr_reader :error
29
+ attr_reader :error_message
30
+ attr_reader :error_backtrace
31
+ attr_reader :recipients
32
+ attr_reader :timed_out
33
+ attr_reader :tags
34
+ attr_reader :last_worker_session_id
35
35
 
36
36
  STATUSES = %w(ok ready processing sleep err failed timeout)
37
37
 
@@ -1,3 +1,28 @@
1
+ DROP FUNCTION IF EXISTS {SCHEMA_NAME}.enqueue(
2
+ p_worker_session_id UUID,
3
+ queue VARCHAR,
4
+ workflow VARCHAR,
5
+ workflow_method VARCHAR,
6
+ workflow_version VARCHAR,
7
+ args JSONB,
8
+ parent_id BIGINT,
9
+ tags JSONB,
10
+ max_attempts INTEGER,
11
+ timeout DOUBLE PRECISION);
12
+
13
+ DROP FUNCTION IF EXISTS {SCHEMA_NAME}.enqueue(
14
+ p_worker_session_id UUID,
15
+ queue VARCHAR,
16
+ workflow VARCHAR,
17
+ workflow_method VARCHAR,
18
+ workflow_version VARCHAR,
19
+ args JSONB,
20
+ parent_id BIGINT,
21
+ tags JSONB,
22
+ max_attempts INTEGER,
23
+ timeout DOUBLE PRECISION,
24
+ p_cron_interval integer);
25
+
1
26
  CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.enqueue(
2
27
  p_worker_session_id UUID,
3
28
  queue VARCHAR,
@@ -8,34 +33,53 @@ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.enqueue(
8
33
  parent_id BIGINT,
9
34
  tags JSONB,
10
35
  max_attempts INTEGER,
11
- timeout DOUBLE PRECISION)
36
+ timeout DOUBLE PRECISION,
37
+ p_cron_interval INTEGER,
38
+ p_is_sticky BOOLEAN)
12
39
  RETURNS SETOF {SCHEMA_NAME}.postjobs
13
40
  AS $$
14
41
  DECLARE
15
42
  job_id BIGINT;
16
43
  BEGIN
17
- -- set defaults ---------------------------------------------------
44
+ -- check arguments --------------------------------------------------------
45
+
18
46
  IF workflow = '__manual__' AND COALESCE(max_attempts, 1) != 1 THEN
19
47
  RAISE NOTICE 'Adjusting max_attempts of __manual__ job to 1';
20
48
  max_attempts := 1;
21
49
  END IF;
22
50
 
51
+ IF p_cron_interval IS NOT NULL THEN
52
+ IF parent_id IS NOT NULL THEN
53
+ RAISE 'A cron job must be a root job';
54
+ END IF;
55
+ IF p_cron_interval < 1 THEN
56
+ RAISE 'The cron_interval must be positive';
57
+ END IF;
58
+ END IF;
59
+
60
+ -- check arguments --------------------------------------------------------
61
+
23
62
  workflow_version := COALESCE(workflow_version, '');
24
63
  queue := COALESCE(queue, 'q');
25
64
  max_attempts := COALESCE(max_attempts, 5);
65
+ p_is_sticky := COALESCE(p_is_sticky, FALSE);
26
66
 
27
67
  -- create postjobs entry ------------------------------------------
28
68
  INSERT INTO {SCHEMA_NAME}.postjobs (
29
69
  last_worker_session_id,
30
70
  queue, workflow, workflow_method, workflow_version, args,
31
71
  parent_id, tags, max_attempts,
32
- timing_out_at
72
+ timing_out_at,
73
+ cron_interval,
74
+ is_sticky
33
75
  )
34
76
  VALUES(
35
77
  p_worker_session_id,
36
78
  queue, workflow, workflow_method, workflow_version, args,
37
79
  parent_id, tags, max_attempts,
38
- (now() at time zone 'utc') + timeout * interval '1 second'
80
+ (now() at time zone 'utc') + timeout * interval '1 second',
81
+ p_cron_interval,
82
+ p_is_sticky
39
83
  ) RETURNING {SCHEMA_NAME}.postjobs.id INTO job_id;
40
84
 
41
85
  -- fill in root_id and full_id ------------------------------------
@@ -2,8 +2,10 @@ DROP FUNCTION IF EXISTS {SCHEMA_NAME}._set_job_processing(job_id BIGINT); -- re
2
2
  CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._set_job_processing(worker_session_id UUID, job_id BIGINT) RETURNS VOID AS $$
3
3
  DECLARE
4
4
  v_pid int;
5
+ v_host_id uuid;
5
6
  BEGIN
6
7
  v_pid := pg_backend_pid();
8
+ SELECT host_id INTO v_host_id FROM {SCHEMA_NAME}.worker_sessions WHERE id=worker_session_id;
7
9
 
8
10
  UPDATE {SCHEMA_NAME}.postjobs
9
11
  SET
@@ -12,8 +14,9 @@ BEGIN
12
14
  error=NULL,
13
15
  error_message=NULL,
14
16
  error_backtrace=NULL,
15
- next_run_at=NULL
16
- WHERE id=job_id;
17
+ next_run_at=NULL,
18
+ sticky_host_id=(CASE WHEN is_sticky THEN v_host_id ELSE NULL END)
19
+ WHERE id=job_id;
17
20
  END;
18
21
  $$ LANGUAGE plpgsql;
19
22
 
@@ -45,10 +45,12 @@ BEGIN
45
45
  END;
46
46
  $$ LANGUAGE plpgsql;
47
47
 
48
+ DROP FUNCTION IF EXISTS {SCHEMA_NAME}._prepare_next_run(BIGINT, {SCHEMA_NAME}.statuses, BOOLEAN);
49
+
48
50
  -- If this is a recoverable error and if we have another run possible
49
51
  -- we'll set next_run_at, and the status to "err", otherwise
50
52
  -- next_run_at will be NULL and the status would be "failed" or "timeout"
51
- CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._prepare_next_run(
53
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._prepare_rerun(
52
54
  job_id BIGINT,
53
55
  p_status {SCHEMA_NAME}.statuses,
54
56
  p_fast_mode BOOLEAN) RETURNS VOID AS $$
@@ -119,8 +121,7 @@ BEGIN
119
121
  next_run_at=NULL
120
122
  WHERE id=job_id;
121
123
 
122
- -- prepare next run, if any
123
- PERFORM {SCHEMA_NAME}._prepare_next_run(job_id, p_status, p_fast_mode);
124
+ PERFORM {SCHEMA_NAME}._prepare_rerun(job_id, p_status, p_fast_mode);
124
125
  PERFORM {SCHEMA_NAME}._wakeup_parent_job(p_worker_session_id, job_id);
125
126
  END;
126
127
  $$ LANGUAGE plpgsql;
@@ -144,7 +145,7 @@ BEGIN
144
145
  WHERE id=job_id;
145
146
 
146
147
  -- prepare next run, if any
147
- PERFORM {SCHEMA_NAME}._prepare_next_run(job_id, 'timeout', p_fast_mode);
148
+ PERFORM {SCHEMA_NAME}._prepare_rerun(job_id, 'timeout', p_fast_mode);
148
149
  PERFORM {SCHEMA_NAME}._wakeup_parent_job(p_worker_session_id, job_id);
149
150
  END;
150
151
  $$ LANGUAGE plpgsql;
@@ -170,7 +171,7 @@ BEGIN
170
171
  WHERE id=job_id;
171
172
 
172
173
  -- prepare next run, if any
173
- PERFORM {SCHEMA_NAME}._prepare_next_run(job_id, 'err', p_fast_mode);
174
+ PERFORM {SCHEMA_NAME}._prepare_rerun(job_id, 'err', p_fast_mode);
174
175
  PERFORM {SCHEMA_NAME}._wakeup_parent_job(p_worker_session_id, job_id);
175
176
  END;
176
177
  $$ LANGUAGE plpgsql;
@@ -49,47 +49,42 @@ BEGIN
49
49
  RAISE 'Invalid parent job id NULL';
50
50
  END IF;
51
51
 
52
- IF v_parent_id IS NOT NULL THEN
53
- SELECT INTO parent * FROM {SCHEMA_NAME}.postjobs WHERE id=v_parent_id;
54
- IF parent.id IS NULL THEN
55
- RAISE 'No such job: %', v_parent_id;
56
- END IF;
57
- END IF;
52
+ -- create a child job if it doesn't exist yet.
58
53
 
59
- -- check for existing child record
54
+ SELECT id INTO child_id FROM {SCHEMA_NAME}.postjobs
55
+ WHERE parent_id=v_parent_id
56
+ AND workflow=v_workflow
57
+ AND workflow_method=v_workflow_method
58
+ AND args=v_args
59
+ ;
60
60
 
61
- -- IF v_parent_id IS NOT NULL THEN
62
- SELECT id INTO child_id FROM {SCHEMA_NAME}.postjobs
63
- WHERE parent_id=v_parent_id
64
- AND workflow=v_workflow
65
- AND workflow_method=v_workflow_method
66
- AND args=v_args
67
- ;
61
+ IF child_id IS NULL THEN
62
+ SELECT * INTO parent
63
+ FROM {SCHEMA_NAME}.postjobs
64
+ WHERE postjobs.id=v_parent_id;
68
65
 
69
- IF child_id IS NOT NULL THEN
70
- -- note that RETURN QUERY does not return the function here. It 'only'
71
- -- adds the specified query to the result set.
72
- RETURN QUERY
73
- SELECT * FROM {SCHEMA_NAME}.postjobs WHERE id=child_id
74
- ;
75
- ELSE
76
- IF v_tags IS NOT NULL THEN
77
- RAISE WARNING 'Ignoring tags %', v_tags;
78
- END IF;
66
+ SELECT id INTO child_id FROM {SCHEMA_NAME}.enqueue(
67
+ p_worker_session_id,
68
+ COALESCE(v_queue, parent.queue), -- queue VARCHAR,
69
+ v_workflow, -- workflow VARCHAR,
70
+ v_workflow_method, -- workflow_method VARCHAR,
71
+ NULL, -- workflow_version VARCHAR,
72
+ v_args, -- args JSONB,
73
+ v_parent_id, -- parent_id BIGINT,
74
+ parent.tags, -- tags JSONB,
75
+ v_max_attempts, -- max_attempts INTEGER,
76
+ v_timeout, -- timeout
77
+ NULL, -- cron_interval,
78
+ parent.is_sticky -- is_sticky?
79
+ );
79
80
 
80
- RETURN QUERY
81
- SELECT * FROM {SCHEMA_NAME}.enqueue(
82
- p_worker_session_id,
83
- COALESCE(v_queue, parent.queue), -- queue VARCHAR,
84
- v_workflow, -- workflow VARCHAR,
85
- v_workflow_method, -- workflow_method VARCHAR,
86
- NULL, -- workflow_version VARCHAR,
87
- v_args, -- args JSONB,
88
- v_parent_id, -- parent_id BIGINT,
89
- parent.tags, -- tags JSONB,
90
- v_max_attempts, -- max_attempts INTEGER,
91
- v_timeout);
92
- END IF;
93
- -- END IF;
81
+ UPDATE {SCHEMA_NAME}.postjobs
82
+ SET sticky_host_id=parent.sticky_host_id
83
+ WHERE id=child_id;
84
+ END IF;
85
+
86
+ RETURN QUERY
87
+ SELECT * FROM {SCHEMA_NAME}.postjobs WHERE id=child_id
88
+ ;
94
89
  END;
95
90
  $$ LANGUAGE plpgsql;
@@ -27,5 +27,5 @@ $$ LANGUAGE plpgsql;
27
27
 
28
28
  -- define version settings ----------------------------------------------------
29
29
 
30
- SELECT {SCHEMA_NAME}.settings_set('version', '0.5.0');
30
+ SELECT {SCHEMA_NAME}.settings_set('version', '0.5.6');
31
31
  SELECT {SCHEMA_NAME}.settings_set('client_version', '{CLIENT_VERSION}');
@@ -4,9 +4,10 @@ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.time_to_next_job(p_worker_session_id UU
4
4
  AS $$
5
5
  DECLARE
6
6
  p_processable_at timestamp;
7
- p_workflows_with_version varchar[];
7
+ session {SCHEMA_NAME}.worker_sessions;
8
8
  BEGIN
9
- SELECT workflows INTO p_workflows_with_version FROM {SCHEMA_NAME}.worker_sessions WHERE id=p_worker_session_id;
9
+ SELECT * INTO session
10
+ FROM {SCHEMA_NAME}.worker_sessions WHERE id=p_worker_session_id;
10
11
 
11
12
  SELECT MIN(processable_at) INTO p_processable_at FROM (
12
13
  SELECT MIN(timing_out_at) AS processable_at
@@ -16,10 +17,11 @@ BEGIN
16
17
  SELECT MIN(next_run_at) AS processable_at
17
18
  FROM {SCHEMA_NAME}.postjobs
18
19
  WHERE status IN ('ready', 'err')
19
- AND workflow || workflow_version = ANY (p_workflows_with_version)
20
+ AND (workflow || workflow_version) = ANY (session.workflows)
21
+ AND COALESCE(sticky_host_id, {SCHEMA_NAME}._null_uuid()) IN (session.host_id, {SCHEMA_NAME}._null_uuid())
20
22
  ) sq;
21
23
 
22
- RETURN EXTRACT(EPOCH FROM p_processable_at - (now() at time zone 'utc'));
24
+ RETURN EXTRACT(EPOCH FROM p_processable_at - (now() at time zone 'utc'));
23
25
  END;
24
26
  $$ LANGUAGE plpgsql;
25
27
 
@@ -29,9 +31,10 @@ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}.checkout(p_worker_session_id UUID, p_fa
29
31
  AS $$
30
32
  DECLARE
31
33
  job {SCHEMA_NAME}.postjobs;
32
- p_workflows_with_version varchar[];
34
+ session {SCHEMA_NAME}.worker_sessions;
33
35
  BEGIN
34
- SELECT workflows INTO p_workflows_with_version FROM {SCHEMA_NAME}.worker_sessions WHERE id=p_worker_session_id;
36
+ SELECT * INTO session
37
+ FROM {SCHEMA_NAME}.worker_sessions WHERE id=p_worker_session_id;
35
38
 
36
39
  LOOP
37
40
  -- try to checkout a job. Each of the conditions here is matching
@@ -47,7 +50,8 @@ BEGIN
47
50
  (
48
51
  s.status IN ('ready', 'err')
49
52
  AND s.next_run_at <= (now() at time zone 'utc')
50
- AND (s.workflow || s.workflow_version) = ANY (p_workflows_with_version)
53
+ AND (s.workflow || s.workflow_version) = ANY (session.workflows)
54
+ AND COALESCE(s.sticky_host_id, {SCHEMA_NAME}._null_uuid()) IN (session.host_id, {SCHEMA_NAME}._null_uuid())
51
55
  )
52
56
  ORDER BY (LEAST(s.next_run_at, s.timing_out_at))
53
57
  FOR UPDATE SKIP LOCKED
@@ -15,6 +15,12 @@ DECLARE
15
15
  BEGIN
16
16
  zombie_count := 0;
17
17
  FOR zombie_id, _one IN
18
+ -- select jobs that have a last_worker_session_id, which points to a
19
+ -- host whose latest heartbeat is older than +zombie_threshold+.
20
+ --
21
+ -- note that we ignore hosts (and, for that matter, jobs) that don't
22
+ -- have any heartbeats, since this scenario should only appear during
23
+ -- tests.
18
24
  SELECT jobs.id, 1
19
25
  FROM {SCHEMA_NAME}.postjobs jobs
20
26
  LEFT JOIN {SCHEMA_NAME}.worker_sessions sessions ON jobs.last_worker_session_id=sessions.id
@@ -27,10 +33,7 @@ BEGIN
27
33
  ) heartbeat ON sessions.host_id = heartbeat.host_id
28
34
  WHERE
29
35
  jobs.status IN ('processing')
30
- AND (
31
- heartbeat.created_at IS NULL OR
32
- heartbeat.created_at < ((now() at time zone 'utc') - zombie_threshold)
33
- )
36
+ AND heartbeat.created_at < ((now() at time zone 'utc') - zombie_threshold)
34
37
  LOOP
35
38
  PERFORM {SCHEMA_NAME}._set_job_zombie(zombie_id, p_fast_mode);
36
39
  zombie_count := zombie_count + 1;
@@ -51,8 +54,10 @@ BEGIN
51
54
  -- event, which has a zombie count value in its attributes.
52
55
  IF NOT EXISTS (SELECT 1 FROM {SCHEMA_NAME}.events WHERE name='zombie' AND created_at > (now() at time zone 'utc') - zombie_check_interval) THEN
53
56
  p_zombie_count := {SCHEMA_NAME}._zombie_check(p_fast_mode);
54
- INSERT INTO {SCHEMA_NAME}.events(name, host_id, attributes)
55
- VALUES('zombie', {SCHEMA_NAME}._null_uuid(), jsonb_build_object('zombie_count', p_zombie_count));
57
+ IF p_zombie_count > 0 THEN
58
+ INSERT INTO {SCHEMA_NAME}.events(name, host_id, attributes)
59
+ VALUES('zombie', {SCHEMA_NAME}._null_uuid(), jsonb_build_object('zombie_count', p_zombie_count));
60
+ END IF;
56
61
  END IF;
57
62
  END;
58
63
  $$ LANGUAGE plpgsql;
@@ -0,0 +1,65 @@
1
+ DO $$
2
+ BEGIN
3
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN cron_interval integer;
4
+ EXCEPTION
5
+ WHEN duplicate_column THEN RAISE DEBUG 'column {SCHEMA_NAME}.postjobs.cron_interval already exists';
6
+ END;
7
+ $$;
8
+
9
+ --- define triggers to automatically restart cron jobs ------------------------
10
+
11
+ -- This method is called whenever a job's status changes. It enqueues a fresh
12
+
13
+ --
14
+ -- An event is created whenever a job's status changes.
15
+ --
16
+
17
+ CREATE OR REPLACE FUNCTION {SCHEMA_NAME}._restart_cron_job()
18
+ RETURNS TRIGGER
19
+ AS $$
20
+ DECLARE
21
+ p_new_jonb_id BIGINT;
22
+ BEGIN
23
+ IF NEW.status = OLD.status THEN
24
+ RETURN NEW;
25
+ END IF;
26
+
27
+ IF TG_OP != 'UPDATE' OR NEW.status NOT IN ('failed', 'ok') THEN
28
+ RETURN NEW;
29
+ END IF;
30
+
31
+ IF NEW.cron_interval IS NULL THEN
32
+ RETURN NEW;
33
+ END IF;
34
+
35
+ SELECT id INTO p_new_jonb_id FROM {SCHEMA_NAME}.enqueue(
36
+ NEW.last_worker_session_id, -- p_worker_session_id
37
+ NEW.queue, -- queue
38
+ NEW.workflow, -- workflow
39
+ NEW.workflow_method, -- workflow_method
40
+ NULL, -- workflow_version
41
+ NEW.args, -- args
42
+ NULL, -- parent_id
43
+ NEW.tags, -- tags
44
+ NEW.max_attempts, -- max_attempts
45
+ (EXTRACT(EPOCH FROM NEW.timing_out_at) - EXTRACT(EPOCH FROM NEW.created_at)), -- timeout
46
+ NEW.cron_interval,
47
+ NEW.is_sticky
48
+ );
49
+
50
+ UPDATE {SCHEMA_NAME}.postjobs
51
+ SET next_run_at = now() at time zone 'utc' + NEW.cron_interval * interval '1 second'
52
+ WHERE id=p_new_jonb_id;
53
+
54
+ RETURN NEW;
55
+ END;
56
+ $$ LANGUAGE plpgsql;
57
+
58
+ BEGIN;
59
+ DROP TRIGGER IF EXISTS _restart_cron_job ON {SCHEMA_NAME}.postjobs;
60
+
61
+ CREATE TRIGGER _restart_cron_job AFTER UPDATE
62
+ ON {SCHEMA_NAME}.postjobs
63
+ FOR EACH ROW
64
+ EXECUTE PROCEDURE {SCHEMA_NAME}._restart_cron_job();
65
+ COMMIT;
@@ -0,0 +1,16 @@
1
+ DO $$
2
+ BEGIN
3
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN is_sticky BOOLEAN NOT NULL DEFAULT FALSE;
4
+ EXCEPTION
5
+ WHEN duplicate_column THEN RAISE DEBUG 'column {SCHEMA_NAME}.postjobs.is_sticky already exists';
6
+ END;
7
+ $$;
8
+
9
+ DO $$
10
+ BEGIN
11
+ ALTER TABLE {SCHEMA_NAME}.postjobs ADD COLUMN sticky_host_id UUID
12
+ REFERENCES {SCHEMA_NAME}.hosts;
13
+ EXCEPTION
14
+ WHEN duplicate_column THEN RAISE DEBUG 'column {SCHEMA_NAME}.postjobs.sticky_host_id already exists';
15
+ END;
16
+ $$;
@@ -16,6 +16,8 @@ module Postjob
16
16
  expect! SCHEMA_NAME != "public"
17
17
 
18
18
  def unmigrate!
19
+ ::Postjob::Host.clear_storage
20
+
19
21
  SQL.exec "SET client_min_messages TO WARNING"
20
22
  SQL.exec "DROP SCHEMA IF EXISTS #{SCHEMA_NAME} CASCADE"
21
23
  end
@@ -45,7 +47,7 @@ module Postjob
45
47
 
46
48
  def run_migration_sql(file)
47
49
  sql = File.read(file)
48
- sql.gsub!(/\{([^\}]+)\}/) { |_| const_get($1) }
50
+ sql.gsub!(/\{([^\}]+)\}/) { |_| const_get(Regexp.last_match(1)) }
49
51
  SQL.exec sql
50
52
  end
51
53
  end