pecorino 0.7.3 → 0.7.4
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +8 -4
- data/lib/pecorino/adapters/postgres_adapter.rb +9 -7
- data/lib/pecorino/adapters/sqlite_adapter.rb +10 -8
- data/lib/pecorino/version.rb +1 -1
- data/lib/pecorino.rb +1 -1
- data/test/adapters/postgres_adapter_test.rb +11 -7
- data/test/adapters/sqlite_adapter_test.rb +4 -2
- data/test/block_test.rb +10 -6
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 392d3fe294cb751ad452716ef2b569f48f536a57464bdac39379042c29aa8242
|
4
|
+
data.tar.gz: 36309ce687caa5e2d32bf5e5cc7c862e264a7065cf5f211062e9ae7f51118ced
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14b6cadec3609d946d786330e5c912e2ae57054ba6f740d4a0dd4b82bc4660417df2bd71e8ac97fa44990b47f20f60540d82a2299c5a176da4b8c191752f46ba
|
7
|
+
data.tar.gz: 5059d7a546df2a1a6822a70ed73290165f7506ba6f36f41daf3779cad158781768c004aa5bd063f24452c15dcb6d5889efd16362b3e79ef51e2f88bbcaffc3e8
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -189,8 +189,10 @@ The Pecorino buckets and blocks are stateful. If you are not running tests with
|
|
189
189
|
```ruby
|
190
190
|
setup do
|
191
191
|
# Delete all transient records
|
192
|
-
ActiveRecord::Base.
|
193
|
-
|
192
|
+
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
193
|
+
connection.execute("TRUNCATE TABLE pecorino_blocks")
|
194
|
+
connection.execute("TRUNCATE TABLE pecorino_leaky_buckets")
|
195
|
+
end
|
194
196
|
end
|
195
197
|
```
|
196
198
|
|
@@ -224,8 +226,10 @@ cached_throttle.request!
|
|
224
226
|
Throttles and leaky buckets are transient resources. If you are using Postgres replication, it might be prudent to set the Pecorino tables to `UNLOGGED` which will exclude them from replication - and save you bandwidth and storage on your RR. To do so, add the following statements to your migration:
|
225
227
|
|
226
228
|
```ruby
|
227
|
-
ActiveRecord::Base.
|
228
|
-
|
229
|
+
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
230
|
+
connection.execute("ALTER TABLE pecorino_leaky_buckets SET UNLOGGED")
|
231
|
+
connection.execute("ALTER TABLE pecorino_blocks SET UNLOGGED")
|
232
|
+
end
|
229
233
|
```
|
230
234
|
|
231
235
|
## Development
|
@@ -30,7 +30,7 @@ class Pecorino::Adapters::PostgresAdapter
|
|
30
30
|
|
31
31
|
# If the return value of the query is a NULL it means no such bucket exists,
|
32
32
|
# so we assume the bucket is empty
|
33
|
-
current_level = @model_class.connection.uncached {
|
33
|
+
current_level = @model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_value(sql) } } || 0.0
|
34
34
|
[current_level, capacity - current_level.abs < 0.01]
|
35
35
|
end
|
36
36
|
|
@@ -83,7 +83,7 @@ class Pecorino::Adapters::PostgresAdapter
|
|
83
83
|
# query as a repeat (since we use "select_one" for the RETURNING bit) and will not call into Postgres
|
84
84
|
# correctly, thus the clock_timestamp() value would be frozen between calls. We don't want that here.
|
85
85
|
# See https://stackoverflow.com/questions/73184531/why-would-postgres-clock-timestamp-freeze-inside-a-rails-unit-test
|
86
|
-
upserted = @model_class.connection.uncached {
|
86
|
+
upserted = @model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_one(sql) } }
|
87
87
|
capped_level_after_fillup, at_capacity = upserted.fetch("level"), upserted.fetch("at_capacity")
|
88
88
|
[capped_level_after_fillup, at_capacity]
|
89
89
|
end
|
@@ -141,7 +141,7 @@ class Pecorino::Adapters::PostgresAdapter
|
|
141
141
|
level AS level_after
|
142
142
|
SQL
|
143
143
|
|
144
|
-
upserted = @model_class.connection.uncached {
|
144
|
+
upserted = @model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_one(sql) } }
|
145
145
|
level_after = upserted.fetch("level_after")
|
146
146
|
level_before = upserted.fetch("level_before")
|
147
147
|
[level_after, level_after >= capacity, level_after != level_before]
|
@@ -159,19 +159,21 @@ class Pecorino::Adapters::PostgresAdapter
|
|
159
159
|
blocked_until = GREATEST(EXCLUDED.blocked_until, t.blocked_until)
|
160
160
|
RETURNING blocked_until
|
161
161
|
SQL
|
162
|
-
@model_class.connection.uncached {
|
162
|
+
@model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_value(block_set_query) } }
|
163
163
|
end
|
164
164
|
|
165
165
|
def blocked_until(key:)
|
166
166
|
block_check_query = @model_class.sanitize_sql_array([<<~SQL, key])
|
167
167
|
SELECT blocked_until FROM pecorino_blocks WHERE key = ? AND blocked_until >= clock_timestamp() LIMIT 1
|
168
168
|
SQL
|
169
|
-
@model_class.connection.uncached {
|
169
|
+
@model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_value(block_check_query) } }
|
170
170
|
end
|
171
171
|
|
172
172
|
def prune
|
173
|
-
@model_class.
|
174
|
-
|
173
|
+
@model_class.connection_pool.with_connection do |connection|
|
174
|
+
connection.execute("DELETE FROM pecorino_blocks WHERE blocked_until < NOW()")
|
175
|
+
connection.execute("DELETE FROM pecorino_leaky_buckets WHERE may_be_deleted_after < NOW()")
|
176
|
+
end
|
175
177
|
end
|
176
178
|
|
177
179
|
def create_tables(active_record_schema)
|
@@ -37,7 +37,7 @@ class Pecorino::Adapters::SqliteAdapter
|
|
37
37
|
|
38
38
|
# If the return value of the query is a NULL it means no such bucket exists,
|
39
39
|
# so we assume the bucket is empty
|
40
|
-
current_level = @model_class.connection.uncached {
|
40
|
+
current_level = @model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_value(sql) } } || 0.0
|
41
41
|
[current_level, capacity - current_level.abs < 0.01]
|
42
42
|
end
|
43
43
|
|
@@ -91,7 +91,7 @@ class Pecorino::Adapters::SqliteAdapter
|
|
91
91
|
# query as a repeat (since we use "select_one" for the RETURNING bit) and will not call into Postgres
|
92
92
|
# correctly, thus the clock_timestamp() value would be frozen between calls. We don't want that here.
|
93
93
|
# See https://stackoverflow.com/questions/73184531/why-would-postgres-clock-timestamp-freeze-inside-a-rails-unit-test
|
94
|
-
upserted = @model_class.connection.uncached {
|
94
|
+
upserted = @model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_one(sql) } }
|
95
95
|
capped_level_after_fillup, one_if_did_overflow = upserted.fetch("level"), upserted.fetch("did_overflow")
|
96
96
|
[capped_level_after_fillup, one_if_did_overflow == 1]
|
97
97
|
end
|
@@ -130,7 +130,7 @@ class Pecorino::Adapters::SqliteAdapter
|
|
130
130
|
-- so that it can't be deleted between our INSERT and our UPDATE
|
131
131
|
may_be_deleted_after = EXCLUDED.may_be_deleted_after
|
132
132
|
SQL
|
133
|
-
@model_class.connection.execute(insert_sql)
|
133
|
+
@model_class.connection_pool.with_connection { |connection| connection.execute(insert_sql) }
|
134
134
|
|
135
135
|
sql = @model_class.sanitize_sql_array([<<~SQL, query_params])
|
136
136
|
-- With SQLite MATERIALIZED has to be used so that level_post is calculated before the UPDATE takes effect
|
@@ -156,7 +156,7 @@ class Pecorino::Adapters::SqliteAdapter
|
|
156
156
|
level AS level_after
|
157
157
|
SQL
|
158
158
|
|
159
|
-
upserted = @model_class.connection.uncached {
|
159
|
+
upserted = @model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_one(sql) } }
|
160
160
|
level_after = upserted.fetch("level_after")
|
161
161
|
level_before = upserted.fetch("level_before")
|
162
162
|
[level_after, level_after >= capacity, level_after != level_before]
|
@@ -174,7 +174,7 @@ class Pecorino::Adapters::SqliteAdapter
|
|
174
174
|
blocked_until = MAX(EXCLUDED.blocked_until, t.blocked_until)
|
175
175
|
RETURNING blocked_until;
|
176
176
|
SQL
|
177
|
-
blocked_until_s = @model_class.connection.uncached {
|
177
|
+
blocked_until_s = @model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_value(block_set_query) } }
|
178
178
|
Time.at(blocked_until_s)
|
179
179
|
end
|
180
180
|
|
@@ -188,14 +188,16 @@ class Pecorino::Adapters::SqliteAdapter
|
|
188
188
|
WHERE
|
189
189
|
key = :key AND blocked_until >= :now_s LIMIT 1
|
190
190
|
SQL
|
191
|
-
blocked_until_s = @model_class.connection.uncached {
|
191
|
+
blocked_until_s = @model_class.connection_pool.with_connection { |connection| connection.uncached { connection.select_value(block_check_query) } }
|
192
192
|
blocked_until_s && Time.at(blocked_until_s)
|
193
193
|
end
|
194
194
|
|
195
195
|
def prune
|
196
196
|
now_s = Time.now.to_f
|
197
|
-
@model_class.
|
198
|
-
|
197
|
+
@model_class.connection_pool.with_connection do |connection|
|
198
|
+
connection.execute("DELETE FROM pecorino_blocks WHERE blocked_until < ?", now_s)
|
199
|
+
connection.execute("DELETE FROM pecorino_leaky_buckets WHERE may_be_deleted_after < ?", now_s)
|
200
|
+
end
|
199
201
|
end
|
200
202
|
|
201
203
|
def create_tables(active_record_schema)
|
data/lib/pecorino/version.rb
CHANGED
data/lib/pecorino.rb
CHANGED
@@ -65,7 +65,7 @@ module Pecorino
|
|
65
65
|
# @return [Pecorino::Adapters::BaseAdapter]
|
66
66
|
def self.default_adapter_from_main_database
|
67
67
|
model_class = ActiveRecord::Base
|
68
|
-
adapter_name = model_class.
|
68
|
+
adapter_name = model_class.connection_pool.with_connection(&:adapter_name)
|
69
69
|
case adapter_name
|
70
70
|
when /postgres/i
|
71
71
|
Pecorino::Adapters::PostgresAdapter.new(model_class)
|
@@ -23,7 +23,7 @@ class PostgresAdapterTest < ActiveSupport::TestCase
|
|
23
23
|
|
24
24
|
def create_postgres_database_if_none
|
25
25
|
self.class.establish_connection(encoding: "unicode", database: SEED_DB_NAME.call)
|
26
|
-
ActiveRecord::Base.connection.execute("SELECT 1 FROM pecorino_leaky_buckets")
|
26
|
+
ActiveRecord::Base.connection_pool.with_connection { |connection| connection.execute("SELECT 1 FROM pecorino_leaky_buckets") }
|
27
27
|
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
|
28
28
|
create_postgres_database
|
29
29
|
retry
|
@@ -38,20 +38,24 @@ class PostgresAdapterTest < ActiveSupport::TestCase
|
|
38
38
|
def create_postgres_database
|
39
39
|
ActiveRecord::Migration.verbose = false
|
40
40
|
self.class.establish_connection(database: "postgres")
|
41
|
-
ActiveRecord::Base.connection.create_database(SEED_DB_NAME.call, charset: :unicode)
|
41
|
+
ActiveRecord::Base.connection_pool.with_connection { |connection| connection.create_database(SEED_DB_NAME.call, charset: :unicode) }
|
42
42
|
ActiveRecord::Base.connection.close
|
43
43
|
self.class.establish_connection(encoding: "unicode", database: SEED_DB_NAME.call)
|
44
44
|
end
|
45
45
|
|
46
46
|
def truncate_test_tables
|
47
|
-
ActiveRecord::Base.
|
48
|
-
|
47
|
+
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
48
|
+
connection.execute("TRUNCATE TABLE pecorino_leaky_buckets")
|
49
|
+
connection.execute("TRUNCATE TABLE pecorino_blocks")
|
50
|
+
end
|
49
51
|
end
|
50
52
|
|
51
53
|
def test_create_tables
|
52
54
|
ActiveRecord::Base.transaction do
|
53
|
-
ActiveRecord::Base.
|
54
|
-
|
55
|
+
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
56
|
+
connection.execute("DROP TABLE pecorino_leaky_buckets")
|
57
|
+
connection.execute("DROP TABLE pecorino_blocks")
|
58
|
+
end
|
55
59
|
# The adapter has to be in a variable as the schema definition is scoped to the migrator, not self
|
56
60
|
retained_adapter = create_adapter # the schema define block is run via instance_exec so it does not retain scope
|
57
61
|
ActiveRecord::Schema.define(version: 1) do |via_definer|
|
@@ -64,6 +68,6 @@ class PostgresAdapterTest < ActiveSupport::TestCase
|
|
64
68
|
Minitest.after_run do
|
65
69
|
ActiveRecord::Base.connection.close
|
66
70
|
establish_connection(database: "postgres")
|
67
|
-
ActiveRecord::Base.connection.drop_database(SEED_DB_NAME.call)
|
71
|
+
ActiveRecord::Base.connection_pool.with_connection { |connection| connection.drop_database(SEED_DB_NAME.call) }
|
68
72
|
end
|
69
73
|
end
|
@@ -33,8 +33,10 @@ class SqliteAdapterTest < ActiveSupport::TestCase
|
|
33
33
|
|
34
34
|
def test_create_tables
|
35
35
|
ActiveRecord::Base.transaction do
|
36
|
-
ActiveRecord::Base.
|
37
|
-
|
36
|
+
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
37
|
+
connection.execute("DROP TABLE pecorino_leaky_buckets")
|
38
|
+
connection.execute("DROP TABLE pecorino_blocks")
|
39
|
+
end
|
38
40
|
# The adapter has to be in a variable as the schema definition is scoped to the migrator, not self
|
39
41
|
retained_adapter = create_adapter # the schema define block is run via instance_exec so it does not retain scope
|
40
42
|
ActiveRecord::Schema.define(version: 1) do |via_definer|
|
data/test/block_test.rb
CHANGED
@@ -4,20 +4,24 @@ require "test_helper"
|
|
4
4
|
require "base64"
|
5
5
|
|
6
6
|
class BlockTest < ActiveSupport::TestCase
|
7
|
+
def setup
|
8
|
+
@adapter = Pecorino::Adapters::MemoryAdapter.new
|
9
|
+
end
|
10
|
+
|
7
11
|
test "sets a block" do
|
8
12
|
k = Base64.strict_encode64(Random.bytes(4))
|
9
|
-
assert_nil Pecorino::Block.blocked_until(key: k)
|
10
|
-
assert Pecorino::Block.set!(key: k, block_for: 30.minutes)
|
13
|
+
assert_nil Pecorino::Block.blocked_until(key: k, adapter: @adapter)
|
14
|
+
assert Pecorino::Block.set!(key: k, block_for: 30.minutes, adapter: @adapter)
|
11
15
|
|
12
|
-
blocked_until = Pecorino::Block.blocked_until(key: k)
|
16
|
+
blocked_until = Pecorino::Block.blocked_until(key: k, adapter: @adapter)
|
13
17
|
assert_in_delta Time.now + 30.minutes, blocked_until, 10
|
14
18
|
end
|
15
19
|
|
16
20
|
test "does not return a block which has lapsed" do
|
17
21
|
k = Base64.strict_encode64(Random.bytes(4))
|
18
|
-
assert_nil Pecorino::Block.blocked_until(key: k)
|
19
|
-
Pecorino::Block.set!(key: k, block_for: -30.minutes)
|
20
|
-
blocked_until = Pecorino::Block.blocked_until(key: k)
|
22
|
+
assert_nil Pecorino::Block.blocked_until(key: k, adapter: @adapter)
|
23
|
+
Pecorino::Block.set!(key: k, block_for: -30.minutes, adapter: @adapter)
|
24
|
+
blocked_until = Pecorino::Block.blocked_until(key: k, adapter: @adapter)
|
21
25
|
assert_nil blocked_until
|
22
26
|
end
|
23
27
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pecorino
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-08-07 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activerecord
|