active_record_proxy_adapters 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,77 @@
1
+ name: active_record_proxy_adapters
2
+
3
+ x-postgres-common: &postgres-common
4
+ restart: always
5
+ user: postgres
6
+ healthcheck:
7
+ test: 'pg_isready -U postgres_primary_test --dbname=postgres'
8
+ interval: 10s
9
+ timeout: 5s
10
+ retries: 5
11
+ networks:
12
+ - postgres
13
+
14
+ services:
15
+ app:
16
+ build:
17
+ args:
18
+ - RUBY_VERSION=${RUBY_VERSION:-3.2.3}
19
+ - RAILS_VERSION=${RAILS_VERSION:-~> 6.1.0}
20
+ container_name: app
21
+ image: active_record_proxy_adapters-app:${ENV_TAG:-latest}
22
+ tty: true
23
+ stdin_open: true
24
+ environment:
25
+ PGHOST: postgres_primary
26
+ PG_PRIMARY_USER: postgres_primary_test
27
+ PG_PRIMARY_PASSWORD: postgres_primary_test
28
+ PG_PRIMARY_HOST: postgres_primary
29
+ PG_PRIMARY_PORT: 5432
30
+ PG_REPLICA_USER: postgres_primary_test
31
+ PG_REPLICA_PASSWORD: postgres_primary_test
32
+ PG_REPLICA_HOST: postgres_replica
33
+ PG_REPLICA_PORT: 5432
34
+ depends_on:
35
+ - postgres_primary
36
+ - postgres_replica
37
+ networks:
38
+ - app
39
+ - postgres
40
+ volumes:
41
+ - .:/app
42
+ postgres_primary:
43
+ <<: *postgres-common
44
+ build:
45
+ context: .
46
+ dockerfile: postgres_primary.dockerfile
47
+ args:
48
+ - POSTGRES_LOGGING_COLLECTOR=${POSTGRES_LOGGING_COLLECTOR:-}
49
+ - POSTGRES_LOG_DESTINATION=${POSTGRES_LOG_DESTINATION:-}
50
+ - POSTGRES_LOG_STATEMENT=${POSTGRES_LOG_STATEMENT:-}
51
+ - REPLICA_USER=replicator
52
+ - REPLICA_PASSWORD=replicator
53
+ environment:
54
+ POSTGRES_DB: postgres
55
+ POSTGRES_USER: postgres_primary_test
56
+ POSTGRES_PASSWORD: postgres_primary_test
57
+ POSTGRES_HOST_AUTH_METHOD: "scram-sha-256\nhost replication all 0.0.0.0/0 md5"
58
+ POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
59
+
60
+ postgres_replica:
61
+ <<: *postgres-common
62
+ build:
63
+ context: .
64
+ dockerfile: postgres_replica.dockerfile
65
+ container_name: postgres_replica
66
+ environment:
67
+ PGUSER: replicator
68
+ PGPASSWORD: replicator
69
+ PRIMARY_DATABASE_HOST: postgres_primary
70
+ depends_on:
71
+ - postgres_primary
72
+ networks:
73
+ app:
74
+ postgres:
75
+
76
+ volumes:
77
+ postgres_primary:
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/tasks/postgresql_proxy_database_tasks"
4
+ require "active_record/connection_adapters/postgresql_adapter"
5
+ require "active_record_proxy_adapters/active_record_context"
6
+ require "active_record_proxy_adapters/hijackable"
7
+ require "active_record_proxy_adapters/postgresql_proxy"
8
+
9
+ module ActiveRecord
10
+ module ConnectionAdapters
11
+ # This adapter is a proxy to the original PostgreSQLAdapter, allowing the use of the
12
+ # ActiveRecordProxyAdapters::PrimaryReplicaProxy.
13
+ class PostgreSQLProxyAdapter < PostgreSQLAdapter
14
+ include ActiveRecordProxyAdapters::Hijackable
15
+
16
+ ADAPTER_NAME = "PostgreSQLProxy"
17
+
18
+ delegate_to_proxy :execute, :exec_query
19
+
20
+ unless ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v8_0_or_greater?
21
+ delegate_to_proxy :exec_no_cache, :exec_cache
22
+ end
23
+
24
+ def initialize(...)
25
+ @proxy = ActiveRecordProxyAdapters::PostgreSQLProxy.new(self)
26
+
27
+ super
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :proxy
33
+ end
34
+ end
35
+ end
36
+
37
+ if ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v7_2_or_greater?
38
+ ActiveRecord::ConnectionAdapters.register(
39
+ "postgresql_proxy",
40
+ "ActiveRecord::ConnectionAdapters::PostgreSQLProxyAdapter",
41
+ "active_record/connection_adapters/postgresql_proxy_adapter"
42
+ )
43
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Tasks
5
+ # Defines the postgresql tasks for dropping, creating, loading schema and dumping schema.
6
+ # Bypasses all the proxy logic to send all requests to primary.
7
+ class PostgreSQLProxyDatabaseTasks < PostgreSQLDatabaseTasks
8
+ def create(...)
9
+ sticking_to_primary { super }
10
+ end
11
+
12
+ def drop(...)
13
+ sticking_to_primary { super }
14
+ end
15
+
16
+ def structure_dump(...)
17
+ sticking_to_primary { super }
18
+ end
19
+
20
+ def structure_load(...)
21
+ sticking_to_primary { super }
22
+ end
23
+
24
+ def purge(...)
25
+ sticking_to_primary { super }
26
+ end
27
+
28
+ private
29
+
30
+ def sticking_to_primary(&)
31
+ ActiveRecord::Base.connected_to(role: context.writing_role, &)
32
+ end
33
+
34
+ def context
35
+ ActiveRecordProxyAdapters::ActiveRecordContext.new
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # Allow proxy adapter to run rake tasks, i.e. db:drop, db:create, db:schema:load db:migrate, etc...
42
+ ActiveRecord::Tasks::DatabaseTasks.register_task(
43
+ /postgresql_proxy/,
44
+ "ActiveRecord::Tasks::PostgreSQLProxyDatabaseTasks"
45
+ )
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordProxyAdapters
4
+ # Collection of helpers to handle common active record methods that are defined in different places in different
5
+ # versions of rails.
6
+ class ActiveRecordContext
7
+ delegate :reading_role, :reading_role=, :writing_role, :writing_role=, to: :ActiveRecord
8
+ delegate :legacy_connection_handling, :legacy_connection_handling=, to: :connection_handling_context
9
+ delegate :version, to: :ActiveRecord, prefix: :active_record
10
+
11
+ class << self
12
+ delegate_missing_to :new
13
+ end
14
+
15
+ NullConnectionHandlingContext = Class.new do
16
+ def legacy_connection_handling
17
+ false
18
+ end
19
+
20
+ def legacy_connection_handling=(_value)
21
+ nil
22
+ end
23
+ end
24
+
25
+ def connection_class_for(connection)
26
+ connection.connection_class || ActiveRecord::Base
27
+ end
28
+
29
+ def connection_handling_context
30
+ # This config option has been removed in Rails 7.1+
31
+ return NullConnectionHandlingContext.new if active_record_v7_1_or_greater?
32
+
33
+ ActiveRecord
34
+ end
35
+
36
+ def active_record_v7_1_or_greater?
37
+ active_record_version >= Gem::Version.new("7.1")
38
+ end
39
+
40
+ def active_record_v7_2_or_greater?
41
+ active_record_version >= Gem::Version.new("7.2")
42
+ end
43
+
44
+ def active_record_v8_0_or_greater?
45
+ active_record_version >= Gem::Version.new("8.0")
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/integer/time"
4
+
5
+ module ActiveRecordProxyAdapters
6
+ # Provides a global configuration object to configure how the proxy should behave.
7
+ class Configuration
8
+ PROXY_DELAY = 2.seconds.freeze
9
+ CHECKOUT_TIMEOUT = 2.seconds.freeze
10
+
11
+ # @return [ActiveSupport::Duration] How long the proxy should reroute all read requests to the primary database
12
+ # since the latest write. Defaults to PROXY_DELAY.
13
+ attr_accessor :proxy_delay
14
+ # @return [ActiveSupport::Duration] How long the proxy should wait for a connection from the replica pool.
15
+ # Defaults to CHECKOUT_TIMEOUT.
16
+ attr_accessor :checkout_timeout
17
+
18
+ def initialize
19
+ self.proxy_delay = PROXY_DELAY
20
+ self.checkout_timeout = CHECKOUT_TIMEOUT
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/postgresql_proxy_adapter"
4
+
5
+ module ActiveRecordProxyAdapters
6
+ # Module to extend ActiveRecord::Base with the connection handling methods.
7
+ # Required to make adapter work in ActiveRecord versions <= 7.2.x
8
+ module ConnectionHandling
9
+ def postgresql_proxy_adapter_class
10
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLProxyAdapter
11
+ end
12
+
13
+ # This method is a copy and paste from Rails' postgresql_connection,
14
+ # replacing PostgreSQLAdapter by PostgreSQLProxyAdapter
15
+ # This is required by ActiveRecord versions <= 7.2.x to establish a connection using the adapter.
16
+ def postgresql_proxy_connection(config) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
17
+ conn_params = config.symbolize_keys.compact
18
+
19
+ # Map ActiveRecords param names to PGs.
20
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
21
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
22
+
23
+ # Forward only valid config params to PG::Connection.connect.
24
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
25
+ conn_params.slice!(*valid_conn_param_keys)
26
+
27
+ postgresql_proxy_adapter_class.new(
28
+ postgresql_proxy_adapter_class.new_client(conn_params),
29
+ logger,
30
+ conn_params,
31
+ config
32
+ )
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/tasks/postgresql_proxy_database_tasks"
4
+ require "active_record/connection_adapters/postgresql_adapter"
5
+ require "active_record_proxy_adapters/primary_replica_proxy"
6
+
7
+ module ActiveRecordProxyAdapters
8
+ # Defines mixins to delegate specific methods from the proxy to the adapter.
9
+ module Hijackable
10
+ extend ActiveSupport::Concern
11
+
12
+ class_methods do
13
+ # Renames the methods from the original Adapter using the proxy suffix (_unproxied)
14
+ # and delegate the original method name to the proxy.
15
+ # Example: delegate_to_proxy(:execute) creates a method `execute_unproxied`,
16
+ # while delegating :execute to the proxy.
17
+ # @param method_name [Array<Symbol>] the names of methods to be redefined.
18
+ def delegate_to_proxy(*method_names)
19
+ method_names.each do |method_name|
20
+ proxy_method_name = proxy_method_name_for(method_name)
21
+ proxy_method_private = private_method_defined?(method_name)
22
+
23
+ # some adapter methods are private. We need to make them public before aliasing.
24
+ public method_name if proxy_method_private
25
+
26
+ alias_method proxy_method_name, method_name
27
+
28
+ # If adapter method was originally private. We now make them private again.
29
+ private method_name, proxy_method_name if proxy_method_private
30
+ end
31
+
32
+ delegate(*method_names, to: :proxy)
33
+ end
34
+
35
+ private
36
+
37
+ def proxy_method_name_for(method_name)
38
+ :"#{method_name}#{ActiveRecordProxyAdapters::PrimaryReplicaProxy::UNPROXIED_METHOD_SUFFIX}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_proxy_adapters/primary_replica_proxy"
4
+ require "active_record_proxy_adapters/active_record_context"
5
+
6
+ module ActiveRecordProxyAdapters
7
+ # Proxy to the original PostgreSQLAdapter, allowing the use of the ActiveRecordProxyAdapters::PrimaryReplicaProxy.
8
+ class PostgreSQLProxy < PrimaryReplicaProxy
9
+ # ActiveRecord::PostgreSQLAdapter methods that should be proxied.
10
+ hijack_method :execute, :exec_query
11
+
12
+ hijack_method :exec_no_cache, :exec_cache unless ActiveRecordContext.active_record_v8_0_or_greater?
13
+ end
14
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_proxy_adapters/configuration"
4
+ require "active_support/core_ext/module/delegation"
5
+ require "active_support/core_ext/object/blank"
6
+ require "concurrent-ruby"
7
+ require "active_record_proxy_adapters/active_record_context"
8
+
9
+ module ActiveRecordProxyAdapters
10
+ # This is the base class for all proxies. It defines the methods that should be proxied
11
+ # and the logic to determine which database to use.
12
+ class PrimaryReplicaProxy # rubocop:disable Metrics/ClassLength
13
+ # All queries that match these patterns should be sent to the primary database
14
+ SQL_PRIMARY_MATCHERS = [
15
+ /\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
16
+ /\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i
17
+ ].map(&:freeze).freeze
18
+ # All queries that match these patterns should be sent to the replica database
19
+ SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].map(&:freeze).freeze
20
+ # All queries that match these patterns should be sent to all databases
21
+ SQL_ALL_MATCHERS = [/\A\s*set\s/i].map(&:freeze).freeze
22
+ # Local sets queries should not be sent to all datbases
23
+ SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].map(&:freeze).freeze
24
+ # These patterns define which database statments are considered write statments, so we can shortly re-route all
25
+ # requests to the primary database so the replica has time to replicate
26
+ WRITE_STATEMENT_MATCHERS = [/\ABEGIN/i, /\ACOMMIT/i, /INSERT\sINTO\s/i, /UPDATE\s/i, /DELETE\sFROM\s/i,
27
+ /DROP\s/i].map(&:freeze).freeze
28
+ UNPROXIED_METHOD_SUFFIX = "_unproxied"
29
+
30
+ # Defines which methods should be hijacked from the original adapter and use the proxy
31
+ # @param method_names [Array<Symbol>] the list of method names from the adapter
32
+ def self.hijack_method(*method_names) # rubocop:disable Metrics/MethodLength
33
+ @hijacked_methods ||= Set.new
34
+ @hijacked_methods += Set.new(method_names)
35
+
36
+ method_names.each do |method_name|
37
+ define_method(method_name) do |*args, **kwargs, &block|
38
+ proxy_bypass_method = "#{method_name}#{UNPROXIED_METHOD_SUFFIX}"
39
+ sql_string = coerce_query_to_string(args.first)
40
+
41
+ appropriate_connection(sql_string) do |conn|
42
+ method_to_call = conn == primary_connection ? proxy_bypass_method : method_name
43
+
44
+ conn.send(method_to_call, *args, **kwargs, &block)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.hijacked_methods
51
+ @hijacked_methods.to_a
52
+ end
53
+
54
+ # @param primary_connection [ActiveRecord::ConnectionAdatpers::AbstractAdapter]
55
+ def initialize(primary_connection)
56
+ @primary_connection = primary_connection
57
+ @last_write_at = 0
58
+ @active_record_context = ActiveRecordContext.new
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :primary_connection, :last_write_at, :active_record_context
64
+
65
+ delegate :connected_to_stack, to: :connection_class
66
+ delegate :reading_role, :writing_role, to: :active_record_context
67
+
68
+ def connection_class
69
+ active_record_context.connection_class_for(primary_connection)
70
+ end
71
+
72
+ def replica_pool
73
+ ActiveRecord::Base.connected_to(role: reading_role) { ActiveRecord::Base.connection_pool }
74
+ end
75
+
76
+ def coerce_query_to_string(sql_or_arel)
77
+ sql_or_arel.respond_to?(:to_sql) ? sql_or_arel.to_sql : sql_or_arel.to_s
78
+ end
79
+
80
+ def appropriate_connection(sql_string, &block)
81
+ roles_for(sql_string).map do |role|
82
+ connection_for(role, sql_string) do |connection|
83
+ block.call(connection)
84
+ end
85
+ end.last
86
+ end
87
+
88
+ def roles_for(sql_string)
89
+ return [top_of_connection_stack_role] if top_of_connection_stack_role.present?
90
+
91
+ if need_all?(sql_string)
92
+ [reading_role, writing_role]
93
+ elsif need_primary?(sql_string)
94
+ [writing_role]
95
+ else
96
+ [reading_role]
97
+ end
98
+ end
99
+
100
+ def top_of_connection_stack_role
101
+ return if connected_to_stack.empty?
102
+
103
+ top = connected_to_stack.last
104
+ role = top[:role]
105
+ return unless role.present?
106
+
107
+ [reading_role, writing_role].include?(role) ? role : nil
108
+ end
109
+
110
+ def connection_for(role, sql_string) # rubocop:disable Metrics/MethodLength
111
+ connection = if role == writing_role
112
+ primary_connection
113
+ else
114
+ begin
115
+ replica_pool.checkout(checkout_timeout)
116
+ # rescue NoDatabaseError to avoid crashing when running db:create rake task
117
+ # rescue ConnectionNotEstablished to handle connectivity issues in the replica
118
+ # (for example, replication delay)
119
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
120
+ primary_connection
121
+ end
122
+ end
123
+
124
+ result = yield(connection)
125
+ update_primary_latest_write_timestamp if !replica_connection?(connection) && write_statement?(sql_string)
126
+
127
+ result
128
+ ensure
129
+ replica_connection?(connection) && replica_pool.checkin(connection)
130
+ end
131
+
132
+ def replica_connection?(connection)
133
+ connection != primary_connection
134
+ end
135
+
136
+ # @return [TrueClass] if there has been a write within the last {#proxy_delay} seconds
137
+ # @return [TrueClass] if sql_string matches a write statement (i.e. INSERT, UPDATE, DELETE, SELECT FOR UPDATE)
138
+ # @return [FalseClass] if sql_string matches a read statement (i.e. SELECT)
139
+ def need_primary?(sql_string)
140
+ return true if recent_write_to_primary?
141
+
142
+ return true if in_transaction?
143
+ return true if SQL_PRIMARY_MATCHERS.any?(&match_sql?(sql_string))
144
+ return false if SQL_REPLICA_MATCHERS.any?(&match_sql?(sql_string))
145
+
146
+ true
147
+ end
148
+
149
+ def need_all?(sql_string)
150
+ return false if SQL_SKIP_ALL_MATCHERS.any?(&match_sql?(sql_string))
151
+
152
+ SQL_ALL_MATCHERS.any?(&match_sql?(sql_string))
153
+ end
154
+
155
+ def write_statement?(sql_string)
156
+ WRITE_STATEMENT_MATCHERS.any?(&match_sql?(sql_string))
157
+ end
158
+
159
+ def match_sql?(sql_string)
160
+ proc { |matcher| matcher.match?(sql_string) }
161
+ end
162
+
163
+ # TODO: implement a context API to re-route requests to the primary database if a recent query was sent to it to
164
+ # avoid replication delay issues
165
+ # @return Boolean
166
+ def recent_write_to_primary?
167
+ Concurrent.monotonic_time - last_write_at < proxy_delay
168
+ end
169
+
170
+ def in_transaction?
171
+ primary_connection.open_transactions.positive?
172
+ end
173
+
174
+ def update_primary_latest_write_timestamp
175
+ @last_write_at = Concurrent.monotonic_time
176
+ end
177
+
178
+ def proxy_delay
179
+ proxy_config.proxy_delay
180
+ end
181
+
182
+ def checkout_timeout
183
+ proxy_config.checkout_timeout
184
+ end
185
+
186
+ def proxy_config
187
+ ActiveRecordProxyAdapters.config
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+
5
+ module ActiveRecordProxyAdapters
6
+ # Hooks into rails boot process to extend ActiveRecord with the proxy adapter.
7
+ class Railtie < Rails::Railtie
8
+ ActiveSupport.on_load :active_record do
9
+ require "active_record_proxy_adapters/connection_handling"
10
+ ActiveRecord::Base.extend(ActiveRecordProxyAdapters::ConnectionHandling)
11
+ end
12
+
13
+ config.to_prepare do
14
+ Rails.autoloaders.each do |autoloader|
15
+ autoloader.inflector.inflect(
16
+ "postgresql_proxy_adapter" => "PostgreSQLProxyAdapter"
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordProxyAdapters
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_record_proxy_adapters/version"
5
+ require "active_record_proxy_adapters/configuration"
6
+
7
+ # The gem namespace.
8
+ module ActiveRecordProxyAdapters
9
+ class Error < StandardError; end
10
+
11
+ module_function
12
+
13
+ def configure
14
+ yield(config)
15
+ end
16
+
17
+ def config
18
+ @config ||= Configuration.new
19
+ end
20
+ end
21
+
22
+ require_relative "active_record_proxy_adapters/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,34 @@
1
+ FROM docker.io/postgres:14-alpine
2
+
3
+ ARG REPLICA_USER=replicator
4
+ ARG REPLICA_PASSWORD=replicator
5
+ ARG REPLICATION_SLOT_NAME=replication_slot
6
+ ARG INIT_SQL=00_init.sql
7
+ ARG POSTGRES_LOGGING_COLLECTOR=
8
+ ARG POSTGRES_LOG_DESTINATION=
9
+ ARG POSTGRES_LOG_STATEMENT=
10
+ ENV CONF_SAMPLE="/usr/local/share/postgresql/postgresql.conf.sample"
11
+
12
+ WORKDIR /docker-entrypoint-initdb.d
13
+
14
+ USER root
15
+
16
+ RUN touch $INIT_SQL
17
+ RUN chown -R postgres:postgres $INIT_SQL
18
+ RUN echo "CREATE USER ${REPLICA_USER} WITH REPLICATION ENCRYPTED PASSWORD '${REPLICA_PASSWORD}';" > $INIT_SQL
19
+ RUN echo "SELECT pg_create_physical_replication_slot('${REPLICATION_SLOT_NAME}');" >> $INIT_SQL
20
+
21
+ # Enable logging collector if given
22
+ RUN if [[ ! -z "${POSTGRES_LOGGING_COLLECTOR}" ]]; then sed -i "s/#\(logging_collector = \)off\(.*\)/\1${POSTGRES_LOGGING_COLLECTOR}\2/" ${CONF_SAMPLE}; fi
23
+
24
+ # Override default log destination if given
25
+ RUN if [[ ! -z "${POSTGRES_LOG_DESTINATION}" ]]; then sed -i "s/#\(log_destination = \)'stderr'\(.*\)/\1'${POSTGRES_LOG_DESTINATION}'\2/" ${CONF_SAMPLE}; fi
26
+
27
+ # Override log statement if given
28
+ RUN if [[ ! -z "${POSTGRES_LOG_STATEMENT}" ]]; then sed -i "s/#\(log_statement = \)'none'\(.*\)/\1'${POSTGRES_LOG_STATEMENT}'\2/" ${CONF_SAMPLE}; fi
29
+
30
+ WORKDIR /
31
+
32
+ USER postgres
33
+
34
+ CMD ["postgres", "-c", "wal_level=replica", "-c", "hot_standby=on", "-c", "max_wal_senders=10", "-c", "max_replication_slots=10", "-c", "hot_standby_feedback=on" ]
@@ -0,0 +1,23 @@
1
+ FROM docker.io/postgres:14-alpine
2
+
3
+ ENV PRIMARY_DATABASE_HOST=localhost
4
+ ENV PRIMARY_DATABASE_PORT=5432
5
+ ENV PRIMARY_REPLICATION_SLOT=replication_slot
6
+
7
+ USER root
8
+ RUN printf '' > cmd.sh
9
+
10
+ RUN echo 'until pg_basebackup --pgdata=/var/lib/postgresql/data -R --slot=$PRIMARY_REPLICATION_SLOT --host=$PRIMARY_DATABASE_HOST --port=$PRIMARY_DATABASE_PORT' >> cmd.sh
11
+ RUN echo 'do' >> cmd.sh
12
+ RUN echo "echo 'Waiting for primary to connect...'" >> cmd.sh
13
+ RUN echo 'sleep 1s' >> cmd.sh
14
+ RUN echo 'done' >> cmd.sh
15
+ RUN echo "echo 'Backup done, starting replica...'" >> cmd.sh
16
+ RUN echo 'chmod 0700 /var/lib/postgresql/data' >> cmd.sh
17
+ RUN echo 'postgres' >> cmd.sh
18
+
19
+ RUN chown -R postgres:postgres cmd.sh
20
+ USER postgres
21
+ RUN chmod u+rwx cmd.sh
22
+
23
+ CMD [ "./cmd.sh" ]
@@ -0,0 +1,4 @@
1
+ module ActiveRecordProxyAdapters
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end