switchman-inst-jobs 3.1.0 → 3.2.7

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: 9466db757372227599ce8e15411f697c1955dc59bc94dba7143fcc0e4e18b47b
4
- data.tar.gz: 0d5e44aecbbf1e9110dcb83e305ada21f98ae52c7b75f7ce052482dc648b4f39
3
+ metadata.gz: ac66ce458b7356068a5f576593f9692b5aa58d608584033a3aca941b31dc4a2d
4
+ data.tar.gz: c790f410b55117257f9b447987f68617b61222b10bf621385736b096affa25f8
5
5
  SHA512:
6
- metadata.gz: 883bfa88ae5677c9b8929be849fd468d4b2861a2947c3c2bb626db12fe598c773f7e45706f4ff817558104e072d7c64aa5c73b7823c86fcb18361b06a5f5bd98
7
- data.tar.gz: caa20756112d7667fbf00ab5fdfbea825f3515bd9593c5135a399b652e351f8722f49a05088db5347df03c29b900a6139d1dff37b975c723d28a8d2e35a198ae
6
+ metadata.gz: 5d21bcfec5e444aeaf4bf839c13ab40453ea7b27c780e9153aeea23d5692800ff0e36dac604c7b7dec7c7723aea6a1baba675748a543240a69b37a3486665110
7
+ data.tar.gz: ea0797fc7294cf1be7b641ad874e4560a69a6a7bc04228ee85f7c284f5fcc1288139b6824c72ae5db1c521b19fa3aec55d36c7831dc819046a8af7f2d623886b
@@ -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
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FixSingletonRaceConditionInsert < 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
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(CONCAT('singleton:', NEW.singleton)));
20
+ -- this condition seems silly, but it forces postgres to use the two partial indexes on singleton,
21
+ -- rather than doing a seq scan
22
+ PERFORM 1 FROM delayed_jobs WHERE singleton = NEW.singleton AND (locked_by IS NULL OR locked_by IS NOT NULL);
23
+ IF FOUND THEN
24
+ NEW.next_in_strand := false;
25
+ END IF;
26
+ END IF;
27
+ RETURN NEW;
28
+ END;
29
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
30
+ SQL
31
+ end
32
+ direction.down do
33
+ execute(<<~SQL)
34
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_before_insert_row_tr_fn')} () RETURNS trigger AS $$
35
+ BEGIN
36
+ IF NEW.strand IS NOT NULL THEN
37
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
38
+ IF (SELECT COUNT(*) FROM (
39
+ SELECT 1 FROM delayed_jobs WHERE strand = NEW.strand AND next_in_strand=true LIMIT NEW.max_concurrent
40
+ ) s) = NEW.max_concurrent THEN
41
+ NEW.next_in_strand := false;
42
+ END IF;
43
+ END IF;
44
+ IF NEW.singleton IS NOT NULL THEN
45
+ -- this condition seems silly, but it forces postgres to use the two partial indexes on singleton,
46
+ -- rather than doing a seq scan
47
+ PERFORM 1 FROM delayed_jobs WHERE singleton = NEW.singleton AND (locked_by IS NULL OR locked_by IS NOT NULL);
48
+ IF FOUND THEN
49
+ NEW.next_in_strand := false;
50
+ END IF;
51
+ END IF;
52
+ RETURN NEW;
53
+ END;
54
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
55
+ SQL
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FixSingletonRaceConditionDelete < 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
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(CONCAT('singleton:', OLD.singleton)));
68
+
69
+ 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);
70
+
71
+ IF transition THEN
72
+ 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);
73
+
74
+ IF next_strand IS NOT NULL THEN
75
+ -- if the singleton has a new strand defined, we need to lock it to ensure we obey n_strand constraints --
76
+ IF NOT pg_try_advisory_xact_lock(half_md5_as_bigint(next_strand)) THEN
77
+ -- a failure to acquire the lock means that another process already has it and will thus handle this singleton --
78
+ RETURN OLD;
79
+ END IF;
80
+ END IF;
81
+ ELSIF OLD.strand IS NOT NULL THEN
82
+ -- if there is no transition and there is a strand then we have already handled this singleton in the case above --
83
+ RETURN OLD;
84
+ END IF;
85
+
86
+ -- handles transitioning a singleton from stranded to not stranded --
87
+ -- handles transitioning a singleton from unstranded to stranded --
88
+ -- handles transitioning a singleton from strand A to strand B --
89
+ -- these transitions are a relatively rare case, so we take a shortcut and --
90
+ -- only start the next singleton if its strand does not currently have any running jobs --
91
+ -- if it does, the next stranded job that finishes will start this singleton if it can --
92
+ UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
93
+ SELECT id FROM delayed_jobs j2
94
+ WHERE next_in_strand=false AND
95
+ j2.singleton=OLD.singleton AND
96
+ j2.locked_by IS NULL AND
97
+ (j2.strand IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.strand=j2.strand AND j3.id<>j2.id))
98
+ FOR UPDATE
99
+ );
100
+ END IF;
101
+ RETURN OLD;
102
+ END;
103
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
104
+ SQL
105
+ end
106
+
107
+ def down
108
+ execute(<<~SQL)
109
+ CREATE OR REPLACE FUNCTION #{connection.quote_table_name('delayed_jobs_after_delete_row_tr_fn')} () RETURNS trigger AS $$
110
+ DECLARE
111
+ next_strand varchar;
112
+ running_count integer;
113
+ should_lock boolean;
114
+ should_be_precise boolean;
115
+ update_query varchar;
116
+ skip_locked varchar;
117
+ transition boolean;
118
+ BEGIN
119
+ IF OLD.strand IS NOT NULL THEN
120
+ should_lock := true;
121
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
122
+
123
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
124
+ running_count := (SELECT COUNT(*) FROM (
125
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
126
+ ) subquery_for_count);
127
+ should_lock := running_count < OLD.max_concurrent;
128
+ END IF;
129
+
130
+ IF should_lock THEN
131
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
132
+ END IF;
133
+
134
+ -- note that we don't really care if the row we're deleting has a singleton, or if it even
135
+ -- matches the row(s) we're going to update. we just need to make sure that whatever
136
+ -- singleton we grab isn't already running (which is a simple existence check, since
137
+ -- the unique indexes ensure there is at most one singleton running, and one queued)
138
+ update_query := 'UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
139
+ SELECT id FROM delayed_jobs j2
140
+ WHERE next_in_strand=false AND
141
+ j2.strand=$1.strand AND
142
+ (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)))
143
+ ORDER BY j2.strand_order_override ASC, j2.id ASC
144
+ LIMIT ';
145
+
146
+ IF should_be_precise THEN
147
+ running_count := (SELECT COUNT(*) FROM (
148
+ SELECT 1 FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
149
+ ) s);
150
+ IF running_count < OLD.max_concurrent THEN
151
+ update_query := update_query || '($1.max_concurrent - $2)';
152
+ ELSE
153
+ -- we have too many running already; just bail
154
+ RETURN OLD;
155
+ END IF;
156
+ ELSE
157
+ update_query := update_query || '1';
158
+
159
+ -- n-strands don't require precise ordering; we can make this query more performant
160
+ IF OLD.max_concurrent > 1 THEN
161
+ skip_locked := ' SKIP LOCKED';
162
+ END IF;
163
+ END IF;
164
+
165
+ update_query := update_query || ' FOR UPDATE' || COALESCE(skip_locked, '') || ')';
166
+ EXECUTE update_query USING OLD, running_count;
167
+ END IF;
168
+
169
+ IF OLD.singleton IS NOT NULL THEN
170
+ 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);
171
+
172
+ IF transition THEN
173
+ 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);
174
+
175
+ IF next_strand IS NOT NULL THEN
176
+ -- if the singleton has a new strand defined, we need to lock it to ensure we obey n_strand constraints --
177
+ IF NOT pg_try_advisory_xact_lock(half_md5_as_bigint(next_strand)) THEN
178
+ -- a failure to acquire the lock means that another process already has it and will thus handle this singleton --
179
+ RETURN OLD;
180
+ END IF;
181
+ END IF;
182
+ ELSIF OLD.strand IS NOT NULL THEN
183
+ -- if there is no transition and there is a strand then we have already handled this singleton in the case above --
184
+ RETURN OLD;
185
+ END IF;
186
+
187
+ -- handles transitioning a singleton from stranded to not stranded --
188
+ -- handles transitioning a singleton from unstranded to stranded --
189
+ -- handles transitioning a singleton from strand A to strand B --
190
+ -- these transitions are a relatively rare case, so we take a shortcut and --
191
+ -- only start the next singleton if its strand does not currently have any running jobs --
192
+ -- if it does, the next stranded job that finishes will start this singleton if it can --
193
+ UPDATE delayed_jobs SET next_in_strand=true WHERE id IN (
194
+ SELECT id FROM delayed_jobs j2
195
+ WHERE next_in_strand=false AND
196
+ j2.singleton=OLD.singleton AND
197
+ j2.locked_by IS NULL AND
198
+ (j2.strand IS NULL OR NOT EXISTS (SELECT 1 FROM delayed_jobs j3 WHERE j3.strand=j2.strand AND j3.id<>j2.id))
199
+ FOR UPDATE
200
+ );
201
+ END IF;
202
+ RETURN OLD;
203
+ END;
204
+ $$ LANGUAGE plpgsql SET search_path TO #{::Switchman::Shard.current.name};
205
+ SQL
206
+ end
207
+ end
@@ -18,7 +18,7 @@ module SwitchmanInstJobs
18
18
  enqueue_options = options.merge(
19
19
  current_shard: current_shard
20
20
  )
21
- enqueue_job = -> { ::GuardRail.activate(:master) { super(object, **enqueue_options) } }
21
+ enqueue_job = -> { ::GuardRail.activate(:primary) { super(object, **enqueue_options) } }
22
22
 
23
23
  # Another dj shard must be currently manually activated, so just use that
24
24
  # In general this will only happen in unusual circumstances like tests
@@ -43,8 +43,7 @@ module SwitchmanInstJobs
43
43
  end
44
44
 
45
45
  def configured_shard_ids
46
- (::Delayed::Settings.worker_config.try(:[], 'workers') || []).
47
- map { |w| w['shard'] }.compact.uniq
46
+ ::SwitchmanInstJobs::Delayed::Settings.configured_shard_ids
48
47
  end
49
48
 
50
49
  def processes_locked_locally
@@ -74,7 +73,7 @@ module SwitchmanInstJobs
74
73
  self.shard_id = shard.id
75
74
  self.shard_id = nil if shard.is_a?(::Switchman::DefaultShard)
76
75
  # If jobs are held for a shard, enqueue new ones as held as well
77
- return unless shard.jobs_held
76
+ return unless ::Switchman::Shard.columns_hash.key?('jobs_held') && shard.jobs_held
78
77
 
79
78
  self.locked_by = ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY
80
79
  self.locked_at = ::Delayed::Job.db_time_now
@@ -82,6 +81,8 @@ module SwitchmanInstJobs
82
81
  end
83
82
 
84
83
  def invoke_job
84
+ raise ShardNotFoundError, shard_id unless current_shard
85
+
85
86
  current_shard.activate { super }
86
87
  end
87
88
 
@@ -0,0 +1,9 @@
1
+ module SwitchmanInstJobs
2
+ module Delayed
3
+ module Settings
4
+ def self.configured_shard_ids
5
+ (::Delayed::Settings.worker_config.try(:[], 'workers') || []).map { |w| w['shard'] }.compact.uniq
6
+ end
7
+ end
8
+ end
9
+ end
@@ -18,22 +18,23 @@ module SwitchmanInstJobs
18
18
  ::Delayed::Settings.worker_health_check_config['service_name'] = original_service_name
19
19
  end
20
20
 
21
- def reschedule_abandoned_jobs(call_super: false)
22
- shards = ::Switchman::Shard.delayed_jobs_shards.to_a
23
- call_super = shards.first if shards.length == 1
24
- unless call_super == false
25
- call_super.activate(:delayed_jobs) do
26
- return munge_service_name(call_super) { super() }
21
+ def reschedule_abandoned_jobs
22
+ shard_ids = ::SwitchmanInstJobs::Delayed::Settings.configured_shard_ids
23
+ shards = shard_ids.map { |shard_id| ::Delayed::Worker.shard(shard_id) }
24
+ ::Switchman::Shard.with_each_shard(shards, [:delayed_jobs]) do
25
+ dj_shard = ::Switchman::Shard.current(:delayed_jobs)
26
+ dj_shard.activate do
27
+ munge_service_name(dj_shard) do
28
+ # because this rescheduling process is running on every host, we need
29
+ # to make sure that it's functioning for each shard the current
30
+ # host is programmed to interact with, but ONLY for those shards.
31
+ # reading the config lets us iterate over any shards this host should
32
+ # work with and lets us pick the correct service name to identify which
33
+ # hosts are currently alive and valid via the health checks
34
+ super()
35
+ end
27
36
  end
28
37
  end
29
-
30
- ::Switchman::Shard.with_each_shard(shards, [:delayed_jobs], exception: :ignore) do
31
- shard = ::Switchman::Shard.current(:delayed_jobs)
32
- singleton = <<~SINGLETON
33
- periodic: Delayed::Worker::HealthCheck.reschedule_abandoned_jobs:#{shard.id}
34
- SINGLETON
35
- delay(singleton: singleton).reschedule_abandoned_jobs(call_super: shard)
36
- end
37
38
  end
38
39
  end
39
40
  end
@@ -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
 
@@ -39,16 +35,12 @@ module SwitchmanInstJobs
39
35
 
40
36
  # Do the updates in batches and then just clear redis instead of clearing them one at a time
41
37
  target_shards.each do |target_shard, shards|
42
- ::Switchman::Shard.where(id: shards).update_all(delayed_jobs_shard_id: target_shard, block_stranded: true)
38
+ updates = { delayed_jobs_shard_id: target_shard, block_stranded: true }
39
+ updates[:updated_at] = Time.zone.now if ::Switchman::Shard.column_names.include?('updated_at')
40
+ ::Switchman::Shard.where(id: shards).update_all(updates)
43
41
  end
44
42
  clear_shard_cache
45
43
 
46
- # Wait a little over the 60 second in-process shard cache clearing
47
- # threshold to ensure that all new stranded jobs are now being
48
- # enqueued with next_in_strand: false
49
- Rails.logger.debug('Waiting for caches to clear')
50
- sleep(65) unless @skip_cache_wait
51
-
52
44
  ::Switchman::Shard.clear_cache
53
45
  # rubocop:disable Style/CombinableLoops
54
46
  # We first migrate strands so that we can stop blocking strands before we migrate unstranded jobs
@@ -59,11 +51,33 @@ module SwitchmanInstJobs
59
51
  source_shards.each do |s|
60
52
  ::Switchman::Shard.lookup(s).activate(:delayed_jobs) { migrate_everything }
61
53
  end
54
+ ensure_unblock_stranded_for(shard_map.map(&:first))
62
55
  # rubocop:enable Style/CombinableLoops
63
56
  end
64
57
 
65
- def clear_shard_cache
58
+ # if :migrate_strands ran on any shards that fell into scenario 1, then
59
+ # block_stranded never got flipped, so do that now.
60
+ def ensure_unblock_stranded_for(shards)
61
+ shards = ::Switchman::Shard.where(id: shards, block_stranded: true).to_a
62
+ return unless shards.any?
63
+
64
+ ::Switchman::Shard.where(id: shards).update_all(block_stranded: false)
65
+ clear_shard_cache
66
+
67
+ # shards is an array of shard objects that is now stale cause block_stranded has been updated.
68
+ shards.map(&:delayed_jobs_shard).uniq.each do |dj_shard|
69
+ unblock_strands(dj_shard)
70
+ end
71
+ end
72
+
73
+ def clear_shard_cache(debug_message = nil)
66
74
  ::Switchman.cache.clear
75
+ Rails.logger.debug("Waiting for caches to clear #{debug_message}")
76
+ # Wait a little over the 60 second in-process shard cache clearing
77
+ # threshold to ensure that all new stranded jobs are now being
78
+ # enqueued with next_in_strand: false
79
+ # @skip_cache_wait is for spec usage only
80
+ sleep(65) unless @skip_cache_wait
67
81
  end
68
82
 
69
83
  # This method expects that all relevant shards already have block_stranded: true
@@ -125,7 +139,7 @@ module SwitchmanInstJobs
125
139
  update_all(next_in_strand: false)
126
140
  end
127
141
 
128
- # 4) is taken care of here, by leaveing next_in_strand alone and
142
+ # 4) is taken care of here, by leaving next_in_strand alone and
129
143
  # it should execute on the new shard
130
144
  batch_move_jobs(
131
145
  target_shard: target_shard,
@@ -141,35 +155,32 @@ module SwitchmanInstJobs
141
155
  updated = ::Switchman::Shard.where(id: source_shard_ids, block_stranded: true).
142
156
  update_all(block_stranded: false)
143
157
  # If this is being manually re-run for some reason to clean something up, don't wait for nothing to happen
144
- unless updated.zero?
145
- clear_shard_cache
146
- # Wait a little over the 60 second in-process shard cache clearing
147
- # threshold to ensure that all new stranded jobs are now being
148
- # enqueued with next_in_strand: false
149
- Rails.logger.debug("Waiting for caches to clear (#{source_shard.id} -> #{target_shard.id})")
150
- # for spec usage only
151
- sleep(65) unless @skip_cache_wait
152
- end
158
+ clear_shard_cache("(#{source_shard.id} -> #{target_shard.id})") unless updated.zero?
159
+
153
160
  ::Switchman::Shard.clear_cache
154
161
  # At this time, let's unblock all the strands on the target shard that aren't being held by a blocker
155
162
  # but actually could have run and we just didn't know it because we didn't know if they had jobs
156
163
  # on the source shard
157
- target_shard.activate(:delayed_jobs) do
158
- loop do
159
- # We only want to unlock stranded jobs where they don't belong to a blocked shard (if they *do* belong)
160
- # to a blocked shard, they must be part of a concurrent jobs migration from a different source shard to
161
- # this target shard, so we shouldn't unlock them yet. We only ever unlock one job here to keep the
162
- # logic cleaner; if the job is n-stranded, after the first one runs, the trigger will unlock larger
163
- # batches
164
- break if ::Delayed::Job.where(id: ::Delayed::Job.select('DISTINCT ON (strand) id').
165
- where.not(strand: nil).
166
- where.not(shard_id: ::Switchman::Shard.where(block_stranded: true).pluck(:id)).where(
167
- ::Delayed::Job.select(1).from("#{::Delayed::Job.quoted_table_name} dj2").
168
- where("dj2.next_in_strand = true OR dj2.source = 'JobsMigrator::StrandBlocker'").
169
- where('dj2.strand = delayed_jobs.strand').arel.exists.not
170
- ).order(:strand, :strand_order_override, :id)).limit(500).update_all(next_in_strand: true).zero?
171
- end
172
- end
164
+ unblock_strands(target_shard)
165
+ end
166
+ end
167
+ end
168
+
169
+ def unblock_strands(target_shard)
170
+ target_shard.activate(:delayed_jobs) do
171
+ loop do
172
+ # We only want to unlock stranded jobs where they don't belong to a blocked shard (if they *do* belong)
173
+ # to a blocked shard, they must be part of a concurrent jobs migration from a different source shard to
174
+ # this target shard, so we shouldn't unlock them yet. We only ever unlock one job here to keep the
175
+ # logic cleaner; if the job is n-stranded, after the first one runs, the trigger will unlock larger
176
+ # batches
177
+ break if ::Delayed::Job.where(id: ::Delayed::Job.select('DISTINCT ON (strand) id').
178
+ where.not(strand: nil).
179
+ where.not(shard_id: ::Switchman::Shard.where(block_stranded: true).pluck(:id)).where(
180
+ ::Delayed::Job.select(1).from("#{::Delayed::Job.quoted_table_name} dj2").
181
+ where("dj2.next_in_strand = true OR dj2.source = 'JobsMigrator::StrandBlocker'").
182
+ where('dj2.strand = delayed_jobs.strand').arel.exists.not
183
+ ).order(:strand, :strand_order_override, :id)).limit(500).update_all(next_in_strand: true).zero?
173
184
  end
174
185
  end
175
186
  end
@@ -266,7 +277,10 @@ module SwitchmanInstJobs
266
277
  connection = ::Delayed::Job.connection
267
278
  quoted_keys = keys.map { |k| connection.quote_column_name(k) }.join(', ')
268
279
 
269
- connection.execute "COPY #{::Delayed::Job.quoted_table_name} (#{quoted_keys}) FROM STDIN"
280
+ connection.execute 'DROP TABLE IF EXISTS delayed_jobs_bulk_copy'
281
+ connection.execute "CREATE TEMPORARY TABLE delayed_jobs_bulk_copy
282
+ (LIKE #{::Delayed::Job.quoted_table_name} INCLUDING DEFAULTS)"
283
+ connection.execute "COPY delayed_jobs_bulk_copy (#{quoted_keys}) FROM STDIN"
270
284
  records.map do |record|
271
285
  connection.raw_connection.put_copy_data("#{keys.map { |k| quote_text(record[k]) }.join("\t")}\n")
272
286
  end
@@ -278,6 +292,9 @@ module SwitchmanInstJobs
278
292
  rescue StandardError => e
279
293
  raise connection.send(:translate_exception, e, 'COPY FROM STDIN')
280
294
  end
295
+ connection.execute "INSERT INTO #{::Delayed::Job.quoted_table_name} (#{quoted_keys})
296
+ SELECT #{quoted_keys} FROM delayed_jobs_bulk_copy
297
+ ON CONFLICT (singleton) WHERE singleton IS NOT NULL AND locked_by IS NULL DO NOTHING"
281
298
  result.cmd_tuples
282
299
  end
283
300
 
@@ -295,5 +312,3 @@ module SwitchmanInstJobs
295
312
  end
296
313
  end
297
314
  end
298
-
299
- # rubocop:enable Metrics/BlockLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/ClassLength
@@ -1,3 +1,3 @@
1
1
  module SwitchmanInstJobs
2
- VERSION = '3.1.0'.freeze
2
+ VERSION = '3.2.7'.freeze
3
3
  end
@@ -14,10 +14,6 @@ module SwitchmanInstJobs
14
14
  ::Delayed::Backend::ActiveRecord::Job.prepend(
15
15
  Delayed::Backend::Base
16
16
  )
17
- ::Delayed::Backend::Redis::Job.prepend(
18
- Delayed::Backend::Base
19
- )
20
- ::Delayed::Backend::Redis::Job.column :shard_id, :integer
21
17
  ::Delayed::Pool.prepend Delayed::Pool
22
18
  ::Delayed::Worker.prepend Delayed::Worker
23
19
  ::Delayed::Worker::HealthCheck.prepend Delayed::Worker::HealthCheck
@@ -38,6 +34,7 @@ end
38
34
 
39
35
  require 'switchman_inst_jobs/active_record/connection_adapters/postgresql_adapter'
40
36
  require 'switchman_inst_jobs/active_record/migration'
37
+ require 'switchman_inst_jobs/delayed/settings'
41
38
  require 'switchman_inst_jobs/delayed/backend/base'
42
39
  require 'switchman_inst_jobs/delayed/message_sending'
43
40
  require 'switchman_inst_jobs/delayed/pool'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman-inst-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Petty
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-07 00:00:00.000000000 Z
11
+ date: 2021-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inst-jobs
@@ -16,20 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.0.3
19
+ version: 2.4.9
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '3.0'
22
+ version: '4.0'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 1.0.3
29
+ version: 2.4.9
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '3.0'
32
+ version: '4.0'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: parallel
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -107,7 +107,7 @@ dependencies:
107
107
  - !ruby/object:Gem::Version
108
108
  version: '0'
109
109
  - !ruby/object:Gem::Dependency
110
- name: imperium
110
+ name: diplomat
111
111
  requirement: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - ">="
@@ -296,6 +296,15 @@ files:
296
296
  - db/migrate/20200822014259_add_block_stranded_to_switchman_shards.rb
297
297
  - db/migrate/20200824222232_speed_up_max_concurrent_delete_trigger.rb
298
298
  - db/migrate/20200825011002_add_strand_order_override.rb
299
+ - db/migrate/20210809145804_add_n_strand_index.rb
300
+ - db/migrate/20210812210128_add_singleton_column.rb
301
+ - db/migrate/20210917232626_add_delete_conflicting_singletons_before_unlock_trigger.rb
302
+ - db/migrate/20210928174754_fix_singleton_condition_in_before_insert.rb
303
+ - db/migrate/20210929204903_update_conflicting_singleton_function_to_use_index.rb
304
+ - db/migrate/20211101190934_update_after_delete_trigger_for_singleton_index.rb
305
+ - db/migrate/20211207094200_update_after_delete_trigger_for_singleton_transition_cases.rb
306
+ - db/migrate/20211220112800_fix_singleton_race_condition_insert.rb
307
+ - db/migrate/20211220113000_fix_singleton_race_condition_delete.rb
299
308
  - lib/switchman-inst-jobs.rb
300
309
  - lib/switchman_inst_jobs.rb
301
310
  - lib/switchman_inst_jobs/active_record/connection_adapters/postgresql_adapter.rb
@@ -303,6 +312,7 @@ files:
303
312
  - lib/switchman_inst_jobs/delayed/backend/base.rb
304
313
  - lib/switchman_inst_jobs/delayed/message_sending.rb
305
314
  - lib/switchman_inst_jobs/delayed/pool.rb
315
+ - lib/switchman_inst_jobs/delayed/settings.rb
306
316
  - lib/switchman_inst_jobs/delayed/worker.rb
307
317
  - lib/switchman_inst_jobs/delayed/worker/health_check.rb
308
318
  - lib/switchman_inst_jobs/engine.rb
@@ -318,7 +328,9 @@ files:
318
328
  homepage: https://github.com/instructure/switchman-inst-jobs
319
329
  licenses:
320
330
  - MIT
321
- metadata: {}
331
+ metadata:
332
+ allowed_push_host: https://rubygems.org
333
+ rubygems_mfa_required: 'true'
322
334
  post_install_message:
323
335
  rdoc_options: []
324
336
  require_paths:
@@ -327,14 +339,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
327
339
  requirements:
328
340
  - - ">="
329
341
  - !ruby/object:Gem::Version
330
- version: '2.5'
342
+ version: '2.6'
331
343
  required_rubygems_version: !ruby/object:Gem::Requirement
332
344
  requirements:
333
345
  - - ">="
334
346
  - !ruby/object:Gem::Version
335
347
  version: '0'
336
348
  requirements: []
337
- rubygems_version: 3.0.3
349
+ rubygems_version: 3.1.4
338
350
  signing_key:
339
351
  specification_version: 4
340
352
  summary: Switchman and Instructure Jobs compatibility gem.