File: database_selector_test.rb

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (318 lines) | stat: -rw-r--r-- 10,102 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# frozen_string_literal: true

require "cases/helper"
require "models/person"
require "action_dispatch"

module ActiveRecord
  class DatabaseSelectorTest < ActiveRecord::TestCase
    setup do
      @session_store = {}
      @session = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
    end

    teardown do
      clean_up_connection_handler
    end

    def test_empty_session
      assert_equal Time.at(0), @session.last_write_timestamp
    end

    def test_writing_the_session_timestamps
      assert @session.update_last_write_timestamp

      session2 = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
      assert_equal @session.last_write_timestamp, session2.last_write_timestamp
    end

    def test_writing_session_time_changes
      assert @session.update_last_write_timestamp

      before = @session.last_write_timestamp
      sleep(0.1)

      assert @session.update_last_write_timestamp
      assert_not_equal before, @session.last_write_timestamp
    end

    def test_read_from_replicas
      @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now - 5.seconds)

      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)

      called = false
      resolver.read do
        called = true
        assert ActiveRecord::Base.connected_to?(role: :reading)
      end
      assert called
    end

    unless in_memory_db?
      def test_can_write_while_reading_from_replicas_if_explicit
        @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now - 5.seconds)

        resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)

        called = false
        resolver.read do
          ActiveRecord::Base.establish_connection :arunit

          called = true

          assert ActiveRecord::Base.connected_to?(role: :reading)
          assert_predicate ActiveRecord::Base.lease_connection, :preventing_writes?

          ActiveRecord::Base.connected_to(role: :writing, prevent_writes: false) do
            assert ActiveRecord::Base.connected_to?(role: :writing)
            assert_not_predicate ActiveRecord::Base.lease_connection, :preventing_writes?
          end

          assert ActiveRecord::Base.connected_to?(role: :reading)
          assert_predicate ActiveRecord::Base.lease_connection, :preventing_writes?
        end
        assert called
      end
    end

    def test_read_from_primary
      @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now)

      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)

      called = false
      resolver.read do
        called = true
        assert ActiveRecord::Base.connected_to?(role: :writing)
      end
      assert called
    end

    def test_write_to_primary
      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)

      # Session should start empty
      assert_nil @session_store[:last_write]

      called = false
      resolver.write do
        assert ActiveRecord::Base.connected_to?(role: :writing)
        called = true
      end
      assert called

      # and be populated by the last write time
      assert @session_store[:last_write]
    end

    def test_write_to_primary_and_update_custom_context
      custom_context = Class.new(ActiveRecord::Middleware::DatabaseSelector::Resolver::Session) do
        def update_last_write_timestamp
          super
          @wrote_to_primary = true
        end

        def save(response)
          response[:wrote_to_primary] = @wrote_to_primary
        end
      end

      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(custom_context.new(@session_store))

      # Session should start empty
      assert_nil @session_store[:last_write]

      called = false
      resolver.write do
        assert ActiveRecord::Base.connected_to?(role: :writing)
        called = true
      end
      assert called
      response = {}
      resolver.update_context(response)

      # and be populated by the last write time
      assert @session_store[:last_write]
      # plus the response updated
      assert response[:wrote_to_primary]
    end

    def test_write_to_primary_with_exception
      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)

      # Session should start empty
      assert_nil @session_store[:last_write]

      called = false
      assert_raises(ActiveRecord::RecordNotFound) do
        resolver.write do
          assert ActiveRecord::Base.connected_to?(role: :writing)
          called = true
          raise ActiveRecord::RecordNotFound
        end
      end
      assert called

      # and be populated by the last write time
      assert @session_store[:last_write]
    end

    def test_read_from_primary_with_options
      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds)

      # Session should start empty
      assert_nil @session_store[:last_write]

      called = false
      resolver.write do
        assert ActiveRecord::Base.connected_to?(role: :writing)
        called = true
      end
      assert called

      # and be populated by the last write time
      assert @session_store[:last_write]

      read = false
      resolver.read do
        assert ActiveRecord::Base.connected_to?(role: :writing)
        read = true
      end
      assert read
    end

    def test_preventing_writes_turns_off_for_primary_write
      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds)

      # Session should start empty
      assert_nil @session_store[:last_write]

      called = false
      resolver.write do
        assert ActiveRecord::Base.connected_to?(role: :writing)
        called = true
      end
      assert called

      # and be populated by the last write time
      assert @session_store[:last_write]

      read = false
      write = false
      resolver.read do
        assert ActiveRecord::Base.connected_to?(role: :writing)
        assert_predicate ActiveRecord::Base.lease_connection, :preventing_writes?
        read = true

        resolver.write do
          assert ActiveRecord::Base.connected_to?(role: :writing)
          assert_not_predicate ActiveRecord::Base.lease_connection, :preventing_writes?
          write = true
        end
      end

      assert write
      assert read
    end

    def test_preventing_writes_works_in_a_threaded_environment
      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds)
      inside_preventing = Concurrent::Event.new
      finished_checking = Concurrent::Event.new

      @session.update_last_write_timestamp

      t1 = Thread.new do
        resolver.read do
          inside_preventing.wait
          assert ActiveRecord::Base.connected_to?(role: :writing)
          assert_predicate ActiveRecord::Base.lease_connection, :preventing_writes?
          finished_checking.set
        end
      end

      t2 = Thread.new do
        resolver.write do
          assert ActiveRecord::Base.connected_to?(role: :writing)
          assert_not_predicate ActiveRecord::Base.lease_connection, :preventing_writes?
          inside_preventing.set
          finished_checking.wait
        end
      end

      t3 = Thread.new do
        resolver.read do
          assert ActiveRecord::Base.connected_to?(role: :writing)
          assert_predicate ActiveRecord::Base.lease_connection, :preventing_writes?
        end
      end

      t1.join
      t2.join
      t3.join
    end

    def test_read_from_replica_with_no_delay
      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 0.seconds)

      # Session should start empty
      assert_nil @session_store[:last_write]

      called = false
      resolver.write do
        assert ActiveRecord::Base.connected_to?(role: :writing)
        called = true
      end
      assert called

      # and be populated by the last write time
      assert @session_store[:last_write]

      read = false
      resolver.read do
        assert ActiveRecord::Base.connected_to?(role: :reading)
        read = true
      end
      assert read
    end

    def test_the_middleware_chooses_writing_role_with_POST_request
      middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
        assert ActiveRecord::Base.connected_to?(role: :writing)
        [200, {}, ["body"]]
      })
      cache = ActiveSupport::Cache::MemoryStore.new
      middleware = ActionDispatch::Session::CacheStore.new(middleware, cache: cache, key: "_session_id")
      assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "POST")
    end

    def test_the_middleware_chooses_reading_role_with_GET_request
      middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
        assert ActiveRecord::Base.connected_to?(role: :reading)
        [200, {}, ["body"]]
      })
      cache = ActiveSupport::Cache::MemoryStore.new
      middleware = ActionDispatch::Session::CacheStore.new(middleware, cache: cache, key: "_session_id")

      assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET")
    end

    class ReadonlyResolver < ActiveRecord::Middleware::DatabaseSelector::Resolver
      def reading_request?(request)
        true
      end
    end

    def test_the_middleware_chooses_reading_role_with_POST_request_if_resolver_tells_it_to
      middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
        assert ActiveRecord::Base.connected_to?(role: :reading)
        [200, {}, ["body"]]
      }, ReadonlyResolver)

      cache = ActiveSupport::Cache::MemoryStore.new
      middleware = ActionDispatch::Session::CacheStore.new(middleware, cache: cache, key: "_session_id")
      assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "POST")
    end
  end
end