koyo-postgres-replication 0.1.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|