que 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 967eded27a81df945b65911275dcdc7925113727bfd06fbc42cb780a3bfb6fcf
4
- data.tar.gz: 87607b262263b13623b4b09822a478c02179e57cfa503f253e3e678ac8b1490a
3
+ metadata.gz: 382e70b455239bf58eda4d83e5772400b55055f98857d2b83fdfc7f36f26d856
4
+ data.tar.gz: dfad699d5bb996c965e3216de224fc9248a3cb2be70cce04f8407ed9b5ee0651
5
5
  SHA512:
6
- metadata.gz: 3d8242fe07445a6f4658f322dd1a7cdb8918c8a56c2bfad2db49c58695b4ebae183e6a56159abc4ff74316006e7d95459ab6cfd8a4c98410eea50a2e4fb52509
7
- data.tar.gz: 24fdf63cab7ff6e8a2ffa6ed598718d3c2ae2cbc6ef61de22ab6f1541b0cc23bab980fe5d10bbcb54823b2a529cd1cdc9d57437db3905e802bec92a04ea3ed94
6
+ metadata.gz: d1c34fc4b2ff8ce02b9b70f8fe9e9d361c8e4e701e36987690464a5f62263905e29453c9dbcb65bce9bda19d511d1430b1e235f1ad8e86cbd1e136e189e71015
7
+ data.tar.gz: 9cb2182726f813fb53c3cb6f2a41dc4bb482810448673a8f8e704860db119e1ad44a64613f3d688df1085f0686ff51ea74a0d1b46bc7353d79d81945cf3555e8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ### 1.3.0 (2022-02-25)
2
+
3
+ **ACTION REQUIRED**
4
+
5
+ This release will allow you to safely upgrade to Que 2 when it comes out, without first needing to empty your `que_jobs` table.
6
+
7
+ **You will need to first update to this version, apply the Que schema migration, and deploy, before you can safely begin the process of upgrading to Que 2.**
8
+
9
+ Que 2 will bring Ruby 3 support, but to do that, the job arguments in the `que_jobs` table will need to be split into two columns - repurposing the existing one for positional arguments only (`args`), and adding a new one for keyword arguments (`kwargs`). This is so that Que running in Ruby 3, when reading job arguments stored in the database, can disambiguate between keyword arguments and a last positional argument hash.
10
+
11
+ The args split hasn't happened yet, but when it does, we still need to be able to successfully process all the existing queued jobs which have their keyword arguments in the `args` column still. Our solution is for you to have both Que 1 workers and Que 2 workers operating simultaneously during the upgrade, each processing only the jobs enqueued from that version. Once all the Que 1 jobs are processed, the Que 1 workers can be retired.
12
+
13
+ To allow the different worker versions to tell which jobs belong to which, we've added a new column to the `que_jobs` table in this version, `job_schema_version`. Jobs enqueued with Que 1 will have a `1` here, and jobs from Que 2 will have a `2`. Que schema migration 5 will default the job schema version of all existing jobs to `1`.
14
+
15
+ You will need to migrate Que to the latest Que schema version (5). For instance, on ActiveRecord and Rails 6:
16
+
17
+ ```ruby
18
+ class UpdateQueTablesToVersion5 < ActiveRecord::Migration[6.0]
19
+ def up
20
+ Que.migrate!(version: 5)
21
+ end
22
+ def down
23
+ Que.migrate!(version: 4)
24
+ end
25
+ end
26
+ ```
27
+
28
+ You must apply the schema migration and deploy to upgrade all workers.
29
+
30
+ No further action is required from you at this stage. The Que 2 release changelog will provide full upgrade instructions for the process briefly described above of simultaneously running both Que 1 & 2 workers. Note that you won't be required to upgrade from Ruby 2.7 to Ruby 3 at the same time as upgrading to Que 2.
31
+
32
+ If you use any Que plugins or custom code that interacts with the `que_jobs` table, before you can upgrade to Que 2, you will need to make sure they are updated for compatibility with Que 2: They'll need to make use of the `kwargs` column, and when inserting jobs, put the result of `Que.job_schema_version` into the `job_schema_version` column rather than continue to rely on its default of `1`.
33
+
1
34
  ### 1.2.0 (2022-02-23)
2
35
 
3
36
  - **Deprecated**
data/README.md CHANGED
@@ -59,7 +59,7 @@ class CreateQueSchema < ActiveRecord::Migration[5.0]
59
59
  # Whenever you use Que in a migration, always specify the version you're
60
60
  # migrating to. If you're unsure what the current version is, check the
61
61
  # changelog.
62
- Que.migrate!(version: 4)
62
+ Que.migrate!(version: 5)
63
63
  end
64
64
 
65
65
  def down
@@ -253,3 +253,29 @@ If you want to try a different version of Postgres, e.g. 12:
253
253
  ```bash
254
254
  export POSTGRES_VERSION=12
255
255
  ```
256
+
257
+ ### Git pre-push hook
258
+
259
+ So we can avoid breaking the build, we've created Git pre-push hooks to verify everything is ok before pushing.
260
+
261
+ To set up the pre-push hook locally, run:
262
+
263
+ ```bash
264
+ echo -e "#\!/bin/bash\n\$(dirname \$0)/../../auto/pre-push-hook" > .git/hooks/pre-push
265
+ chmod +x .git/hooks/pre-push
266
+ ```
267
+
268
+ ### Release process
269
+
270
+ The process for releasing a new version of the gem is:
271
+
272
+ - Merge PR(s)
273
+ - Git pull locally
274
+ - Update the version number, bundle install, and commit
275
+ - Update `CHANGELOG.md`, and commit
276
+ - Tag the commit with the version number, prefixed by `v`
277
+ - Git push to master
278
+ - Git push the tag
279
+ - Publish the new version of the gem to RubyGems: `gem build -o que.gem && gem push que.gem`
280
+ - Create a GitHub release - rather than describe anything there, link to the heading for the release in `CHANGELOG.md`
281
+ - Post on the Que Discord in `#announcements`
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+
3
+ set -Eeuo pipefail
4
+
5
+ cd "$(dirname "$0")/.."
6
+
7
+ green='\e[32m'; blue='\e[36m'; red='\e[31m'; bold='\e[1m'; reset='\e[0m'
8
+ coloured-arrow() { printf "$bold$1==> $2$reset\n"; }
9
+ success() { coloured-arrow "$green" "$1"; }
10
+ info() { coloured-arrow "$blue" "$1"; }
11
+ err() { coloured-arrow "$red" "$1"; exit 1; }
12
+
13
+ info 'Running pre-push hook...'
14
+
15
+ on-exit() {
16
+ [[ -n "${succeeded-}" ]] || err 'Pre-push checks failed'
17
+ }
18
+ trap on-exit EXIT
19
+
20
+ info 'Checking for uncommitted changes...'
21
+ [[ -z $(git status -s) ]] || err 'ERROR: You have uncommited changes'
22
+
23
+ info 'Checking bundle...'
24
+ bundle check --dry-run || bundle install
25
+
26
+ info 'Specs...'
27
+ auto/test
28
+
29
+ succeeded=true
30
+ success 'All pre-push checks passed! =)'
@@ -232,7 +232,16 @@ OUTPUT
232
232
  $stop_que_executable = false
233
233
  %w[INT TERM].each { |signal| trap(signal) { $stop_que_executable = true } }
234
234
 
235
- output.puts "Que waiting for jobs..."
235
+ output.puts(
236
+ <<~STARTUP
237
+ Que #{Que::VERSION} started worker process with:
238
+ Worker threads: #{locker.workers.length} (priorities: #{locker.workers.map { |w| w.priority || 'any' }.join(', ')})
239
+ Buffer size: #{locker.job_buffer.minimum_size}-#{locker.job_buffer.maximum_size}
240
+ Queues:
241
+ #{locker.queues.map { |queue, interval| " - #{queue} (poll interval: #{interval}s)" }.join("\n")}
242
+ Que waiting for jobs...
243
+ STARTUP
244
+ )
236
245
 
237
246
  loop do
238
247
  sleep 0.01
data/docs/README.md CHANGED
@@ -129,11 +129,11 @@ There are other docs to read if you're using [Sequel](#using-sequel) or [plain P
129
129
  After you've connected Que to the database, you can manage the jobs table. You'll want to migrate to a specific version in a migration file, to ensure that they work the same way even when you upgrade Que in the future:
130
130
 
131
131
  ```ruby
132
- # Update the schema to version #4.
133
- Que.migrate! version: 4
132
+ # Update the schema to version #5.
133
+ Que.migrate!(version: 5)
134
134
 
135
135
  # Remove Que's jobs table entirely.
136
- Que.migrate! version: 0
136
+ Que.migrate!(version: 0)
137
137
  ```
138
138
 
139
139
  There's also a helper method to clear all jobs from the jobs table:
@@ -405,11 +405,11 @@ Some new releases of Que may require updates to the database schema. It's recomm
405
405
  ```ruby
406
406
  class UpdateQue < ActiveRecord::Migration[5.0]
407
407
  def self.up
408
- Que.migrate! version: 3
408
+ Que.migrate!(version: 3)
409
409
  end
410
410
 
411
411
  def self.down
412
- Que.migrate! version: 2
412
+ Que.migrate!(version: 2)
413
413
  end
414
414
  end
415
415
  ```
@@ -418,7 +418,7 @@ This will make sure that your database schema stays consistent with your codebas
418
418
 
419
419
  ```ruby
420
420
  # Change schema to version 3.
421
- Que.migrate! version: 3
421
+ Que.migrate!(version: 3)
422
422
 
423
423
  # Check your current schema version.
424
424
  Que.db_version #=> 3
@@ -550,11 +550,11 @@ require 'que'
550
550
  Sequel.migration do
551
551
  up do
552
552
  Que.connection = self
553
- Que.migrate! :version => 3
553
+ Que.migrate!(version: 5)
554
554
  end
555
555
  down do
556
556
  Que.connection = self
557
- Que.migrate! :version => 0
557
+ Que.migrate!(version: 0)
558
558
  end
559
559
  end
560
560
  ```
data/lib/que/job.rb CHANGED
@@ -12,7 +12,7 @@ module Que
12
12
  SQL[:insert_job] =
13
13
  %{
14
14
  INSERT INTO public.que_jobs
15
- (queue, priority, run_at, job_class, args, data)
15
+ (queue, priority, run_at, job_class, args, data, job_schema_version)
16
16
  VALUES
17
17
  (
18
18
  coalesce($1, 'default')::text,
@@ -20,7 +20,8 @@ module Que
20
20
  coalesce($3, now())::timestamptz,
21
21
  $4::text,
22
22
  coalesce($5, '[]')::jsonb,
23
- coalesce($6, '{}')::jsonb
23
+ coalesce($6, '{}')::jsonb,
24
+ #{Que.job_schema_version}
24
25
  )
25
26
  RETURNING *
26
27
  }
data/lib/que/locker.rb CHANGED
@@ -24,12 +24,12 @@ module Que
24
24
 
25
25
  SQL[:register_locker] =
26
26
  %{
27
- INSERT INTO public.que_lockers (pid, worker_count, worker_priorities, ruby_pid, ruby_hostname, listening, queues)
28
- VALUES (pg_backend_pid(), $1::integer, $2::integer[], $3::integer, $4::text, $5::boolean, $6::text[])
27
+ INSERT INTO public.que_lockers (pid, worker_count, worker_priorities, ruby_pid, ruby_hostname, listening, queues, job_schema_version)
28
+ VALUES (pg_backend_pid(), $1::integer, $2::integer[], $3::integer, $4::text, $5::boolean, $6::text[], $7::integer)
29
29
  }
30
30
 
31
31
  class Locker
32
- attr_reader :thread, :workers, :job_buffer, :locks
32
+ attr_reader :thread, :workers, :job_buffer, :locks, :queues, :poll_interval
33
33
 
34
34
  MESSAGE_RESOLVERS = {}
35
35
  RESULT_RESOLVERS = {}
@@ -101,7 +101,20 @@ module Que
101
101
  # Local cache of which advisory locks are held by this connection.
102
102
  @locks = Set.new
103
103
 
104
- @queue_names = queues.is_a?(Hash) ? queues.keys : queues
104
+ @poll_interval = poll_interval
105
+
106
+ if queues.is_a?(Hash)
107
+ @queue_names = queues.keys
108
+ @queues = queues.transform_values do |interval|
109
+ interval || poll_interval
110
+ end
111
+ else
112
+ @queue_names = queues
113
+ @queues = queues.map do |queue_name|
114
+ [queue_name, poll_interval]
115
+ end.to_h
116
+ end
117
+
105
118
  @wait_period = wait_period.to_f / 1000 # Milliseconds to seconds.
106
119
 
107
120
  @workers =
@@ -183,11 +196,11 @@ module Que
183
196
 
184
197
  @pollers =
185
198
  if poll
186
- queues.map do |queue, interval|
199
+ @queues.map do |queue_name, interval|
187
200
  Poller.new(
188
201
  connection: @connection,
189
- queue: queue,
190
- poll_interval: interval || poll_interval,
202
+ queue: queue_name,
203
+ poll_interval: interval,
191
204
  )
192
205
  end
193
206
  end
@@ -266,6 +279,7 @@ module Que
266
279
  CURRENT_HOSTNAME,
267
280
  !!@listener,
268
281
  "{\"#{@queue_names.join('","')}\"}",
282
+ Que.job_schema_version,
269
283
  ]
270
284
  end
271
285
 
@@ -146,7 +146,9 @@ CREATE FUNCTION que_job_notify() RETURNS trigger AS $$
146
146
  FROM (
147
147
  SELECT *
148
148
  FROM public.que_lockers ql, generate_series(1, ql.worker_count) AS id
149
- WHERE listening AND queues @> ARRAY[NEW.queue]
149
+ WHERE
150
+ listening AND
151
+ queues @> ARRAY[NEW.queue]
150
152
  ORDER BY md5(pid::text || id::text)
151
153
  ) t1
152
154
  ) t2
@@ -0,0 +1,73 @@
1
+ DROP TRIGGER que_job_notify ON que_jobs;
2
+ DROP FUNCTION que_job_notify();
3
+
4
+ DROP INDEX que_poll_idx_with_job_schema_version;
5
+
6
+ ALTER TABLE que_jobs
7
+ DROP COLUMN job_schema_version;
8
+
9
+ ALTER TABLE que_lockers
10
+ DROP COLUMN job_schema_version;
11
+
12
+ CREATE FUNCTION que_job_notify() RETURNS trigger AS $$
13
+ DECLARE
14
+ locker_pid integer;
15
+ sort_key json;
16
+ BEGIN
17
+ -- Don't do anything if the job is scheduled for a future time.
18
+ IF NEW.run_at IS NOT NULL AND NEW.run_at > now() THEN
19
+ RETURN null;
20
+ END IF;
21
+
22
+ -- Pick a locker to notify of the job's insertion, weighted by their number
23
+ -- of workers. Should bounce pseudorandomly between lockers on each
24
+ -- invocation, hence the md5-ordering, but still touch each one equally,
25
+ -- hence the modulo using the job_id.
26
+ SELECT pid
27
+ INTO locker_pid
28
+ FROM (
29
+ SELECT *, last_value(row_number) OVER () + 1 AS count
30
+ FROM (
31
+ SELECT *, row_number() OVER () - 1 AS row_number
32
+ FROM (
33
+ SELECT *
34
+ FROM public.que_lockers ql, generate_series(1, ql.worker_count) AS id
35
+ WHERE
36
+ listening AND
37
+ queues @> ARRAY[NEW.queue]
38
+ ORDER BY md5(pid::text || id::text)
39
+ ) t1
40
+ ) t2
41
+ ) t3
42
+ WHERE NEW.id % count = row_number;
43
+
44
+ IF locker_pid IS NOT NULL THEN
45
+ -- There's a size limit to what can be broadcast via LISTEN/NOTIFY, so
46
+ -- rather than throw errors when someone enqueues a big job, just
47
+ -- broadcast the most pertinent information, and let the locker query for
48
+ -- the record after it's taken the lock. The worker will have to hit the
49
+ -- DB in order to make sure the job is still visible anyway.
50
+ SELECT row_to_json(t)
51
+ INTO sort_key
52
+ FROM (
53
+ SELECT
54
+ 'job_available' AS message_type,
55
+ NEW.queue AS queue,
56
+ NEW.priority AS priority,
57
+ NEW.id AS id,
58
+ -- Make sure we output timestamps as UTC ISO 8601
59
+ to_char(NEW.run_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS run_at
60
+ ) t;
61
+
62
+ PERFORM pg_notify('que_listener_' || locker_pid::text, sort_key::text);
63
+ END IF;
64
+
65
+ RETURN null;
66
+ END
67
+ $$
68
+ LANGUAGE plpgsql;
69
+
70
+ CREATE TRIGGER que_job_notify
71
+ AFTER INSERT ON que_jobs
72
+ FOR EACH ROW
73
+ EXECUTE PROCEDURE public.que_job_notify();
@@ -0,0 +1,76 @@
1
+ DROP TRIGGER que_job_notify ON que_jobs;
2
+ DROP FUNCTION que_job_notify();
3
+
4
+ ALTER TABLE que_jobs
5
+ ADD COLUMN job_schema_version INTEGER DEFAULT 1;
6
+
7
+ ALTER TABLE que_lockers
8
+ ADD COLUMN job_schema_version INTEGER DEFAULT 1;
9
+
10
+ CREATE INDEX que_poll_idx_with_job_schema_version
11
+ ON que_jobs (job_schema_version, queue, priority, run_at, id)
12
+ WHERE (finished_at IS NULL AND expired_at IS NULL);
13
+
14
+ CREATE FUNCTION que_job_notify() RETURNS trigger AS $$
15
+ DECLARE
16
+ locker_pid integer;
17
+ sort_key json;
18
+ BEGIN
19
+ -- Don't do anything if the job is scheduled for a future time.
20
+ IF NEW.run_at IS NOT NULL AND NEW.run_at > now() THEN
21
+ RETURN null;
22
+ END IF;
23
+
24
+ -- Pick a locker to notify of the job's insertion, weighted by their number
25
+ -- of workers. Should bounce pseudorandomly between lockers on each
26
+ -- invocation, hence the md5-ordering, but still touch each one equally,
27
+ -- hence the modulo using the job_id.
28
+ SELECT pid
29
+ INTO locker_pid
30
+ FROM (
31
+ SELECT *, last_value(row_number) OVER () + 1 AS count
32
+ FROM (
33
+ SELECT *, row_number() OVER () - 1 AS row_number
34
+ FROM (
35
+ SELECT *
36
+ FROM public.que_lockers ql, generate_series(1, ql.worker_count) AS id
37
+ WHERE
38
+ listening AND
39
+ queues @> ARRAY[NEW.queue] AND
40
+ ql.job_schema_version = NEW.job_schema_version
41
+ ORDER BY md5(pid::text || id::text)
42
+ ) t1
43
+ ) t2
44
+ ) t3
45
+ WHERE NEW.id % count = row_number;
46
+
47
+ IF locker_pid IS NOT NULL THEN
48
+ -- There's a size limit to what can be broadcast via LISTEN/NOTIFY, so
49
+ -- rather than throw errors when someone enqueues a big job, just
50
+ -- broadcast the most pertinent information, and let the locker query for
51
+ -- the record after it's taken the lock. The worker will have to hit the
52
+ -- DB in order to make sure the job is still visible anyway.
53
+ SELECT row_to_json(t)
54
+ INTO sort_key
55
+ FROM (
56
+ SELECT
57
+ 'job_available' AS message_type,
58
+ NEW.queue AS queue,
59
+ NEW.priority AS priority,
60
+ NEW.id AS id,
61
+ -- Make sure we output timestamps as UTC ISO 8601
62
+ to_char(NEW.run_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS run_at
63
+ ) t;
64
+
65
+ PERFORM pg_notify('que_listener_' || locker_pid::text, sort_key::text);
66
+ END IF;
67
+
68
+ RETURN null;
69
+ END
70
+ $$
71
+ LANGUAGE plpgsql;
72
+
73
+ CREATE TRIGGER que_job_notify
74
+ AFTER INSERT ON que_jobs
75
+ FOR EACH ROW
76
+ EXECUTE PROCEDURE public.que_job_notify();
@@ -4,7 +4,7 @@ module Que
4
4
  module Migrations
5
5
  # In order to ship a schema change, add the relevant up and down sql files
6
6
  # to the migrations directory, and bump the version here.
7
- CURRENT_VERSION = 4
7
+ CURRENT_VERSION = 5
8
8
 
9
9
  class << self
10
10
  def migrate!(version:)
@@ -28,7 +28,6 @@ module Que
28
28
  step,
29
29
  direction,
30
30
  ].join('/') << '.sql'
31
-
32
31
  Que.execute(File.read(filename))
33
32
  end
34
33
 
data/lib/que/poller.rb CHANGED
@@ -68,6 +68,7 @@ module Que
68
68
  SELECT j
69
69
  FROM public.que_jobs AS j
70
70
  WHERE queue = $1::text
71
+ AND job_schema_version = #{Que.job_schema_version}
71
72
  AND NOT id = ANY($2::bigint[])
72
73
  AND priority <= pg_temp.que_highest_remaining_priority($3::jsonb)
73
74
  AND run_at <= now()
@@ -88,6 +89,7 @@ module Que
88
89
  SELECT j
89
90
  FROM public.que_jobs AS j
90
91
  WHERE queue = $1::text
92
+ AND job_schema_version = #{Que.job_schema_version}
91
93
  AND NOT id = ANY($2::bigint[])
92
94
  AND priority <= pg_temp.que_highest_remaining_priority(jobs.remaining_priorities)
93
95
  AND run_at <= now()
data/lib/que/version.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Que
4
- VERSION = '1.2.0'
4
+ VERSION = '1.3.0'
5
+
6
+ def self.job_schema_version
7
+ 1
8
+ end
5
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: que
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Hanks
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-23 00:00:00.000000000 Z
11
+ date: 2022-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -40,6 +40,7 @@ files:
40
40
  - README.md
41
41
  - Rakefile
42
42
  - auto/dev
43
+ - auto/pre-push-hook
43
44
  - auto/psql
44
45
  - auto/test
45
46
  - auto/test-postgres-14
@@ -68,6 +69,8 @@ files:
68
69
  - lib/que/migrations/3/up.sql
69
70
  - lib/que/migrations/4/down.sql
70
71
  - lib/que/migrations/4/up.sql
72
+ - lib/que/migrations/5/down.sql
73
+ - lib/que/migrations/5/up.sql
71
74
  - lib/que/poller.rb
72
75
  - lib/que/rails/railtie.rb
73
76
  - lib/que/result_queue.rb
@@ -106,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
109
  - !ruby/object:Gem::Version
107
110
  version: '0'
108
111
  requirements: []
109
- rubygems_version: 3.1.6
112
+ rubygems_version: 3.3.6
110
113
  signing_key:
111
114
  specification_version: 4
112
115
  summary: A PostgreSQL-based Job Queue