switchman-inst-jobs 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e76d31061b35e05fb85de802c868f5244c386f39490b236b7e2fa48160c1b4d
4
- data.tar.gz: 81d2c12f67012e962b7d45157f5df7f045a3f4f0c45df2c9294661125524f234
3
+ metadata.gz: e6f9f61d51da46ec92185649cc434d9168740c491ade56179d13537d9552fa8b
4
+ data.tar.gz: 5c53dbe8087c254444f995ee738cb4164db03fc8a91ae823938a13b372e3eb0d
5
5
  SHA512:
6
- metadata.gz: cb73f1c14a052801826a5f95cdfda52ecd73703cdc1b82b320a4b4ba69ba5ef2b4f77f01d41b1bec667df5b16a02529d8297f7de5726a392e65e086e0ad81570
7
- data.tar.gz: 6b806f4e8e1e74e589090f75e54de9f12fe3e6ac5f97fc08296adc1c998055a082f7f815d0f7a6362d4745770e7791db100f640e6dadba5c8196ed0e940109b4
6
+ metadata.gz: 44c3483d276b435c93abc3547b458cde60739b9a4843465541065ae5d71eb8dcee9d2c4ebebf8b1fe4fc31fd6c7951f983d8af76c44a340e7e29ce811466778f
7
+ data.tar.gz: 0acdee9429002d28e9368bf29fce1b64fa492132f43b8ff884bca36f5b5aded965343a621c2e61b07de7bd18dcc059b5e11d1a8571a8319a0582d9dc8e1fa206
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddNStrandIndex < ActiveRecord::Migration[5.2]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ add_index :delayed_jobs, %i[strand next_in_strand id],
8
+ name: 'n_strand_index',
9
+ where: 'strand IS NOT NULL',
10
+ algorithm: :concurrently
11
+ end
12
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddSingletonColumn < ActiveRecord::Migration[5.2]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ add_column :delayed_jobs, :singleton, :string, if_not_exists: true
8
+ add_column :failed_jobs, :singleton, :string, if_not_exists: true
9
+ # only one job can be queued in a singleton
10
+ add_index :delayed_jobs,
11
+ :singleton,
12
+ where: 'singleton IS NOT NULL AND locked_by IS NULL',
13
+ unique: true,
14
+ name: 'index_delayed_jobs_on_singleton_not_running',
15
+ algorithm: :concurrently
16
+ # only one job can be running for a singleton
17
+ add_index :delayed_jobs,
18
+ :singleton,
19
+ where: 'singleton IS NOT NULL AND locked_by IS NOT NULL',
20
+ unique: true,
21
+ name: 'index_delayed_jobs_on_singleton_running',
22
+ algorithm: :concurrently
23
+
24
+ reversible do |direction|
25
+ direction.up do
26
+ execute(<<~SQL)
27
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn')} () RETURNS trigger AS $$
28
+ DECLARE
29
+ running_count integer;
30
+ should_lock boolean;
31
+ should_be_precise boolean;
32
+ update_query varchar;
33
+ skip_locked varchar;
34
+ BEGIN
35
+ IF OLD.strand IS NOT NULL THEN
36
+ should_lock := true;
37
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
38
+
39
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
40
+ running_count := (SELECT COUNT(*) FROM (
41
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
42
+ ) subquery_for_count);
43
+ should_lock := running_count < OLD.max_concurrent;
44
+ END IF;
45
+
46
+ IF should_lock THEN
47
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
48
+ END IF;
49
+
50
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
51
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
52
+ -- singleton we grab isn't already running (which is a simple existence check, since
53
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
54
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
55
+ SELECT id FROM delayed_jobs j2
56
+ WHERE next_in_strand=false AND
57
+ j2.strand=$1.strand AND
58
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id))
59
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
60
+ LIMIT ';
61
+
62
+ IF should_be_precise THEN
63
+ running_count := (SELECT COUNT(*) FROM (
64
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
65
+ ) s);
66
+ IF running_count < OLD.max_concurrent THEN
67
+ update_query := update_query || '($1.max_concurrent - $2)';
68
+ ELSE
69
+ -- we have too many running already; just bail
70
+ RETURN OLD;
71
+ END IF;
72
+ ELSE
73
+ update_query := update_query || '1';
74
+
75
+ -- n-strands don't require precise ordering; we can make this query more performant
76
+ IF OLD.max_concurrent > 1 THEN
77
+ skip_locked := ' SKIP LOCKED';
78
+ END IF;
79
+ END IF;
80
+
81
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
82
+ EXECUTE update_query USING OLD, running_count;
83
+ ELSIF OLD.singleton IS NOT NULL THEN
84
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE singleton=OLD.singleton AND next_in_strand=false;
85
+ END IF;
86
+ RETURN OLD;
87
+ END;
88
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
89
+ SQL
90
+ execute(<<~SQL)
91
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_before_insert_row_tr_fn')} () RETURNS trigger AS $$
92
+ BEGIN
93
+ IF NEW.strand IS NOT NULL THEN
94
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
95
+ IF (SELECT COUNT(*) FROM (
96
+ SELECT 1 FROM delayed_jobs WHERE strand = NEW.strand AND next_in_strand=true LIMIT NEW.max_concurrent
97
+ ) s) = NEW.max_concurrent THEN
98
+ NEW.next_in_strand := false;
99
+ END IF;
100
+ END IF;
101
+ IF NEW.singleton IS NOT NULL THEN
102
+ PERFORM 1 FROM delayed_jobs WHERE singleton = NEW.singleton;
103
+ IF FOUND THEN
104
+ NEW.next_in_strand := false;
105
+ END IF;
106
+ END IF;
107
+ RETURN NEW;
108
+ END;
109
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
110
+ SQL
111
+ end
112
+ direction.down do
113
+ execute(<<~SQL)
114
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn')} () RETURNS trigger AS $$
115
+ DECLARE
116
+ running_count integer;
117
+ should_lock boolean;
118
+ should_be_precise boolean;
119
+ BEGIN
120
+ IF OLD.strand IS NOT NULL THEN
121
+ should_lock := true;
122
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
123
+
124
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
125
+ running_count := (SELECT COUNT(*) FROM (
126
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
127
+ ) subquery_for_count);
128
+ should_lock := running_count < OLD.max_concurrent;
129
+ END IF;
130
+
131
+ IF should_lock THEN
132
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
133
+ END IF;
134
+
135
+ IF should_be_precise THEN
136
+ running_count := (SELECT COUNT(*) FROM (
137
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
138
+ ) subquery_for_count);
139
+ IF running_count < OLD.max_concurrent THEN
140
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id IN (
141
+ SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
142
+ j2.strand = OLD.strand ORDER BY j2.strand_order_override ASC, j2.id ASC LIMIT (OLD.max_concurrent - running_count) FOR UPDATE
143
+ );
144
+ END IF;
145
+ ELSE
146
+ -- n-strands don't require precise ordering; we can make this query more performant
147
+ IF OLD.max_concurrent > 1 THEN
148
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
149
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
150
+ j2.strand = OLD.strand ORDER BY j2.strand_order_override ASC, j2.id ASC LIMIT 1 FOR UPDATE SKIP LOCKED);
151
+ ELSE
152
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
153
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
154
+ j2.strand = OLD.strand ORDER BY j2.strand_order_override ASC, j2.id ASC LIMIT 1 FOR UPDATE);
155
+ END IF;
156
+ END IF;
157
+ END IF;
158
+ RETURN OLD;
159
+ END;
160
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
161
+ SQL
162
+ execute(<<~SQL)
163
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_before_insert_row_tr_fn')} () RETURNS trigger AS $$
164
+ BEGIN
165
+ IF NEW.strand IS NOT NULL THEN
166
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
167
+ IF (SELECT COUNT(*) FROM (
168
+ SELECT 1 AS one FROM delayed_jobs WHERE strand = NEW.strand LIMIT NEW.max_concurrent
169
+ ) subquery_for_count) = NEW.max_concurrent THEN
170
+ NEW.next_in_strand := 'f';
171
+ END IF;
172
+ END IF;
173
+ RETURN NEW;
174
+ END;
175
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
176
+ SQL
177
+ end
178
+ end
179
+
180
+ connection.transaction do
181
+ reversible do |direction|
182
+ direction.up do
183
+ drop_triggers
184
+ execute("CREATE TRIGGER delayed_jobs_before_insert_row_tr BEFORE INSERT ON #{::Delayed::Job.quoted_table_name} FOR EACH ROW WHEN (NEW.strand IS NOT NULL OR NEW.singleton IS NOT NULL) EXECUTE PROCEDURE #{connection.quote_table_name('delayed_jobs_before_insert_row_tr_fn')}()")
185
+ execute("CREATE TRIGGER delayed_jobs_after_delete_row_tr AFTER DELETE ON #{::Delayed::Job.quoted_table_name} FOR EACH ROW WHEN ((OLD.strand IS NOT NULL OR OLD.singleton IS NOT NULL) AND OLD.next_in_strand=true) EXECUTE PROCEDURE #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn')}()")
186
+ end
187
+ direction.down do
188
+ drop_triggers
189
+ execute("CREATE TRIGGER delayed_jobs_before_insert_row_tr BEFORE INSERT ON #{::Delayed::Job.quoted_table_name} FOR EACH ROW WHEN (NEW.strand IS NOT NULL) EXECUTE PROCEDURE #{connection.quote_table_name('delayed_jobs_before_insert_row_tr_fn')}()")
190
+ execute("CREATE TRIGGER delayed_jobs_after_delete_row_tr AFTER DELETE ON #{::Delayed::Job.quoted_table_name} FOR EACH ROW WHEN (OLD.strand IS NOT NULL AND OLD.next_in_strand = 't') EXECUTE PROCEDURE #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn()')}")
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ def drop_triggers
197
+ execute("DROP TRIGGER delayed_jobs_before_insert_row_tr ON #{::Delayed::Job.quoted_table_name}")
198
+ execute("DROP TRIGGER delayed_jobs_after_delete_row_tr ON #{::Delayed::Job.quoted_table_name}")
199
+ end
200
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDeleteConflictingSingletonsBeforeUnlockTrigger < ActiveRecord::Migration[5.2]
4
+ def up
5
+ execute(<<~SQL)
6
+ CREATE FUNCTION #{connection.quote_table_name('delayed_jobs_before_unlock_delete_conflicting_singletons_row_fn')} () RETURNS trigger AS $$
7
+ BEGIN
8
+ IF EXISTS (SELECT 1 FROM delayed_jobs j2 WHERE j2.singleton=OLD.singleton) THEN
9
+ DELETE FROM delayed_jobs WHERE id<>OLD.id AND singleton=OLD.singleton;
10
+ END IF;
11
+ RETURN NEW;
12
+ END;
13
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
14
+ SQL
15
+ execute(<<~SQL)
16
+ CREATE TRIGGER delayed_jobs_before_unlock_delete_conflicting_singletons_row_tr BEFORE UPDATE ON #{::Delayed::Job.quoted_table_name} FOR EACH ROW WHEN (
17
+ OLD.singleton IS NOT NULL AND
18
+ OLD.singleton=NEW.singleton AND
19
+ OLD.locked_by IS NOT NULL AND
20
+ NEW.locked_by IS NULL) EXECUTE PROCEDURE #{connection.quote_table_name('delayed_jobs_before_unlock_delete_conflicting_singletons_row_fn')}();
21
+ SQL
22
+ end
23
+
24
+ def down
25
+ execute("DROP FUNCTION #{connection.quote_table_name('delayed_jobs_before_unlock_delete_conflicting_singletons_row_tr_fn')}() CASCADE")
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FixSingletonConditionInBeforeInsert < ActiveRecord::Migration[5.2]
4
+ def change
5
+ reversible do |direction|
6
+ direction.up do
7
+ execute(<<~SQL)
8
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_before_insert_row_tr_fn')} () RETURNS trigger AS $$
9
+ BEGIN
10
+ IF NEW.strand IS NOT NULL THEN
11
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
12
+ IF (SELECT COUNT(*) FROM (
13
+ SELECT 1 FROM delayed_jobs WHERE strand = NEW.strand AND next_in_strand=true LIMIT NEW.max_concurrent
14
+ ) s) = NEW.max_concurrent THEN
15
+ NEW.next_in_strand := false;
16
+ END IF;
17
+ END IF;
18
+ IF NEW.singleton IS NOT NULL THEN
19
+ -- this condition seems silly, but it forces postgres to use the two partial indexes on singleton,
20
+ -- rather than doing a seq scan
21
+ PERFORM 1 FROM delayed_jobs WHERE singleton = NEW.singleton AND (locked_by IS NULL OR locked_by IS NOT NULL);
22
+ IF FOUND THEN
23
+ NEW.next_in_strand := false;
24
+ END IF;
25
+ END IF;
26
+ RETURN NEW;
27
+ END;
28
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
29
+ SQL
30
+ end
31
+ direction.down do
32
+ execute(<<~SQL)
33
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_before_insert_row_tr_fn')} () RETURNS trigger AS $$
34
+ BEGIN
35
+ IF NEW.strand IS NOT NULL THEN
36
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
37
+ IF (SELECT COUNT(*) FROM (
38
+ SELECT 1 FROM delayed_jobs WHERE strand = NEW.strand AND next_in_strand=true LIMIT NEW.max_concurrent
39
+ ) s) = NEW.max_concurrent THEN
40
+ NEW.next_in_strand := false;
41
+ END IF;
42
+ END IF;
43
+ IF NEW.singleton IS NOT NULL THEN
44
+ PERFORM 1 FROM delayed_jobs WHERE singleton = NEW.singleton;
45
+ IF FOUND THEN
46
+ NEW.next_in_strand := false;
47
+ END IF;
48
+ END IF;
49
+ RETURN NEW;
50
+ END;
51
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
52
+ SQL
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateConflictingSingletonFunctionToUseIndex < ActiveRecord::Migration[5.2]
4
+ def up
5
+ execute(<<~SQL)
6
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_before_unlock_delete_conflicting_singletons_row_fn')} () RETURNS trigger AS $$
7
+ BEGIN
8
+ DELETE FROM delayed_jobs WHERE id<>OLD.id AND singleton=OLD.singleton AND locked_by IS NULL;
9
+ RETURN NEW;
10
+ END;
11
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
12
+ SQL
13
+ end
14
+
15
+ def down
16
+ execute(<<~SQL)
17
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_before_unlock_delete_conflicting_singletons_row_fn')} () RETURNS trigger AS $$
18
+ BEGIN
19
+ IF EXISTS (SELECT 1 FROM delayed_jobs j2 WHERE j2.singleton=OLD.singleton) THEN
20
+ DELETE FROM delayed_jobs WHERE id<>OLD.id AND singleton=OLD.singleton;
21
+ END IF;
22
+ RETURN NEW;
23
+ END;
24
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
25
+ SQL
26
+ end
27
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateAfterDeleteTriggerForSingletonIndex < ActiveRecord::Migration[5.2]
4
+ def up
5
+ execute(<<~SQL)
6
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn')} () RETURNS trigger AS $$
7
+ DECLARE
8
+ running_count integer;
9
+ should_lock boolean;
10
+ should_be_precise boolean;
11
+ update_query varchar;
12
+ skip_locked varchar;
13
+ BEGIN
14
+ IF OLD.strand IS NOT NULL THEN
15
+ should_lock := true;
16
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
17
+
18
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
19
+ running_count := (SELECT COUNT(*) FROM (
20
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
21
+ ) subquery_for_count);
22
+ should_lock := running_count < OLD.max_concurrent;
23
+ END IF;
24
+
25
+ IF should_lock THEN
26
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
27
+ END IF;
28
+
29
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
30
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
31
+ -- singleton we grab isn't already running (which is a simple existence check, since
32
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
33
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
34
+ SELECT id FROM delayed_jobs j2
35
+ WHERE next_in_strand=false AND
36
+ j2.strand=$1.strand AND
37
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id AND (j3.locked_by IS NULL OR j3.locked_by IS NOT NULL)))
38
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
39
+ LIMIT ';
40
+
41
+ IF should_be_precise THEN
42
+ running_count := (SELECT COUNT(*) FROM (
43
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
44
+ ) s);
45
+ IF running_count < OLD.max_concurrent THEN
46
+ update_query := update_query || '($1.max_concurrent - $2)';
47
+ ELSE
48
+ -- we have too many running already; just bail
49
+ RETURN OLD;
50
+ END IF;
51
+ ELSE
52
+ update_query := update_query || '1';
53
+
54
+ -- n-strands don't require precise ordering; we can make this query more performant
55
+ IF OLD.max_concurrent > 1 THEN
56
+ skip_locked := ' SKIP LOCKED';
57
+ END IF;
58
+ END IF;
59
+
60
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
61
+ EXECUTE update_query USING OLD, running_count;
62
+ ELSIF OLD.singleton IS NOT NULL THEN
63
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE singleton=OLD.singleton AND next_in_strand=false AND locked_by IS NULL;
64
+ END IF;
65
+ RETURN OLD;
66
+ END;
67
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
68
+ SQL
69
+ end
70
+
71
+ def down
72
+ execute(<<~SQL)
73
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn')} () RETURNS trigger AS $$
74
+ DECLARE
75
+ running_count integer;
76
+ should_lock boolean;
77
+ should_be_precise boolean;
78
+ update_query varchar;
79
+ skip_locked varchar;
80
+ BEGIN
81
+ IF OLD.strand IS NOT NULL THEN
82
+ should_lock := true;
83
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
84
+
85
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
86
+ running_count := (SELECT COUNT(*) FROM (
87
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
88
+ ) subquery_for_count);
89
+ should_lock := running_count < OLD.max_concurrent;
90
+ END IF;
91
+
92
+ IF should_lock THEN
93
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
94
+ END IF;
95
+
96
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
97
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
98
+ -- singleton we grab isn't already running (which is a simple existence check, since
99
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
100
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
101
+ SELECT id FROM delayed_jobs j2
102
+ WHERE next_in_strand=false AND
103
+ j2.strand=$1.strand AND
104
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id))
105
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
106
+ LIMIT ';
107
+
108
+ IF should_be_precise THEN
109
+ running_count := (SELECT COUNT(*) FROM (
110
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
111
+ ) s);
112
+ IF running_count < OLD.max_concurrent THEN
113
+ update_query := update_query || '($1.max_concurrent - $2)';
114
+ ELSE
115
+ -- we have too many running already; just bail
116
+ RETURN OLD;
117
+ END IF;
118
+ ELSE
119
+ update_query := update_query || '1';
120
+
121
+ -- n-strands don't require precise ordering; we can make this query more performant
122
+ IF OLD.max_concurrent > 1 THEN
123
+ skip_locked := ' SKIP LOCKED';
124
+ END IF;
125
+ END IF;
126
+
127
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
128
+ EXECUTE update_query USING OLD, running_count;
129
+ ELSIF OLD.singleton IS NOT NULL THEN
130
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE singleton=OLD.singleton AND next_in_strand=false;
131
+ END IF;
132
+ RETURN OLD;
133
+ END;
134
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
135
+ SQL
136
+ end
137
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateAfterDeleteTriggerForSingletonTransitionCases < ActiveRecord::Migration[5.2]
4
+ def up
5
+ execute(<<~SQL)
6
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn')} () RETURNS trigger AS $$
7
+ DECLARE
8
+ next_strand varchar;
9
+ running_count integer;
10
+ should_lock boolean;
11
+ should_be_precise boolean;
12
+ update_query varchar;
13
+ skip_locked varchar;
14
+ transition boolean;
15
+ BEGIN
16
+ IF OLD.strand IS NOT NULL THEN
17
+ should_lock := true;
18
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
19
+
20
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
21
+ running_count := (SELECT COUNT(*) FROM (
22
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
23
+ ) subquery_for_count);
24
+ should_lock := running_count < OLD.max_concurrent;
25
+ END IF;
26
+
27
+ IF should_lock THEN
28
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
29
+ END IF;
30
+
31
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
32
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
33
+ -- singleton we grab isn't already running (which is a simple existence check, since
34
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
35
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
36
+ SELECT id FROM delayed_jobs j2
37
+ WHERE next_in_strand=false AND
38
+ j2.strand=$1.strand AND
39
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id AND (j3.locked_by IS NULL OR j3.locked_by IS NOT NULL)))
40
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
41
+ LIMIT ';
42
+
43
+ IF should_be_precise THEN
44
+ running_count := (SELECT COUNT(*) FROM (
45
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
46
+ ) s);
47
+ IF running_count < OLD.max_concurrent THEN
48
+ update_query := update_query || '($1.max_concurrent - $2)';
49
+ ELSE
50
+ -- we have too many running already; just bail
51
+ RETURN OLD;
52
+ END IF;
53
+ ELSE
54
+ update_query := update_query || '1';
55
+
56
+ -- n-strands don't require precise ordering; we can make this query more performant
57
+ IF OLD.max_concurrent > 1 THEN
58
+ skip_locked := ' SKIP LOCKED';
59
+ END IF;
60
+ END IF;
61
+
62
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
63
+ EXECUTE update_query USING OLD, running_count;
64
+ END IF;
65
+
66
+ IF OLD.singleton IS NOT NULL THEN
67
+ transition := EXISTS (SELECT 1 FROM delayed_jobs AS j1 WHERE j1.singleton = OLD.singleton AND j1.strand IS DISTINCT FROM OLD.strand AND locked_by IS NULL);
68
+
69
+ IF transition THEN
70
+ next_strand := (SELECT j1.strand FROM delayed_jobs AS j1 WHERE j1.singleton = OLD.singleton AND j1.strand IS DISTINCT FROM OLD.strand AND locked_by IS NULL AND j1.strand IS NOT NULL LIMIT 1);
71
+
72
+ IF next_strand IS NOT NULL THEN
73
+ -- if the singleton has a new strand defined, we need to lock it to ensure we obey n_strand constraints --
74
+ IF NOT pg_try_advisory_xact_lock(half_md5_as_bigint(next_strand)) THEN
75
+ -- a failure to acquire the lock means that another process already has it and will thus handle this singleton --
76
+ RETURN OLD;
77
+ END IF;
78
+ END IF;
79
+ ELSIF OLD.strand IS NOT NULL THEN
80
+ -- if there is no transition and there is a strand then we have already handled this singleton in the case above --
81
+ RETURN OLD;
82
+ END IF;
83
+
84
+ -- handles transitioning a singleton from stranded to not stranded --
85
+ -- handles transitioning a singleton from unstranded to stranded --
86
+ -- handles transitioning a singleton from strand A to strand B --
87
+ -- these transitions are a relatively rare case, so we take a shortcut and --
88
+ -- only start the next singleton if its strand does not currently have any running jobs --
89
+ -- if it does, the next stranded job that finishes will start this singleton if it can --
90
+ UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
91
+ SELECT id FROM delayed_jobs j2
92
+ WHERE next_in_strand=false AND
93
+ j2.singleton=OLD.singleton AND
94
+ j2.locked_by IS NULL AND
95
+ (j2.strand IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.strand=j2.strand AND j3.id<>j2.id))
96
+ FOR UPDATE
97
+ );
98
+ END IF;
99
+ RETURN OLD;
100
+ END;
101
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
102
+ SQL
103
+ end
104
+
105
+ def down
106
+ execute(<<~SQL)
107
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn')} () RETURNS trigger AS $$
108
+ DECLARE
109
+ running_count integer;
110
+ should_lock boolean;
111
+ should_be_precise boolean;
112
+ update_query varchar;
113
+ skip_locked varchar;
114
+ BEGIN
115
+ IF OLD.strand IS NOT NULL THEN
116
+ should_lock := true;
117
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
118
+
119
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
120
+ running_count := (SELECT COUNT(*) FROM (
121
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
122
+ ) subquery_for_count);
123
+ should_lock := running_count < OLD.max_concurrent;
124
+ END IF;
125
+
126
+ IF should_lock THEN
127
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
128
+ END IF;
129
+
130
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
131
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
132
+ -- singleton we grab isn't already running (which is a simple existence check, since
133
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
134
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
135
+ SELECT id FROM delayed_jobs j2
136
+ WHERE next_in_strand=false AND
137
+ j2.strand=$1.strand AND
138
+ (j2.singleton IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.singleton=j2.singleton AND j3.id<>j2.id AND (j3.locked_by IS NULL OR j3.locked_by IS NOT NULL)))
139
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
140
+ LIMIT ';
141
+
142
+ IF should_be_precise THEN
143
+ running_count := (SELECT COUNT(*) FROM (
144
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
145
+ ) s);
146
+ IF running_count < OLD.max_concurrent THEN
147
+ update_query := update_query || '($1.max_concurrent - $2)';
148
+ ELSE
149
+ -- we have too many running already; just bail
150
+ RETURN OLD;
151
+ END IF;
152
+ ELSE
153
+ update_query := update_query || '1';
154
+
155
+ -- n-strands don't require precise ordering; we can make this query more performant
156
+ IF OLD.max_concurrent > 1 THEN
157
+ skip_locked := ' SKIP LOCKED';
158
+ END IF;
159
+ END IF;
160
+
161
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
162
+ EXECUTE update_query USING OLD, running_count;
163
+ ELSIF OLD.singleton IS NOT NULL THEN
164
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE singleton=OLD.singleton AND next_in_strand=false AND locked_by IS NULL;
165
+ END IF;
166
+ RETURN OLD;
167
+ END;
168
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
169
+ SQL
170
+ end
171
+ end
@@ -74,7 +74,7 @@ module SwitchmanInstJobs
74
74
  self.shard_id = shard.id
75
75
  self.shard_id = nil if shard.is_a?(::Switchman::DefaultShard)
76
76
  # If jobs are held for a shard, enqueue new ones as held as well
77
- return unless shard.jobs_held
77
+ return unless ::Switchman::Shard.columns_hash.key?('jobs_held') && shard.jobs_held
78
78
 
79
79
  self.locked_by = ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY
80
80
  self.locked_at = ::Delayed::Job.db_time_now
@@ -1,7 +1,3 @@
1
- # Just disabling all the rubocop metrics for this file for now,
2
- # as it is a direct port-in of existing code
3
-
4
- # rubocop:disable Metrics/BlockLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/ClassLength
5
1
  require 'set'
6
2
  require 'parallel'
7
3
 
@@ -76,7 +72,7 @@ module SwitchmanInstJobs
76
72
 
77
73
  def clear_shard_cache(debug_message = nil)
78
74
  ::Switchman.cache.clear
79
- Rails.logger.debug("Waiting for caches to clear #{debug_message}")
75
+ Rails.logger.debug { "Waiting for caches to clear #{debug_message}" }
80
76
  # Wait a little over the 60 second in-process shard cache clearing
81
77
  # threshold to ensure that all new stranded jobs are now being
82
78
  # enqueued with next_in_strand: false
@@ -225,12 +221,14 @@ module SwitchmanInstJobs
225
221
  target_jobs = scope.limit(1000).lock('FOR UPDATE SKIP LOCKED')
226
222
 
227
223
  query = source_shard.activate(::Delayed::Backend::ActiveRecord::AbstractJob) do
228
- "WITH limited_jobs AS (#{target_jobs.to_sql}) " \
229
- "UPDATE #{::Delayed::Job.quoted_table_name} " \
230
- "SET locked_by = #{::Delayed::Job.connection.quote(::Delayed::Backend::Base::ON_HOLD_LOCKED_BY)}, " \
231
- "locked_at = #{::Delayed::Job.connection.quote(::Delayed::Job.db_time_now)} "\
232
- "FROM limited_jobs WHERE limited_jobs.id=#{::Delayed::Job.quoted_table_name}.id " \
233
- "RETURNING #{::Delayed::Job.quoted_table_name}.*"
224
+ <<~SQL
225
+ WITH limited_jobs AS (#{target_jobs.to_sql})
226
+ UPDATE #{::Delayed::Job.quoted_table_name}
227
+ SET locked_by = #{::Delayed::Job.connection.quote(::Delayed::Backend::Base::ON_HOLD_LOCKED_BY)},
228
+ locked_at = #{::Delayed::Job.connection.quote(::Delayed::Job.db_time_now)}
229
+ FROM limited_jobs WHERE limited_jobs.id=#{::Delayed::Job.quoted_table_name}.id
230
+ RETURNING #{::Delayed::Job.quoted_table_name}.*
231
+ SQL
234
232
  end
235
233
 
236
234
  jobs = source_shard.activate(::Delayed::Backend::ActiveRecord::AbstractJob) do
@@ -283,7 +281,10 @@ module SwitchmanInstJobs
283
281
  connection = ::Delayed::Job.connection
284
282
  quoted_keys = keys.map { |k| connection.quote_column_name(k) }.join(', ')
285
283
 
286
- connection.execute "COPY #{::Delayed::Job.quoted_table_name} (#{quoted_keys}) FROM STDIN"
284
+ connection.execute 'DROP TABLE IF EXISTS delayed_jobs_bulk_copy'
285
+ connection.execute "CREATE TEMPORARY TABLE delayed_jobs_bulk_copy
286
+ (LIKE #{::Delayed::Job.quoted_table_name} INCLUDING DEFAULTS)"
287
+ connection.execute "COPY delayed_jobs_bulk_copy (#{quoted_keys}) FROM STDIN"
287
288
  records.map do |record|
288
289
  connection.raw_connection.put_copy_data("#{keys.map { |k| quote_text(record[k]) }.join("\t")}\n")
289
290
  end
@@ -295,6 +296,9 @@ module SwitchmanInstJobs
295
296
  rescue StandardError => e
296
297
  raise connection.send(:translate_exception, e, 'COPY FROM STDIN')
297
298
  end
299
+ connection.execute "INSERT INTO #{::Delayed::Job.quoted_table_name} (#{quoted_keys})
300
+ SELECT #{quoted_keys} FROM delayed_jobs_bulk_copy
301
+ ON CONFLICT (singleton) WHERE singleton IS NOT NULL AND locked_by IS NULL DO NOTHING"
298
302
  result.cmd_tuples
299
303
  end
300
304
 
@@ -312,5 +316,3 @@ module SwitchmanInstJobs
312
316
  end
313
317
  end
314
318
  end
315
-
316
- # rubocop:enable Metrics/BlockLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/ClassLength
@@ -1,3 +1,3 @@
1
1
  module SwitchmanInstJobs
2
- VERSION = '4.0.0'.freeze
2
+ VERSION = '4.0.2'.freeze
3
3
  end
@@ -17,10 +17,6 @@ module SwitchmanInstJobs
17
17
  ::Delayed::Backend::ActiveRecord::Job.prepend(
18
18
  Delayed::Backend::Base
19
19
  )
20
- ::Delayed::Backend::Redis::Job.prepend(
21
- Delayed::Backend::Base
22
- )
23
- ::Delayed::Backend::Redis::Job.column :shard_id, :integer
24
20
  ::Delayed::Pool.prepend Delayed::Pool
25
21
  ::Delayed::Worker.prepend Delayed::Worker
26
22
  ::Delayed::Worker::HealthCheck.prepend Delayed::Worker::HealthCheck
metadata CHANGED
@@ -1,35 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman-inst-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Petty
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-24 00:00:00.000000000 Z
11
+ date: 2021-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inst-jobs
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '2.0'
20
17
  - - ">="
21
18
  - !ruby/object:Gem::Version
22
- version: 2.3.1
19
+ version: 2.4.9
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4.0'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - "~>"
28
- - !ruby/object:Gem::Version
29
- version: '2.0'
30
27
  - - ">="
31
28
  - !ruby/object:Gem::Version
32
- version: 2.3.1
29
+ version: 2.4.9
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: parallel
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -238,6 +238,34 @@ dependencies:
238
238
  - - "~>"
239
239
  - !ruby/object:Gem::Version
240
240
  version: '2.10'
241
+ - !ruby/object:Gem::Dependency
242
+ name: rubocop-rake
243
+ requirement: !ruby/object:Gem::Requirement
244
+ requirements:
245
+ - - "~>"
246
+ - !ruby/object:Gem::Version
247
+ version: '0.6'
248
+ type: :development
249
+ prerelease: false
250
+ version_requirements: !ruby/object:Gem::Requirement
251
+ requirements:
252
+ - - "~>"
253
+ - !ruby/object:Gem::Version
254
+ version: '0.6'
255
+ - !ruby/object:Gem::Dependency
256
+ name: rubocop-rspec
257
+ requirement: !ruby/object:Gem::Requirement
258
+ requirements:
259
+ - - "~>"
260
+ - !ruby/object:Gem::Version
261
+ version: '2.4'
262
+ type: :development
263
+ prerelease: false
264
+ version_requirements: !ruby/object:Gem::Requirement
265
+ requirements:
266
+ - - "~>"
267
+ - !ruby/object:Gem::Version
268
+ version: '2.4'
241
269
  - !ruby/object:Gem::Dependency
242
270
  name: simplecov
243
271
  requirement: !ruby/object:Gem::Requirement
@@ -266,7 +294,7 @@ dependencies:
266
294
  - - "~>"
267
295
  - !ruby/object:Gem::Version
268
296
  version: '1.4'
269
- description:
297
+ description:
270
298
  email:
271
299
  - bpetty@instructure.com
272
300
  executables: []
@@ -303,6 +331,13 @@ files:
303
331
  - db/migrate/20200822014259_add_block_stranded_to_switchman_shards.rb
304
332
  - db/migrate/20200824222232_speed_up_max_concurrent_delete_trigger.rb
305
333
  - db/migrate/20200825011002_add_strand_order_override.rb
334
+ - db/migrate/20210809145804_add_n_strand_index.rb
335
+ - db/migrate/20210812210128_add_singleton_column.rb
336
+ - db/migrate/20210917232626_add_delete_conflicting_singletons_before_unlock_trigger.rb
337
+ - db/migrate/20210928174754_fix_singleton_condition_in_before_insert.rb
338
+ - db/migrate/20210929204903_update_conflicting_singleton_function_to_use_index.rb
339
+ - db/migrate/20211101190934_update_after_delete_trigger_for_singleton_index.rb
340
+ - db/migrate/20211207094200_update_after_delete_trigger_for_singleton_transition_cases.rb
306
341
  - lib/switchman-inst-jobs.rb
307
342
  - lib/switchman_inst_jobs.rb
308
343
  - lib/switchman_inst_jobs/active_record/connection_adapters/connection_pool.rb
@@ -327,8 +362,10 @@ files:
327
362
  homepage: https://github.com/instructure/switchman-inst-jobs
328
363
  licenses:
329
364
  - MIT
330
- metadata: {}
331
- post_install_message:
365
+ metadata:
366
+ allowed_push_host: https://rubygems.org
367
+ rubygems_mfa_required: 'true'
368
+ post_install_message:
332
369
  rdoc_options: []
333
370
  require_paths:
334
371
  - lib
@@ -343,8 +380,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
343
380
  - !ruby/object:Gem::Version
344
381
  version: '0'
345
382
  requirements: []
346
- rubygems_version: 3.2.15
347
- signing_key:
383
+ rubygems_version: 3.1.4
384
+ signing_key:
348
385
  specification_version: 4
349
386
  summary: Switchman and Instructure Jobs compatibility gem.
350
387
  test_files: []