koyo-postgres-replication 0.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rspec +1 -0
- data/.rubocop.yml +23 -0
- data/.yardoc/checksums +14 -0
- data/.yardoc/complete +0 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +230 -0
- data/MIT-LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +10 -0
- data/changelog.md +14 -0
- data/koyo-postgres-replication.gemspec +43 -0
- data/lib/koyo/repl/configuration.rb +110 -0
- data/lib/koyo/repl/data.rb +52 -0
- data/lib/koyo/repl/data_row.rb +100 -0
- data/lib/koyo/repl/database.rb +183 -0
- data/lib/koyo/repl/diagnostics.rb +93 -0
- data/lib/koyo/repl/event_handler_service.rb +58 -0
- data/lib/koyo/repl/install.rb +67 -0
- data/lib/koyo/repl/log.rb +98 -0
- data/lib/koyo/repl/mod.rb +41 -0
- data/lib/koyo/repl/postgres_server.rb +184 -0
- data/lib/koyo/repl/railtie.rb +39 -0
- data/lib/koyo/repl/templates/koyo_postgres_replication_config.txt +54 -0
- data/lib/koyo/repl/templates/koyo_repl_handler_service.txt +52 -0
- data/lib/koyo/repl/templates/koyo_repl_model_example.txt +19 -0
- data/lib/koyo/repl/version.rb +7 -0
- data/lib/koyo.rb +27 -0
- data/lib/koyo_postgres_replication.rb +5 -0
- metadata +168 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koyo
|
4
|
+
module Repl
|
5
|
+
# Supports config/initializer or ENV and adds defaults
|
6
|
+
# lib/koyo/repl/templates/koyo_postges_replication_config is
|
7
|
+
# copied to config/initializers when `rake koyo:repl:install` is
|
8
|
+
# run. If you set the associated ENV it will override these settings.
|
9
|
+
class Configuration
|
10
|
+
attr_writer :auto_create_replication_slot,
|
11
|
+
:config_prefix,
|
12
|
+
:database_name,
|
13
|
+
:disable_logging,
|
14
|
+
:slot,
|
15
|
+
:sql_delay,
|
16
|
+
:test_mode
|
17
|
+
|
18
|
+
# Try to auto create replication slot if it doesn't exist
|
19
|
+
# Defaults to true
|
20
|
+
# Override with ENV['KOYO_REPL_AUTO_CREATE_REPLICATION_SLOT']
|
21
|
+
def auto_create_replication_slot
|
22
|
+
val = @auto_create_replication_slot ||
|
23
|
+
ENV["#{config_prefix}_AUTO_CREATE_REPLICATION_SLOT"] ||
|
24
|
+
'true'
|
25
|
+
Koyo::Repl::Database.to_bool(val)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Overrides the default prefix of ENV variables
|
29
|
+
# Override with ENV['KOYO_REPL_CONFIG_PREFIX']
|
30
|
+
def config_prefix
|
31
|
+
@config_prefix || ENV['KOYO_REPL_CONFIG_PREFIX'] || 'KOYO_REPL'
|
32
|
+
end
|
33
|
+
|
34
|
+
# Name of config/database.yml connection to use for replication
|
35
|
+
#
|
36
|
+
# Since this requires admin priveleges you might want to use
|
37
|
+
# a different connection to prevent all rails actions having
|
38
|
+
# admin priveleges to your DB. Default to whatever the default
|
39
|
+
# DB is for the project
|
40
|
+
# Override with ENV['KOYO_REPL_DB_CONN_NAME']
|
41
|
+
def database_name
|
42
|
+
@database_name || ENV["#{config_prefix}_DATABASE_NAME"]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Disables logging (not recommended)
|
46
|
+
# Defaults to false
|
47
|
+
# Override with ENV['KOYO_REPL_DISABLE_LOGGING']
|
48
|
+
def disable_logging
|
49
|
+
Koyo::Repl::Database.to_bool(@disable_logging ||
|
50
|
+
ENV["#{config_prefix}_DISABLE_LOGGING"])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Replication Slot name - can be any string - but must be
|
54
|
+
# unique to your database server.
|
55
|
+
#
|
56
|
+
# This is the name of the replication slot in postgres
|
57
|
+
# You can check replication slots that exist with:
|
58
|
+
#
|
59
|
+
# select slot_name
|
60
|
+
# from pg_replication_slots
|
61
|
+
# where
|
62
|
+
# and plugin = 'wal2json'
|
63
|
+
#
|
64
|
+
# Override with ENV['KOYO_REPL_SLOT']
|
65
|
+
def slot
|
66
|
+
@slot ||
|
67
|
+
ENV["#{config_prefix}_SLOT"] ||
|
68
|
+
"koyo_repl_#{Koyo::Repl::Database.current_db_name}_#{Rails.env}"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Time to wait before checking Replication Slot again in seconds
|
72
|
+
# Note: that if there 10,000 things on the replciation-queue it will
|
73
|
+
# process all of those as fast as possible, then pause for this many
|
74
|
+
# seconds before re-checking the replication-queue
|
75
|
+
# Overide with ENV['KOYO_REPL_SQL_DELAY']
|
76
|
+
def sql_delay
|
77
|
+
@sql_delay || (ENV["#{config_prefix}_SQL_DELAY"] || 1).to_i
|
78
|
+
end
|
79
|
+
|
80
|
+
# When true we only "peek" the replication slot
|
81
|
+
# Peek (when this is false):
|
82
|
+
# leaves the data on the postgres-replication queue
|
83
|
+
# Read (when this is true):
|
84
|
+
# removes data from the postgres-replication queue
|
85
|
+
# Defaults to false
|
86
|
+
# Override with ENV['KOYO_REPL_TEST_MODE']
|
87
|
+
def test_mode
|
88
|
+
val = @test_mode || ENV["#{config_prefix}_TEST_MODE"]
|
89
|
+
Koyo::Repl::Database.to_bool(val)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Helper method that converts config settings into a hash
|
93
|
+
def to_h
|
94
|
+
{
|
95
|
+
auto_create_replication_slot: auto_create_replication_slot,
|
96
|
+
config_prefix: config_prefix,
|
97
|
+
database_name: database_name,
|
98
|
+
slot: slot,
|
99
|
+
sql_delay: sql_delay,
|
100
|
+
test_mode: test_mode
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
# Helper method that converts config settings into a string
|
105
|
+
def to_s
|
106
|
+
to_h.map { |k, v| "#{k}: #{v}" }.join("\n")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koyo
|
4
|
+
module Repl
|
5
|
+
# Class wrapper for replication data pulls
|
6
|
+
# Example data packet
|
7
|
+
#
|
8
|
+
# {
|
9
|
+
# "change": [
|
10
|
+
# {
|
11
|
+
# "kind": "update",
|
12
|
+
# "schema": "public",
|
13
|
+
# "table": "users",
|
14
|
+
# "columnnames": ["id", "email", ...],
|
15
|
+
# "columntypes": ["integer", "character varying(255)", ...],
|
16
|
+
# "columnvalues": [120665, "", ...]
|
17
|
+
# "oldkeys": {
|
18
|
+
# "keynames": ["id"],
|
19
|
+
# "keytypes": ["integer"],
|
20
|
+
# "keyvalues": [120665]
|
21
|
+
# }
|
22
|
+
# }
|
23
|
+
# ]
|
24
|
+
# }
|
25
|
+
class Data
|
26
|
+
attr_accessor :row, # raw results from db
|
27
|
+
:lsn, # ???
|
28
|
+
:xid # uniq id of row returned
|
29
|
+
|
30
|
+
# Takes a row from ReplDatabase.(peek_slot/read_slot!)
|
31
|
+
# @see For details on `row` see:
|
32
|
+
# https://github.com/wiseleyb/koyo-postgres-replication/wiki/Koyo::Repl::DataRow-data-spec
|
33
|
+
def initialize(row)
|
34
|
+
@row = row
|
35
|
+
@lsn = row['lsn']
|
36
|
+
@xid = row['xid']
|
37
|
+
# TODO: find faster JSON lib for this
|
38
|
+
@data_rows = Koyo::Repl::Database.parse_json(row['data'])['change']
|
39
|
+
end
|
40
|
+
|
41
|
+
# Collection of Koyo::Repl::DataRow
|
42
|
+
#
|
43
|
+
# When you read from a replicaiton slot it can return 0-n change
|
44
|
+
# events. In general this system works on a row based level,
|
45
|
+
# which is a bit slower but is simpler to code.
|
46
|
+
def rows
|
47
|
+
# @data_rows.map { |d| ReplDataRow.new(xid, d) }
|
48
|
+
@data_rows.map { |d| Koyo::Repl::DataRow.new(d) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koyo
|
4
|
+
module Repl
|
5
|
+
# Class wrapper for replication row row
|
6
|
+
# @see
|
7
|
+
# https://github.com/wiseleyb/koyo-postgres-replication/wiki/Koyo::Repl::DataRow-row-spec
|
8
|
+
# @see For details on row
|
9
|
+
class DataRow
|
10
|
+
attr_accessor :row, # raw json of row - see example below
|
11
|
+
:kind, # insert/update/delete
|
12
|
+
:schema, # always public for this - not needed
|
13
|
+
:table, # table being changed
|
14
|
+
:id, # table.id
|
15
|
+
:id_type, # integer/uuid
|
16
|
+
:columns, # all columns from table - array
|
17
|
+
:column_types, # all types of columns - array
|
18
|
+
:values # all values from table - array
|
19
|
+
|
20
|
+
# Initialized attributes
|
21
|
+
def initialize(row)
|
22
|
+
@row = row
|
23
|
+
@kind = @row['kind']
|
24
|
+
@schema = @row['schema']
|
25
|
+
@table = @row['table']
|
26
|
+
@columns = @row['columnnames']
|
27
|
+
@column_types = @row['columntypes']
|
28
|
+
@values = @row['columnvalues']
|
29
|
+
check_set_primary_keys
|
30
|
+
end
|
31
|
+
|
32
|
+
# This doesn't work for multiple primary keys right now
|
33
|
+
#
|
34
|
+
# WARN: this breaks for multiple primary keys
|
35
|
+
def check_set_primary_keys
|
36
|
+
if @row['oldkeys']
|
37
|
+
if @row['oldkeys']['keynames'].size > 1
|
38
|
+
raise "This doesn't support multiple keys right now"
|
39
|
+
end
|
40
|
+
|
41
|
+
@id = @row['oldkeys']['keyvalues'].first
|
42
|
+
@id_type = @row['oldkeys']['keytypes'].first
|
43
|
+
else
|
44
|
+
@id = val(:id)
|
45
|
+
@id_type = type(:id)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Gets a value for a name from columnsvalues
|
50
|
+
# @param name column name
|
51
|
+
def val(name)
|
52
|
+
values[columns.index(name.to_s)]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get a val type from columntypes
|
56
|
+
# @param name column name
|
57
|
+
def type(name)
|
58
|
+
column_types[columns.index(name.to_s)]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Example row packet
|
65
|
+
#
|
66
|
+
# {
|
67
|
+
# "change": [
|
68
|
+
# {
|
69
|
+
# "kind": "delete",
|
70
|
+
# "schema": "public",
|
71
|
+
# "table": "users",
|
72
|
+
# "oldkeys": {
|
73
|
+
# "keynames": ["id"],
|
74
|
+
# "keytypes": ["integer"],
|
75
|
+
# "keyvalues": [123]
|
76
|
+
# }
|
77
|
+
# },
|
78
|
+
# {
|
79
|
+
# "kind": "insert",
|
80
|
+
# "schema": "public",
|
81
|
+
# "table": "users",
|
82
|
+
# "columnnames": ["id", "name", "email", ...],
|
83
|
+
# "columntypes": ["integer", "character varying", ...],
|
84
|
+
# "columnvalues": [234, "User", ...]
|
85
|
+
# },
|
86
|
+
# {
|
87
|
+
# "kind": "update",
|
88
|
+
# "schema": "public",
|
89
|
+
# "table": "users",
|
90
|
+
# "columnnames": ["id", "name", "email", ...],
|
91
|
+
# "columntypes": ["integer", "character varying", ...],
|
92
|
+
# "columnvalues": [234, "User", ...]
|
93
|
+
# "oldkeys": {
|
94
|
+
# "keynames": ["id"],
|
95
|
+
# "keytypes": ["integer"],
|
96
|
+
# "keyvalues": [233]
|
97
|
+
# }
|
98
|
+
# }
|
99
|
+
# ]
|
100
|
+
# }
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koyo
|
4
|
+
module Repl
|
5
|
+
# Basic utilities for postgres replication
|
6
|
+
# @see Postgres doc for more options
|
7
|
+
# https://www.postgresql.org/docs/9.4/logicaldecoding-example.html
|
8
|
+
class Database
|
9
|
+
class << self
|
10
|
+
# TODO: refactor this out of the config class
|
11
|
+
# DB connection name in config/database.yml. Defaults to Rails.env
|
12
|
+
# (so standard connection on most rails app). We add this because you need
|
13
|
+
# admin priveleges to use replication and some companies have problems with
|
14
|
+
# this. Whatever this is called it will have Rails.env tacked on so if it's
|
15
|
+
# replication - the connection would be "replciation_#{Rails.env}"
|
16
|
+
def conn
|
17
|
+
return @conn if @conn
|
18
|
+
|
19
|
+
conn_name = Koyo::Repl.config.database_name
|
20
|
+
|
21
|
+
unless conn_name
|
22
|
+
@conn = ActiveRecord::Base.connection
|
23
|
+
return @conn
|
24
|
+
end
|
25
|
+
|
26
|
+
conn_name = "#{conn_name}_#{Rails.env}"
|
27
|
+
|
28
|
+
msg = "source=KoyoReplication Connecting to #{conn_name}"
|
29
|
+
Rails.logger.info msg
|
30
|
+
|
31
|
+
config =
|
32
|
+
ApplicationRecord.configurations.find_db_config(conn_name)
|
33
|
+
ActiveRecord::Base.establish_connection config
|
34
|
+
@conn = ActiveRecord::Base.connection
|
35
|
+
@conn
|
36
|
+
end
|
37
|
+
|
38
|
+
# Reads from the replication slot.
|
39
|
+
# Reading from this marks the rows read (so you won't see them again)
|
40
|
+
# For testing you can use `peek_slot` if you want to - which will keep
|
41
|
+
# the data in the slot. A known issue is that this slot can grow so
|
42
|
+
# large that you it'll time out when trying to read from it. This is a
|
43
|
+
# major downside of this approach. Please open an issue if you know a
|
44
|
+
# solution.
|
45
|
+
def read_slot!
|
46
|
+
sql = %(
|
47
|
+
SELECT *
|
48
|
+
FROM pg_logical_slot_get_changes('#{config_slot}',
|
49
|
+
NULL,
|
50
|
+
NULL,
|
51
|
+
'pretty-print',
|
52
|
+
'1');
|
53
|
+
)
|
54
|
+
exec_sql(sql)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Peeks at data in the replication-slot. Use this for debugging. This
|
58
|
+
# will leave the data in the replication-slot. If
|
59
|
+
# Configuration.test_mode=true the code will default to peek'ing
|
60
|
+
# instead of reading.
|
61
|
+
def peek_slot
|
62
|
+
sql = %(
|
63
|
+
SELECT *
|
64
|
+
FROM pg_logical_slot_peek_changes('#{config_slot}',
|
65
|
+
NULL,
|
66
|
+
NULL,
|
67
|
+
'pretty-print',
|
68
|
+
'1');
|
69
|
+
)
|
70
|
+
exec_sql(sql)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Checks count of current slot
|
74
|
+
def replication_slot_count
|
75
|
+
sql = %(
|
76
|
+
SELECT count(*)
|
77
|
+
FROM pg_logical_slot_peek_changes('#{config_slot}',
|
78
|
+
NULL,
|
79
|
+
NULL,
|
80
|
+
'pretty-print',
|
81
|
+
'1');
|
82
|
+
)
|
83
|
+
exec_sql(sql).first['count'].to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
# Checks to see if the replication slot exists
|
87
|
+
def replication_slot_exists?
|
88
|
+
sql = %(
|
89
|
+
select count(*)
|
90
|
+
from pg_replication_slots
|
91
|
+
where
|
92
|
+
slot_name = '#{config_slot}'
|
93
|
+
and database = '#{current_db_name}'
|
94
|
+
and plugin = 'wal2json'
|
95
|
+
)
|
96
|
+
exec_sql(sql).first['count'].to_i.positive?
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns all data for current replication slot.
|
100
|
+
def replication_slot
|
101
|
+
sql = %(
|
102
|
+
select *
|
103
|
+
from pg_replication_slots
|
104
|
+
where plugin = 'wal2json'
|
105
|
+
and slot_name = '#{config_slot}'
|
106
|
+
and database = '#{current_db_name}'
|
107
|
+
)
|
108
|
+
exec_sql(sql).first
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns all replication slots on the system that support wal2json
|
112
|
+
def replication_slots
|
113
|
+
sql = %(
|
114
|
+
select *
|
115
|
+
from pg_replication_slots
|
116
|
+
where plugin = 'wal2json'
|
117
|
+
)
|
118
|
+
exec_sql(sql)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Creates a replication slot. You need admin priveleges for this.
|
122
|
+
def create_replication_slot!
|
123
|
+
return if replication_slot_exists?
|
124
|
+
|
125
|
+
sql = %(
|
126
|
+
SELECT 'init'
|
127
|
+
FROM pg_create_logical_replication_slot('#{config_slot}',
|
128
|
+
'wal2json')
|
129
|
+
)
|
130
|
+
exec_sql(sql)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Deletes replication slot. You need admin priveleges for this.
|
134
|
+
def delete_replication_slot!
|
135
|
+
return unless replication_slot_exists?
|
136
|
+
|
137
|
+
sql = %(select pg_drop_replication_slot('#{config_slot}'))
|
138
|
+
exec_sql(sql)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Drops and recreates replication slot
|
142
|
+
def drop_create_slot!
|
143
|
+
Koyo::Repl::Database.delete_replication_slot!
|
144
|
+
Koyo::Repl::Database.create_replication_slot!
|
145
|
+
end
|
146
|
+
|
147
|
+
# Checks the wal_level - which should be "logical" if things are setup
|
148
|
+
# properly. You can change the wal_level in postgres config. See the
|
149
|
+
# README for details on on this. When you change this you need to
|
150
|
+
# restart the postgres server
|
151
|
+
def wal_level
|
152
|
+
sql = %(show wal_level)
|
153
|
+
exec_sql(sql).first['wal_level']
|
154
|
+
end
|
155
|
+
|
156
|
+
# Helper - just returns the configured database name being used. Can
|
157
|
+
# be changed using Configuration.slot
|
158
|
+
def current_db_name
|
159
|
+
Rails.configuration.database_configuration[Rails.env]['database']
|
160
|
+
end
|
161
|
+
|
162
|
+
# Runs SQL commands
|
163
|
+
def exec_sql(sql)
|
164
|
+
# ActiveRecord::Base.connection.execute(sql)
|
165
|
+
conn.execute(sql)
|
166
|
+
end
|
167
|
+
|
168
|
+
# wrap this to support faster JSON parsing in the future
|
169
|
+
def parse_json(json)
|
170
|
+
JSON.parse(json)
|
171
|
+
end
|
172
|
+
|
173
|
+
def config_slot
|
174
|
+
Koyo::Repl.config.slot
|
175
|
+
end
|
176
|
+
|
177
|
+
def to_bool(val)
|
178
|
+
%w[1 true t yes].include?(val.to_s.downcase.strip)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koyo
|
4
|
+
module Repl
|
5
|
+
# Provides state and debugging info for Repl setup
|
6
|
+
# can be run with rake koyo::repl::diagnostics
|
7
|
+
class Diagnostics
|
8
|
+
# For use with rake koyo::repl::diagnostics
|
9
|
+
# Outputs repl setup and current state info
|
10
|
+
def rake_info
|
11
|
+
[
|
12
|
+
"Config settings: \n#{h_to_s(Koyo::Repl.config.to_h)}",
|
13
|
+
"Replication slot exists: #{replication_slot_exists?}",
|
14
|
+
"Registered tables: \n#{h_to_s(registered_tables)}",
|
15
|
+
"Can connect to db: #{can_connect?}",
|
16
|
+
"Connection adapter: #{adapter_name}",
|
17
|
+
"Wal Level (should be 'logical'): #{wal_level}",
|
18
|
+
"Can access replication slot: #{can_access_replication_slot?}",
|
19
|
+
"Replication slot count: #{repl_count}"
|
20
|
+
]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Checks if replication slot exists.
|
24
|
+
# TODO: maybe this should create if it doesn't?
|
25
|
+
def replication_slot_exists?
|
26
|
+
Koyo::Repl::Database.replication_slot_exists?
|
27
|
+
rescue StandardError => e
|
28
|
+
"Error: #{e.message}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns list of models that have registered a call back
|
32
|
+
def registered_tables
|
33
|
+
res =
|
34
|
+
Koyo::Repl::PostgresServer.tables_that_handle_koyo_replication || {}
|
35
|
+
if res == {}
|
36
|
+
res = {
|
37
|
+
warning: 'No tables registered - see example file in '\
|
38
|
+
'app/models/koyo_repl_model_example.rb to see '\
|
39
|
+
'how to monitor replication for a table from a model. '\
|
40
|
+
'This is optional - you can just use '\
|
41
|
+
'app/models/koyo_repl_handler_service.rb as a catch all '\
|
42
|
+
'for all replication events if you want.'
|
43
|
+
}
|
44
|
+
end
|
45
|
+
res
|
46
|
+
rescue StandardError => e
|
47
|
+
"Error: #{e.message}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Checks connection to database
|
51
|
+
def can_connect?
|
52
|
+
Koyo::Repl::Database.conn.execute('select now()')
|
53
|
+
true
|
54
|
+
rescue StandardError => e
|
55
|
+
"Error: #{e.message}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Checks access/permissions to replication slot
|
59
|
+
def can_access_replication_slot?
|
60
|
+
Koyo::Repl::Database.peek_slot
|
61
|
+
true
|
62
|
+
rescue StandardError => e
|
63
|
+
"Error: #{e.message}"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Checks that replication slot exists
|
67
|
+
def repl_count
|
68
|
+
Koyo::Repl::Database.replication_slot_count
|
69
|
+
rescue StandardError => e
|
70
|
+
"Error: #{e.message}"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns configured database name
|
74
|
+
def adapter_name
|
75
|
+
Koyo::Repl::Datasbase.conn.adapter_name
|
76
|
+
rescue StandardError => e
|
77
|
+
"Error: #{e.message}"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns configured wal_level. Should be "logical"
|
81
|
+
def wal_level
|
82
|
+
Koyo::Repl::Database.wal_level
|
83
|
+
rescue StandardError => e
|
84
|
+
"Error: #{e.message}"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Helper - outputs a hash
|
88
|
+
def h_to_s(hash)
|
89
|
+
hash.map { |k, v| " #{k}: #{v}" }.join("\n")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koyo
|
4
|
+
module Repl
|
5
|
+
# Calls back to Rails project
|
6
|
+
# This is primarily for overriding in the rails app to do things like ping
|
7
|
+
# Slack, or restart things on errors, etc
|
8
|
+
class EventHandlerService
|
9
|
+
class << self
|
10
|
+
# example row:
|
11
|
+
# User.handle_replcation called
|
12
|
+
def koyo_handle_all_replication(row)
|
13
|
+
KoyoReplHandlerService.koyo_handle_all_replication(row)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Called whenever an error is raised in Koyo::Repl code
|
17
|
+
def koyo_error(err)
|
18
|
+
KoyoReplHandlerService.koyo_error(err)
|
19
|
+
end
|
20
|
+
|
21
|
+
# log_level: :debug, :info, :warn, :error, :fatal
|
22
|
+
# Example of message
|
23
|
+
# source=KoyoReplication logid=d7f1f0bb2a
|
24
|
+
# message=Init: Finding models that support koyo_repl_handler
|
25
|
+
# You can use this as a catch all for any log event or use methods
|
26
|
+
# below if that's easier
|
27
|
+
def koyo_log_event(message, log_level)
|
28
|
+
KoyoReplHandlerService.koyo_log_event(message, log_level)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Called whenever Rails.logger.debug is called from Koyo::Repl code
|
32
|
+
def koyo_log_event_debug(message)
|
33
|
+
KoyoReplHandlerService.koyo_log_event_debug(message)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Called whenever Rails.logger.debug is called from Koyo::Repl code
|
37
|
+
def koyo_log_event_info(message)
|
38
|
+
KoyoReplHandlerService.koyo_log_event_info(message)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Called whenever Rails.logger.debug is called from Koyo::Repl code
|
42
|
+
def koyo_log_event_warn(message)
|
43
|
+
KoyoReplHandlerService.koyo_log_event_warn(message)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Called whenever Rails.logger.debug is called from Koyo::Repl code
|
47
|
+
def koyo_log_event_error(message)
|
48
|
+
KoyoReplHandlerService.koyo_log_event_error(message)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Called whenever Rails.logger.debug is called from Koyo::Repl code
|
52
|
+
def koyo_log_event_fatal(message)
|
53
|
+
KoyoReplHandlerService.koyo_log_event_fatal(message)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koyo
|
4
|
+
module Repl
|
5
|
+
# Copies required files to Rails project
|
6
|
+
class Install
|
7
|
+
def self.copy!
|
8
|
+
kri = Koyo::Repl::Install.new
|
9
|
+
kri.copy!
|
10
|
+
Koyo::Repl::Database.drop_create_slot!
|
11
|
+
end
|
12
|
+
|
13
|
+
# Copies files unless they already exist
|
14
|
+
def copy!
|
15
|
+
debugp ''
|
16
|
+
debugp '-' * 80
|
17
|
+
copy("#{template_path}/koyo_postgres_replication_config.txt",
|
18
|
+
"#{rails_path}/config/initializers/"\
|
19
|
+
'koyo_postgres_replication_config.rb')
|
20
|
+
|
21
|
+
copy("#{template_path}/koyo_repl_handler_service.txt",
|
22
|
+
"#{rails_path}/app/models/koyo_repl_handler_service.rb")
|
23
|
+
|
24
|
+
copy("#{template_path}/koyo_repl_model_example.txt",
|
25
|
+
"#{rails_path}/app/models/koyo_repl_model_example.rb")
|
26
|
+
debugp '-' * 80
|
27
|
+
end
|
28
|
+
|
29
|
+
# Debugging helper
|
30
|
+
def debugp(msg)
|
31
|
+
puts msg unless Rails.env.test? # don't pollute spec output
|
32
|
+
end
|
33
|
+
|
34
|
+
# Helper for checking if files exist
|
35
|
+
# @param fname file name (with path)
|
36
|
+
def file_exists?(fname)
|
37
|
+
if File.exist?(fname)
|
38
|
+
puts "SKIPPING: #{fname} exists. Delete this file to recreated it."
|
39
|
+
return true
|
40
|
+
end
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
# Copies individual file
|
45
|
+
# @param from_fname file to copy from
|
46
|
+
# @param to_fname file to copy to
|
47
|
+
def copy(from_fname, to_fname)
|
48
|
+
return if file_exists?(to_fname)
|
49
|
+
|
50
|
+
puts "ADDING #{to_fname}"
|
51
|
+
dir_name = File.dirname(to_fname)
|
52
|
+
FileUtils.mkdir_p(dir_name)
|
53
|
+
FileUtils.cp(from_fname, to_fname.gsub('.txt', '.rb'))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Path of template files to copy
|
57
|
+
def template_path
|
58
|
+
"#{File.dirname(__FILE__)}/templates"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Rails path helper
|
62
|
+
def rails_path
|
63
|
+
Rails.root.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|