pg_online_schema_change 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
Binary file
data/docs/load-test.md ADDED
@@ -0,0 +1,138 @@
1
+ # Preliminary Load Test
2
+
3
+ ## pg-osc: No downtime schema changes with 7K+ writes/s & 12k+ reads/s
4
+
5
+ This is a very basic load test performed with `pgbench` against a single instance PostgreSQL DB running on DigitialOcean with the following configuration:
6
+
7
+ - **128GB RAM**
8
+ - **32vCPU**
9
+ - **695GB Disk**
10
+ - Trasanction based connection pool with **500 pool limit**
11
+
12
+ Total time taken to run schema change: **<3mins**
13
+
14
+ ## Simulating load with pgbench
15
+
16
+ **Initialize**
17
+ ```
18
+ pgbench -p $PORT --initialize -s 20 -F 20 --foreign-keys --host $HOST -U $USERNAME -d $DB
19
+ ```
20
+
21
+ This creates bunch of pgbench tables. The table being used with `pg-osc` is `pgbench_accounts` which has FKs and also references by other tables with FKS, containing 2M rows.
22
+
23
+ **Begin**
24
+ ```
25
+ pgbench -p $PORT -j 72 -c 288 -T 500 -r --host $DB_HOST -U $USERNAME -d $DB
26
+ ```
27
+
28
+ ## Running pg-osc
29
+
30
+ Simple `ALTER` statement for experimentation purposes.
31
+
32
+ ```sql
33
+ ALTER TABLE pgbench_accounts ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE;
34
+ ```
35
+
36
+ **Execution**
37
+
38
+ ```bash
39
+ bundle exec bin/pg-online-schema-change perform \
40
+ -a 'ALTER TABLE pgbench_accounts ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE;' \
41
+ -d "pool" \
42
+ -p 25061
43
+ -h "..." \
44
+ -u "..." \
45
+ --pull-batch-count 2000 \
46
+ --delta-count 200
47
+ ```
48
+
49
+ ## Outcome
50
+
51
+ **pgbench results**
52
+
53
+ ```
54
+ number of transactions actually processed: 1060382
55
+ latency average = 144.874 ms
56
+ tps = 1767.057392 (including connections establishing)
57
+ tps = 1777.971823 (excluding connections establishing)
58
+ statement latencies in milliseconds:
59
+ 0.479 \set aid random(1, 100000 * :scale)
60
+ 0.409 \set bid random(1, 1 * :scale)
61
+ 0.247 \set tid random(1, 10 * :scale)
62
+ 0.208 \set delta random(-5000, 5000)
63
+ 3.136 BEGIN;
64
+ 4.243 UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
65
+ 4.488 SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
66
+ 71.017 UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
67
+ 46.689 UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;
68
+ 4.035 INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
69
+ 4.166 END;
70
+ ```
71
+
72
+ **Metrics**
73
+ ![load-test](load-test-1.png)
74
+
75
+ **New table structure**
76
+
77
+ Added `purchased` column.
78
+
79
+ ```
80
+ defaultdb=> \d+ pgbench_accounts;
81
+ Table "public.pgbench_accounts"
82
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
83
+ -----------+---------------+-----------+----------+---------+----------+--------------+-------------
84
+ aid | integer | | not null | | plain | |
85
+ bid | integer | | | | plain | |
86
+ abalance | integer | | | | plain | |
87
+ filler | character(84) | | | | extended | |
88
+ purchased | boolean | | | false | plain | |
89
+ Indexes:
90
+ "pgosc_st_pgbench_accounts_815029_pkey" PRIMARY KEY, btree (aid)
91
+ Foreign-key constraints:
92
+ "pgbench_accounts_bid_fkey" FOREIGN KEY (bid) REFERENCES pgbench_branches(bid)
93
+ Referenced by:
94
+ TABLE "pgbench_history" CONSTRAINT "pgbench_history_aid_fkey" FOREIGN KEY (aid) REFERENCES pgbench_accounts(aid)
95
+ Options: autovacuum_enabled=false, fillfactor=20
96
+ ```
97
+
98
+ **Logs**
99
+
100
+ <details>
101
+ <summary>Logs from pg-osc</summary>
102
+
103
+ ```json
104
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:01.147-05:00","v":0,"msg":"Setting up audit table","audit_table":"pgosc_at_pgbench_accounts_714a8b","version":"0.4.0"}
105
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:01.660-05:00","v":0,"msg":"Setting up triggers","version":"0.4.0"}
106
+ NOTICE: trigger "primary_to_audit_table_trigger" for relation "pgbench_accounts" does not exist, skipping
107
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:01.814-05:00","v":0,"msg":"Setting up shadow table","shadow_table":"pgosc_st_pgbench_accounts_714a8b","version":"0.4.0"}
108
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:02.169-05:00","v":0,"msg":"Running alter statement on shadow table","shadow_table":"pgosc_st_pgbench_accounts_714a8b","parent_table":"pgbench_accounts","version":"0.4.0"}
109
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:02.204-05:00","v":0,"msg":"Clearing contents of audit table before copy..","shadow_table":"pgosc_st_pgbench_accounts_714a8b","parent_table":"pgbench_accounts","version":"0.4.0"}
110
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:02.240-05:00","v":0,"msg":"Copying contents..","shadow_table":"pgosc_st_pgbench_accounts_714a8b","parent_table":"pgbench_accounts","version":"0.4.0"}
111
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:20.481-05:00","v":0,"msg":"Performing ANALYZE!","version":"0.4.0"}
112
+ INFO: analyzing "public.pgbench_accounts"
113
+ INFO: "pgbench_accounts": scanned 30000 of 166667 pages, containing 360000 live rows and 200 dead rows; 30000 rows in sample, 2000004 estimated total rows
114
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:21.078-05:00","v":0,"msg":"Replaying rows, count: 2000","version":"0.4.0"}
115
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:21.580-05:00","v":0,"msg":"Replaying rows, count: 2000","version":"0.4.0"}
116
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:22.022-05:00","v":0,"msg":"Replaying rows, count: 2000","version":"0.4.0"}
117
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:22.490-05:00","v":0,"msg":"Replaying rows, count: 2000","version":"0.4.0"}
118
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:22.866-05:00","v":0,"msg":"Replaying rows, count: 661","version":"0.4.0"}
119
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:23.212-05:00","v":0,"msg":"Replaying rows, count: 533","version":"0.4.0"}
120
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:23.512-05:00","v":0,"msg":"Replaying rows, count: 468","version":"0.4.0"}
121
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:23.809-05:00","v":0,"msg":"Remaining rows below delta count, proceeding towards swap","version":"0.4.0"}
122
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:23.809-05:00","v":0,"msg":"Performing swap!","version":"0.4.0"}
123
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:24.259-05:00","v":0,"msg":"Replaying rows, count: 449","version":"0.4.0"}
124
+ NOTICE: trigger "primary_to_audit_table_trigger" for relation "pgbench_accounts" does not exist, skipping
125
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:24.650-05:00","v":0,"msg":"Performing ANALYZE!","version":"0.4.0"}
126
+ INFO: analyzing "public.pgbench_accounts"
127
+ INFO: "pgbench_accounts": scanned 30000 of 32935 pages, containing 1821834 live rows and 6056 dead rows; 30000 rows in sample, 2000070 estimated total rows
128
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:24.941-05:00","v":0,"msg":"Validating constraints!","version":"0.4.0"}
129
+ NOTICE: table "pgosc_st_pgbench_accounts_714a8b" does not exist, skipping
130
+ {"name":"pg-online-schema-change","hostname":"MacBook-Pro.local","pid":13263,"level":30,"time":"2022-02-25T17:22:26.159-05:00","v":0,"msg":"All tasks successfully completed","version":"0.4.0"}
131
+ ```
132
+
133
+ </details>
134
+
135
+
136
+ ## Conclusion
137
+
138
+ By tweaking `--pull-batch-count` to `2000` (replay 2k rows at once) and `--delta-count` to `200` (time to swap when remaining rows is <200), `pg-osc` was able to perform the schema change with no impact within very quick time. Depending on the database size and load on the table, you can further tune them to achieve desired impact. At some point this is going to plateau - I can imagine the replay factor not working quite well for say 100k commits/s workloads. So, YMMV.
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "thor"
2
4
 
3
5
  module PgOnlineSchemaChange
6
+ PULL_BATCH_COUNT = 1000
7
+ DELTA_COUNT = 20
4
8
  class CLI < Thor
5
- desc "perform", "Perform the set of operations to safely apply the schema change with minimal locks"
9
+ desc "perform", "Safely apply schema changes with minimal locks"
6
10
  method_option :alter_statement, aliases: "-a", type: :string, required: true,
7
11
  desc: "The ALTER statement to perform the schema change"
8
12
  method_option :schema, aliases: "-s", type: :string, required: true, default: "public",
@@ -11,7 +15,7 @@ module PgOnlineSchemaChange
11
15
  method_option :host, aliases: "-h", type: :string, required: true, desc: "Server host where the Database is located"
12
16
  method_option :username, aliases: "-u", type: :string, required: true, desc: "Username for the Database"
13
17
  method_option :port, aliases: "-p", type: :numeric, required: true, default: 5432, desc: "Port for the Database"
14
- method_option :password, aliases: "-w", type: :string, required: true, desc: "Password for the Database"
18
+ method_option :password, aliases: "-w", type: :string, required: true, desc: "DEPRECATED: Password for the Database. Please pass PGPASSWORD environment variable instead."
15
19
  method_option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Emit logs in debug mode"
16
20
  method_option :drop, aliases: "-f", type: :boolean, default: false,
17
21
  desc: "Drop the original table in the end after the swap"
@@ -21,11 +25,19 @@ module PgOnlineSchemaChange
21
25
  desc: "Time to wait before killing backends to acquire lock and/or retrying upto 3 times. It will kill backends if --kill-backends is true, otherwise try upto 3 times and exit if it cannot acquire a lock."
22
26
  method_option :copy_statement, aliases: "-c", type: :string, required: false, default: "",
23
27
  desc: "Takes a .sql file location where you can provide a custom query to be played (ex: backfills) when pgosc copies data from the primary to the shadow table. More examples in README."
28
+ method_option :pull_batch_count, aliases: "-b", type: :numeric, required: false, default: PULL_BATCH_COUNT,
29
+ desc: "Number of rows to be replayed on each iteration after copy. This can be tuned for faster catch up and swap. Best used with delta-count."
30
+ method_option :delta_count, aliases: "-e", type: :numeric, required: false, default: DELTA_COUNT,
31
+ desc: "Indicates how many rows should be remaining before a swap should be performed. This can be tuned for faster catch up and swap, especially on highly volume tables. Best used with pull-batch-count."
24
32
 
25
33
  def perform
26
34
  client_options = Struct.new(*options.keys.map(&:to_sym)).new(*options.values)
35
+ PgOnlineSchemaChange.logger(verbose: client_options.verbose)
36
+
37
+ PgOnlineSchemaChange.logger.warn("DEPRECATED: -w is deprecated. Please pass PGPASSWORD environment variable instead.") if client_options.password
38
+
39
+ client_options.password = ENV["PGPASSWORD"] || client_options.password
27
40
 
28
- PgOnlineSchemaChange.logger = client_options.verbose
29
41
  PgOnlineSchemaChange::Orchestrate.run!(client_options)
30
42
  end
31
43
 
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg"
2
4
 
3
5
  module PgOnlineSchemaChange
4
6
  class Client
5
7
  attr_accessor :alter_statement, :schema, :dbname, :host, :username, :port, :password, :connection, :table, :drop,
6
- :kill_backends, :wait_time_for_lock, :copy_statement
8
+ :kill_backends, :wait_time_for_lock, :copy_statement, :pull_batch_count, :delta_count
7
9
 
8
10
  def initialize(options)
9
11
  @alter_statement = options.alter_statement
@@ -16,7 +18,11 @@ module PgOnlineSchemaChange
16
18
  @drop = options.drop
17
19
  @kill_backends = options.kill_backends
18
20
  @wait_time_for_lock = options.wait_time_for_lock
21
+ @pull_batch_count = options.pull_batch_count
22
+ @delta_count = options.delta_count
23
+
19
24
  handle_copy_statement(options.copy_statement)
25
+ handle_validations
20
26
 
21
27
  @connection = PG.connect(
22
28
  dbname: @dbname,
@@ -26,17 +32,19 @@ module PgOnlineSchemaChange
26
32
  port: @port,
27
33
  )
28
34
 
29
- raise Error, "Not a valid ALTER statement: #{@alter_statement}" unless Query.alter_statement?(@alter_statement)
30
-
31
- unless Query.same_table?(@alter_statement)
32
- raise Error "All statements should belong to the same table: #{@alter_statement}"
33
- end
34
-
35
35
  @table = Query.table(@alter_statement)
36
36
 
37
37
  PgOnlineSchemaChange.logger.debug("Connection established")
38
38
  end
39
39
 
40
+ def handle_validations
41
+ raise Error, "Not a valid ALTER statement: #{@alter_statement}" unless Query.alter_statement?(@alter_statement)
42
+
43
+ return if Query.same_table?(@alter_statement)
44
+
45
+ raise Error "All statements should belong to the same table: #{@alter_statement}"
46
+ end
47
+
40
48
  def handle_copy_statement(statement)
41
49
  return if statement.nil? || statement == ""
42
50
 
@@ -1,4 +1,6 @@
1
- FUNC_FIX_SERIAL_SEQUENCE = <<~SQL.freeze
1
+ # frozen_string_literal: true
2
+
3
+ FUNC_FIX_SERIAL_SEQUENCE = <<~SQL
2
4
  CREATE OR REPLACE FUNCTION fix_serial_sequence(_table regclass, _newtable text)
3
5
  RETURNS void AS
4
6
  $func$
@@ -35,7 +37,7 @@ FUNC_FIX_SERIAL_SEQUENCE = <<~SQL.freeze
35
37
  $func$ LANGUAGE plpgsql VOLATILE;
36
38
  SQL
37
39
 
38
- FUNC_CREATE_TABLE_ALL = <<~SQL.freeze
40
+ FUNC_CREATE_TABLE_ALL = <<~SQL
39
41
  CREATE OR REPLACE FUNCTION create_table_all(source_table text, newsource_table text)
40
42
  RETURNS void language plpgsql
41
43
  as $$
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PgOnlineSchemaChange
2
4
  module Helper
3
5
  def primary_key
@@ -15,7 +17,14 @@ module PgOnlineSchemaChange
15
17
  result = Store.send(:get, method)
16
18
  return result if result
17
19
 
18
- raise ArgumentError, "Method `#{method}` doesn't exist."
20
+ super
21
+ end
22
+
23
+ def respond_to_missing?(method_name, *args)
24
+ result = Store.send(:get, method)
25
+ return true if result
26
+
27
+ super
19
28
  end
20
29
  end
21
30
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
 
3
5
  module PgOnlineSchemaChange
4
6
  class Orchestrate
5
- SWAP_STATEMENT_TIMEOUT = "5s".freeze
7
+ SWAP_STATEMENT_TIMEOUT = "5s"
6
8
 
7
9
  extend Helper
8
10
 
@@ -21,12 +23,20 @@ module PgOnlineSchemaChange
21
23
  Query.run(client.connection, FUNC_FIX_SERIAL_SEQUENCE)
22
24
  Query.run(client.connection, FUNC_CREATE_TABLE_ALL)
23
25
 
26
+ setup_store
27
+ end
28
+
29
+ def setup_store
24
30
  # Set this early on to ensure their creation and cleanup (unexpected)
25
31
  # happens at all times. IOW, the calls from Store.get always return
26
32
  # the same value.
27
33
  Store.set(:old_primary_table, "pgosc_op_table_#{client.table}")
28
- Store.set(:audit_table, "pgosc_at_#{client.table}_#{random_string}")
29
- Store.set(:shadow_table, "pgosc_st_#{client.table}_#{random_string}")
34
+ Store.set(:audit_table, "pgosc_at_#{client.table}_#{pgosc_identifier}")
35
+ Store.set(:operation_type_column, "operation_type_#{pgosc_identifier}")
36
+ Store.set(:trigger_time_column, "trigger_time_#{pgosc_identifier}")
37
+ Store.set(:audit_table_pk, "at_#{pgosc_identifier}_id")
38
+ Store.set(:audit_table_pk_sequence, "#{audit_table}_#{audit_table_pk}_seq")
39
+ Store.set(:shadow_table, "pgosc_st_#{client.table}_#{pgosc_identifier}")
30
40
  end
31
41
 
32
42
  def run!(options)
@@ -70,7 +80,7 @@ module PgOnlineSchemaChange
70
80
  reader = setup_signals!
71
81
  signal = reader.gets.chomp
72
82
 
73
- while !reader.closed? && IO.select([reader])
83
+ while !reader.closed? && IO.select([reader]) # rubocop:disable Lint/UnreachableLoop
74
84
  logger.info "Signal #{signal} received, cleaning up"
75
85
 
76
86
  client.connection.cancel
@@ -85,7 +95,7 @@ module PgOnlineSchemaChange
85
95
  logger.info("Setting up audit table", { audit_table: audit_table })
86
96
 
87
97
  sql = <<~SQL
88
- CREATE TABLE #{audit_table} (operation_type text, trigger_time timestamp, LIKE #{client.table});
98
+ CREATE TABLE #{audit_table} (#{audit_table_pk} SERIAL PRIMARY KEY, #{operation_type_column} text, #{trigger_time_column} timestamp, LIKE #{client.table});
89
99
  SQL
90
100
 
91
101
  Query.run(client.connection, sql)
@@ -109,13 +119,13 @@ module PgOnlineSchemaChange
109
119
  $$
110
120
  BEGIN
111
121
  IF ( TG_OP = 'INSERT') THEN
112
- INSERT INTO \"#{audit_table}\" select 'INSERT', now(), NEW.* ;
122
+ INSERT INTO \"#{audit_table}\" select nextval(\'#{audit_table_pk_sequence}\'), 'INSERT', clock_timestamp(), NEW.* ;
113
123
  RETURN NEW;
114
124
  ELSIF ( TG_OP = 'UPDATE') THEN
115
- INSERT INTO \"#{audit_table}\" select 'UPDATE', now(), NEW.* ;
125
+ INSERT INTO \"#{audit_table}\" select nextval(\'#{audit_table_pk_sequence}\'), 'UPDATE', clock_timestamp(), NEW.* ;
116
126
  RETURN NEW;
117
127
  ELSIF ( TG_OP = 'DELETE') THEN
118
- INSERT INTO \"#{audit_table}\" select 'DELETE', now(), OLD.* ;
128
+ INSERT INTO \"#{audit_table}\" select nextval(\'#{audit_table_pk_sequence}\'), 'DELETE', clock_timestamp(), OLD.* ;
119
129
  RETURN NEW;
120
130
  END IF;
121
131
  END;
@@ -153,7 +163,7 @@ module PgOnlineSchemaChange
153
163
  # re-uses transaction with serializable
154
164
  # Disabling vacuum to avoid any issues during the process
155
165
  result = Query.storage_parameters_for(client, client.table, true) || ""
156
- primary_table_storage_parameters = Store.set(:primary_table_storage_parameters, result)
166
+ Store.set(:primary_table_storage_parameters, result)
157
167
 
158
168
  logger.debug("Disabling vacuum on shadow and audit table",
159
169
  { shadow_table: shadow_table, audit_table: audit_table })
@@ -185,8 +195,7 @@ module PgOnlineSchemaChange
185
195
  # Begin the process to copy data into copy table
186
196
  # depending on the size of the table, this can be a time
187
197
  # taking operation.
188
- logger.info("Clearing contents of audit table before copy..",
189
- { shadow_table: shadow_table, parent_table: client.table })
198
+ logger.info("Clearing contents of audit table before copy..", { shadow_table: shadow_table, parent_table: client.table })
190
199
  Query.run(client.connection, "DELETE FROM #{audit_table}", true)
191
200
 
192
201
  logger.info("Copying contents..", { shadow_table: shadow_table, parent_table: client.table })
@@ -195,7 +204,7 @@ module PgOnlineSchemaChange
195
204
  return Query.run(client.connection, query, true)
196
205
  end
197
206
 
198
- sql = Query.copy_data_statement(client, shadow_table)
207
+ sql = Query.copy_data_statement(client, shadow_table, true)
199
208
  Query.run(client.connection, sql, true)
200
209
  ensure
201
210
  Query.run(client.connection, "COMMIT;") # commit the serializable transaction
@@ -272,8 +281,10 @@ module PgOnlineSchemaChange
272
281
  Query.run(client.connection, sql)
273
282
  end
274
283
 
275
- private def random_string
276
- @random_string ||= SecureRandom.hex(3)
284
+ private
285
+
286
+ def pgosc_identifier
287
+ @pgosc_identifier ||= SecureRandom.hex(3)
277
288
  end
278
289
  end
279
290
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_query"
2
4
  require "pg"
3
5
 
@@ -5,7 +7,7 @@ module PgOnlineSchemaChange
5
7
  class Query
6
8
  extend Helper
7
9
 
8
- INDEX_SUFFIX = "_pgosc".freeze
10
+ INDEX_SUFFIX = "_pgosc"
9
11
  DROPPED_COLUMN_TYPE = :AT_DropColumn
10
12
  RENAMED_COLUMN_TYPE = :AT_RenameColumn
11
13
  LOCK_ATTEMPT = 4
@@ -15,28 +17,28 @@ module PgOnlineSchemaChange
15
17
  PgQuery.parse(query).tree.stmts.all? do |statement|
16
18
  statement.stmt.alter_table_stmt.instance_of?(PgQuery::AlterTableStmt) || statement.stmt.rename_stmt.instance_of?(PgQuery::RenameStmt)
17
19
  end
18
- rescue PgQuery::ParseError => e
20
+ rescue PgQuery::ParseError
19
21
  false
20
22
  end
21
23
 
22
24
  def same_table?(query)
23
- tables = PgQuery.parse(query).tree.stmts.map do |statement|
25
+ tables = PgQuery.parse(query).tree.stmts.filter_map do |statement|
24
26
  if statement.stmt.alter_table_stmt.instance_of?(PgQuery::AlterTableStmt)
25
27
  statement.stmt.alter_table_stmt.relation.relname
26
28
  elsif statement.stmt.rename_stmt.instance_of?(PgQuery::RenameStmt)
27
29
  statement.stmt.rename_stmt.relation.relname
28
30
  end
29
- end.compact
31
+ end
30
32
 
31
33
  tables.uniq.count == 1
32
- rescue PgQuery::ParseError => e
34
+ rescue PgQuery::ParseError
33
35
  false
34
36
  end
35
37
 
36
38
  def table(query)
37
- from_rename_statement = PgQuery.parse(query).tree.stmts.map do |statement|
39
+ from_rename_statement = PgQuery.parse(query).tree.stmts.filter_map do |statement|
38
40
  statement.stmt.rename_stmt&.relation&.relname
39
- end.compact[0]
41
+ end[0]
40
42
  PgQuery.parse(query).tables[0] || from_rename_statement
41
43
  end
42
44
 
@@ -48,7 +50,7 @@ module PgOnlineSchemaChange
48
50
  connection.async_exec("BEGIN;")
49
51
 
50
52
  result = connection.async_exec(query, &block)
51
- rescue Exception
53
+ rescue Exception # rubocop:disable Lint/RescueException
52
54
  connection.cancel if connection.transaction_status != PG::PQTRANS_IDLE
53
55
  connection.block
54
56
  logger.info("Exception raised, rolling back query", { rollback: true, query: query })
@@ -144,11 +146,11 @@ module PgOnlineSchemaChange
144
146
  end
145
147
 
146
148
  references.map do |row|
147
- if row["definition"].end_with?("NOT VALID")
148
- add_statement = "ALTER TABLE #{row["table_on"]} ADD CONSTRAINT #{row["constraint_name"]} #{row["definition"]};"
149
- else
150
- add_statement = "ALTER TABLE #{row["table_on"]} ADD CONSTRAINT #{row["constraint_name"]} #{row["definition"]} NOT VALID;"
151
- end
149
+ add_statement = if row["definition"].end_with?("NOT VALID")
150
+ "ALTER TABLE #{row["table_on"]} ADD CONSTRAINT #{row["constraint_name"]} #{row["definition"]};"
151
+ else
152
+ "ALTER TABLE #{row["table_on"]} ADD CONSTRAINT #{row["constraint_name"]} #{row["definition"]} NOT VALID;"
153
+ end
152
154
 
153
155
  drop_statement = "ALTER TABLE #{row["table_on"]} DROP CONSTRAINT #{row["constraint_name"]};"
154
156
 
@@ -291,7 +293,7 @@ module PgOnlineSchemaChange
291
293
  client.connection.quote_ident(select_column)
292
294
  end
293
295
 
294
- sql = <<~SQL
296
+ <<~SQL
295
297
  INSERT INTO #{shadow_table}(#{insert_into_columns.join(", ")})
296
298
  SELECT #{select_columns.join(", ")}
297
299
  FROM ONLY #{client.table}
@@ -1,12 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
4
+
1
5
  module PgOnlineSchemaChange
2
6
  class Replay
3
7
  extend Helper
4
8
 
5
9
  class << self
6
- PULL_BATCH_COUNT = 1000
7
- DELTA_COUNT = 20
8
- RESERVED_COLUMNS = %w[operation_type trigger_time].freeze
9
-
10
10
  # This, picks PULL_BATCH_COUNT rows by primary key from audit_table,
11
11
  # replays it on the shadow_table. Once the batch is done,
12
12
  # it them deletes those PULL_BATCH_COUNT rows from audit_table. Then, pull another batch,
@@ -17,7 +17,7 @@ module PgOnlineSchemaChange
17
17
  loop do
18
18
  rows = rows_to_play
19
19
 
20
- raise CountBelowDelta if rows.count <= DELTA_COUNT
20
+ raise CountBelowDelta if rows.count <= client.delta_count
21
21
 
22
22
  play!(rows)
23
23
  end
@@ -25,7 +25,7 @@ module PgOnlineSchemaChange
25
25
 
26
26
  def rows_to_play(reuse_trasaction = false)
27
27
  select_query = <<~SQL
28
- SELECT * FROM #{audit_table} ORDER BY #{primary_key} LIMIT #{PULL_BATCH_COUNT};
28
+ SELECT * FROM #{audit_table} ORDER BY #{audit_table_pk} LIMIT #{client.pull_batch_count};
29
29
  SQL
30
30
 
31
31
  rows = []
@@ -34,6 +34,10 @@ module PgOnlineSchemaChange
34
34
  rows
35
35
  end
36
36
 
37
+ def reserved_columns
38
+ @reserved_columns ||= [trigger_time_column, operation_type_column, audit_table_pk]
39
+ end
40
+
37
41
  def play!(rows, reuse_trasaction = false)
38
42
  logger.info("Replaying rows, count: #{rows.size}")
39
43
 
@@ -44,7 +48,7 @@ module PgOnlineSchemaChange
44
48
 
45
49
  # Remove audit table cols, since we will be
46
50
  # re-mapping them for inserts and updates
47
- RESERVED_COLUMNS.each do |col|
51
+ reserved_columns.each do |col|
48
52
  new_row.delete(col)
49
53
  end
50
54
 
@@ -73,7 +77,7 @@ module PgOnlineSchemaChange
73
77
  client.connection.escape_string(value)
74
78
  end
75
79
 
76
- case row["operation_type"]
80
+ case row[operation_type_column]
77
81
  when "INSERT"
78
82
  values = new_row.map { |_, val| "'#{val}'" }.join(",")
79
83
 
@@ -110,13 +114,15 @@ module PgOnlineSchemaChange
110
114
  Query.run(client.connection, to_be_replayed.join, reuse_trasaction)
111
115
 
112
116
  # Delete items from the audit now that are replayed
113
- if to_be_deleted_rows.count >= 1
114
- delete_query = <<~SQL
115
- DELETE FROM #{audit_table} WHERE #{primary_key} IN (#{to_be_deleted_rows.join(",")})
116
- SQL
117
- Query.run(client.connection, delete_query, reuse_trasaction)
118
- end
117
+ return unless to_be_deleted_rows.count >= 1
118
+
119
+ delete_query = <<~SQL
120
+ DELETE FROM #{audit_table} WHERE #{primary_key} IN (#{to_be_deleted_rows.join(",")})
121
+ SQL
122
+ Query.run(client.connection, delete_query, reuse_trasaction)
119
123
  end
120
124
  end
121
125
  end
122
126
  end
127
+
128
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -1,17 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pg_query"
2
4
  require "pg"
3
5
 
4
6
  module PgOnlineSchemaChange
5
7
  class Store
6
8
  class << self
7
- @@object = {}
9
+ @object = {}
8
10
 
9
11
  def get(key)
10
- @@object[key.to_s] || @@object[key.to_sym]
12
+ @object ||= {}
13
+ @object[key.to_s] || @object[key.to_sym]
11
14
  end
12
15
 
13
16
  def set(key, value)
14
- @@object[key.to_sym] = value
17
+ @object ||= {}
18
+ @object[key.to_sym] = value
15
19
  end
16
20
  end
17
21
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgOnlineSchemaChange
4
- VERSION = "0.2.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -6,28 +6,24 @@ require "ougai"
6
6
  require "pg_online_schema_change/version"
7
7
  require "pg_online_schema_change/helper"
8
8
  require "pg_online_schema_change/functions"
9
- require "pg_online_schema_change/cli"
10
9
  require "pg_online_schema_change/client"
11
10
  require "pg_online_schema_change/query"
12
11
  require "pg_online_schema_change/store"
13
12
  require "pg_online_schema_change/replay"
14
13
  require "pg_online_schema_change/orchestrate"
14
+ require "pg_online_schema_change/cli"
15
15
 
16
16
  module PgOnlineSchemaChange
17
17
  class Error < StandardError; end
18
18
  class CountBelowDelta < StandardError; end
19
19
  class AccessExclusiveLockNotAcquired < StandardError; end
20
20
 
21
- def self.logger=(verbose)
22
- @@logger ||= begin
23
- logger = Ougai::Logger.new($stdout)
24
- logger.level = verbose ? Ougai::Logger::TRACE : Ougai::Logger::INFO
25
- logger.with_fields = { version: PgOnlineSchemaChange::VERSION }
26
- logger
27
- end
28
- end
29
-
30
- def self.logger
31
- @@logger
21
+ def self.logger(verbose: false)
22
+ @logger ||= begin
23
+ logger = Ougai::Logger.new($stdout)
24
+ logger.level = verbose ? Ougai::Logger::TRACE : Ougai::Logger::INFO
25
+ logger.with_fields = { version: PgOnlineSchemaChange::VERSION }
26
+ logger
27
+ end
32
28
  end
33
29
  end
@@ -0,0 +1,28 @@
1
+ export VERSION=$1
2
+ echo "VERSION: ${VERSION}"
3
+
4
+ echo "=== Pushing tags to github ===="
5
+ git tag v$VERSION
6
+ git push origin --tags
7
+
8
+ echo "=== Building Gem ===="
9
+ gem build pg_online_schema_change.gemspec
10
+
11
+ echo "=== Pushing gem ===="
12
+ gem push pg_online_schema_change-$VERSION.gem
13
+
14
+ echo "=== Sleeping for 5s ===="
15
+ sleep 5
16
+
17
+ echo "=== Building Image ===="
18
+ docker build . --build-arg VERSION=$VERSION -t shayonj/pg-osc:$VERSION
19
+
20
+ echo "=== Tagging Image ===="
21
+ docker image tag shayonj/pg-osc:$VERSION shayonj/pg-osc:latest
22
+
23
+ echo "=== Pushing Image ===="
24
+ docker push shayonj/pg-osc:$VERSION
25
+ docker push shayonj/pg-osc:latest
26
+
27
+ echo "=== Cleaning up ===="
28
+ rm pg_online_schema_change-$VERSION.gem