fbe 0.19.3 → 0.20.0
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/Gemfile +1 -0
- data/Gemfile.lock +4 -2
- data/lib/fbe/middleware/sqlite_store.rb +48 -8
- data/lib/fbe/octo.rb +1 -1
- data/lib/fbe.rb +1 -1
- data/test/fbe/middleware/test_sqlite_store.rb +82 -9
- data/test/fbe/test_octo.rb +41 -0
- data/test/test__helper.rb +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c33c559724c991d4aabc2ad9c097170e53de0002e362dcfadfe956d77eed7ec
|
4
|
+
data.tar.gz: 787dbc8ce12ebacbd0b96cd8174ced19aa11665f29d3d410fb93c397dbb80d87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ca43bc96f5c8f426a72fb3ab722684d36b67613526b9d2e1be175abae03581c4fc0318314e92628c9064d2c7ba8637814ed950e349ba86b64caedc768619be8f
|
7
|
+
data.tar.gz: 81d737f13af364095e871df44000f6293d1c062929d7f859cd3836d23c3cf602a1362a9653fd739f1726c4d1004e0bad1806e3cb3fde23dc4aef081e39dfbd6a
|
data/Gemfile
CHANGED
@@ -8,6 +8,7 @@ gemspec
|
|
8
8
|
|
9
9
|
gem 'minitest', '~>5.25', require: false
|
10
10
|
gem 'minitest-reporters', '~>1.7', require: false
|
11
|
+
gem 'minitest-stub-const', '~>0.6', require: false
|
11
12
|
gem 'os', '~>1.1', require: false
|
12
13
|
gem 'qbash', '~>0.4', require: false
|
13
14
|
gem 'rake', '~>13.3', require: false
|
data/Gemfile.lock
CHANGED
@@ -143,6 +143,7 @@ GEM
|
|
143
143
|
builder
|
144
144
|
minitest (>= 5.0)
|
145
145
|
ruby-progressbar
|
146
|
+
minitest-stub-const (0.6)
|
146
147
|
moments (0.3.0)
|
147
148
|
multipart-post (2.4.1)
|
148
149
|
net-http (0.6.0)
|
@@ -179,7 +180,7 @@ GEM
|
|
179
180
|
regexp_parser (2.10.0)
|
180
181
|
retries (0.0.5)
|
181
182
|
rexml (3.4.1)
|
182
|
-
rubocop (1.76.
|
183
|
+
rubocop (1.76.1)
|
183
184
|
json (~> 2.3)
|
184
185
|
language_server-protocol (~> 3.17.0.2)
|
185
186
|
lint_roller (~> 1.1.0)
|
@@ -190,7 +191,7 @@ GEM
|
|
190
191
|
rubocop-ast (>= 1.45.0, < 2.0)
|
191
192
|
ruby-progressbar (~> 1.7)
|
192
193
|
unicode-display_width (>= 2.4.0, < 4.0)
|
193
|
-
rubocop-ast (1.45.
|
194
|
+
rubocop-ast (1.45.1)
|
194
195
|
parser (>= 3.3.7.2)
|
195
196
|
prism (~> 1.4)
|
196
197
|
rubocop-minitest (0.38.1)
|
@@ -258,6 +259,7 @@ DEPENDENCIES
|
|
258
259
|
fbe!
|
259
260
|
minitest (~> 5.25)
|
260
261
|
minitest-reporters (~> 1.7)
|
262
|
+
minitest-stub-const (~> 0.6)
|
261
263
|
os (~> 1.1)
|
262
264
|
qbash (~> 0.4)
|
263
265
|
rake (~> 13.3)
|
@@ -3,6 +3,7 @@
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
|
+
require 'time'
|
6
7
|
require 'json'
|
7
8
|
require 'sqlite3'
|
8
9
|
require_relative '../../fbe'
|
@@ -16,15 +17,20 @@ require_relative '../../fbe/middleware'
|
|
16
17
|
class Fbe::Middleware::SqliteStore
|
17
18
|
attr_reader :path
|
18
19
|
|
19
|
-
def initialize(path)
|
20
|
+
def initialize(path, version)
|
20
21
|
raise ArgumentError, 'Database path cannot be nil or empty' if path.nil? || path.empty?
|
21
22
|
dir = File.dirname(path)
|
22
23
|
raise ArgumentError, "Directory #{dir} does not exist" unless File.directory?(dir)
|
24
|
+
raise ArgumentError, 'Version cannot be nil or empty' if version.nil? || version.empty?
|
23
25
|
@path = File.absolute_path(path)
|
26
|
+
@version = version
|
24
27
|
end
|
25
28
|
|
26
29
|
def read(key)
|
27
|
-
value = perform
|
30
|
+
value = perform do |t|
|
31
|
+
t.execute('UPDATE cache SET touched_at = ?2 WHERE key = ?1;', [key, Time.now.utc.iso8601])
|
32
|
+
t.execute('SELECT value FROM cache WHERE key = ? LIMIT 1;', [key])
|
33
|
+
end.dig(0, 0)
|
28
34
|
JSON.parse(value) if value
|
29
35
|
end
|
30
36
|
|
@@ -39,17 +45,22 @@ class Fbe::Middleware::SqliteStore
|
|
39
45
|
req['url'].include?('?') || req['method'] != 'get'
|
40
46
|
end
|
41
47
|
value = JSON.dump(value)
|
48
|
+
return if value.bytesize > 10_000
|
42
49
|
perform do |t|
|
43
|
-
t.execute(<<~SQL, [key, value])
|
44
|
-
INSERT INTO cache(key, value) VALUES(?1, ?2)
|
45
|
-
ON CONFLICT(key) DO UPDATE SET value = ?2
|
50
|
+
t.execute(<<~SQL, [key, value, Time.now.utc.iso8601])
|
51
|
+
INSERT INTO cache(key, value, touched_at) VALUES(?1, ?2, ?3)
|
52
|
+
ON CONFLICT(key) DO UPDATE SET value = ?2, touched_at = ?3
|
46
53
|
SQL
|
47
54
|
end
|
48
55
|
nil
|
49
56
|
end
|
50
57
|
|
51
58
|
def clear
|
52
|
-
perform
|
59
|
+
perform do |t|
|
60
|
+
t.execute 'DELETE FROM cache;'
|
61
|
+
t.execute "UPDATE meta SET value = ? WHERE key = 'version';", [@version]
|
62
|
+
end
|
63
|
+
@db.execute 'VACUUM;'
|
53
64
|
end
|
54
65
|
|
55
66
|
def all
|
@@ -62,8 +73,37 @@ class Fbe::Middleware::SqliteStore
|
|
62
73
|
@db ||=
|
63
74
|
SQLite3::Database.new(@path).tap do |d|
|
64
75
|
d.transaction do |t|
|
65
|
-
t.execute
|
66
|
-
|
76
|
+
t.execute <<~SQL
|
77
|
+
CREATE TABLE IF NOT EXISTS cache(
|
78
|
+
key TEXT UNIQUE NOT NULL, value TEXT, touched_at TEXT NOT NULL
|
79
|
+
);
|
80
|
+
SQL
|
81
|
+
t.execute 'CREATE INDEX IF NOT EXISTS cache_key_idx ON cache(key);'
|
82
|
+
t.execute 'CREATE INDEX IF NOT EXISTS cache_touched_at_idx ON cache(touched_at);'
|
83
|
+
t.execute 'CREATE TABLE IF NOT EXISTS meta(key TEXT UNIQUE NOT NULL, value TEXT);'
|
84
|
+
t.execute 'CREATE INDEX IF NOT EXISTS meta_key_idx ON meta(key);'
|
85
|
+
t.execute "INSERT INTO meta(key, value) VALUES('version', ?) ON CONFLICT(key) DO NOTHING;", [@version]
|
86
|
+
end
|
87
|
+
if d.execute("SELECT value FROM meta WHERE key = 'version' LIMIT 1;").dig(0, 0) != @version
|
88
|
+
d.transaction do |t|
|
89
|
+
t.execute 'DELETE FROM cache;'
|
90
|
+
t.execute "UPDATE meta SET value = ? WHERE key = 'version';", [@version]
|
91
|
+
end
|
92
|
+
d.execute 'VACUUM;'
|
93
|
+
end
|
94
|
+
if File.size(@path) > 10 * 1024 * 1024
|
95
|
+
while d.execute(<<~SQL).dig(0, 0) > 10 * 1024 * 1024
|
96
|
+
SELECT (page_count - freelist_count) * page_size AS size
|
97
|
+
FROM pragma_page_count(), pragma_freelist_count(), pragma_page_size();
|
98
|
+
SQL
|
99
|
+
d.transaction do |t|
|
100
|
+
t.execute <<~SQL
|
101
|
+
DELETE FROM cache
|
102
|
+
WHERE key IN (SELECT key FROM cache ORDER BY touched_at LIMIT 50)
|
103
|
+
SQL
|
104
|
+
end
|
105
|
+
end
|
106
|
+
d.execute 'VACUUM;'
|
67
107
|
end
|
68
108
|
at_exit { @db&.close }
|
69
109
|
end
|
data/lib/fbe/octo.rb
CHANGED
@@ -77,7 +77,7 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
|
|
77
77
|
backoff_factor: 2
|
78
78
|
)
|
79
79
|
if options.sqlite_cache
|
80
|
-
store = Fbe::Middleware::SqliteStore.new(options.sqlite_cache)
|
80
|
+
store = Fbe::Middleware::SqliteStore.new(options.sqlite_cache, Fbe::VERSION)
|
81
81
|
loog.info(
|
82
82
|
"Using HTTP cache in SQLite file: #{store.path} (" \
|
83
83
|
"#{File.exist?(store.path) ? "#{File.size(store.path)} bytes" : 'file is absent'}" \
|
data/lib/fbe.rb
CHANGED
@@ -15,7 +15,7 @@ require_relative '../../../lib/fbe/middleware/sqlite_store'
|
|
15
15
|
class SqliteStoreTest < Fbe::Test
|
16
16
|
def test_simple_caching_algorithm
|
17
17
|
with_tmpfile('x.db') do |f|
|
18
|
-
store = Fbe::Middleware::SqliteStore.new(f)
|
18
|
+
store = Fbe::Middleware::SqliteStore.new(f, '0.0.0')
|
19
19
|
k = 'some-key'
|
20
20
|
assert_nil(store.read(k))
|
21
21
|
assert_nil(store.delete(k))
|
@@ -33,14 +33,14 @@ class SqliteStoreTest < Fbe::Test
|
|
33
33
|
|
34
34
|
def test_returns_empty_list
|
35
35
|
with_tmpfile('b.db') do |f|
|
36
|
-
store = Fbe::Middleware::SqliteStore.new(f)
|
36
|
+
store = Fbe::Middleware::SqliteStore.new(f, '0.0.0')
|
37
37
|
assert_empty(store.all)
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
def test_clear_all_keys
|
42
42
|
with_tmpfile('a.db') do |f|
|
43
|
-
store = Fbe::Middleware::SqliteStore.new(f)
|
43
|
+
store = Fbe::Middleware::SqliteStore.new(f, '0.0.0')
|
44
44
|
k = 'a key'
|
45
45
|
store.write(k, 'some value')
|
46
46
|
store.clear
|
@@ -50,20 +50,20 @@ class SqliteStoreTest < Fbe::Test
|
|
50
50
|
|
51
51
|
def test_empty_all_if_not_written
|
52
52
|
with_tmpfile do |f|
|
53
|
-
store = Fbe::Middleware::SqliteStore.new(f)
|
53
|
+
store = Fbe::Middleware::SqliteStore.new(f, '0.0.0')
|
54
54
|
assert_empty(store.all)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
58
|
def test_wrong_db_path
|
59
59
|
assert_raises(ArgumentError) do
|
60
|
-
Fbe::Middleware::SqliteStore.new(nil).read('my_key')
|
60
|
+
Fbe::Middleware::SqliteStore.new(nil, '0.0.0').read('my_key')
|
61
61
|
end
|
62
62
|
assert_raises(ArgumentError) do
|
63
|
-
Fbe::Middleware::SqliteStore.new('').read('my_key')
|
63
|
+
Fbe::Middleware::SqliteStore.new('', '0.0.0').read('my_key')
|
64
64
|
end
|
65
65
|
assert_raises(ArgumentError) do
|
66
|
-
Fbe::Middleware::SqliteStore.new('/fakepath/fakefolder/test.db').read('my_key')
|
66
|
+
Fbe::Middleware::SqliteStore.new('/fakepath/fakefolder/test.db', '0.0.0').read('my_key')
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
@@ -72,7 +72,7 @@ class SqliteStoreTest < Fbe::Test
|
|
72
72
|
File.binwrite(f, Array.new(20) { rand(0..255) }.pack('C*'))
|
73
73
|
ex =
|
74
74
|
assert_raises(SQLite3::NotADatabaseException) do
|
75
|
-
Fbe::Middleware::SqliteStore.new(f).read('my_key')
|
75
|
+
Fbe::Middleware::SqliteStore.new(f, '0.0.0').read('my_key')
|
76
76
|
end
|
77
77
|
assert_match('file is not a database', ex.message)
|
78
78
|
end
|
@@ -94,7 +94,7 @@ class SqliteStoreTest < Fbe::Test
|
|
94
94
|
end
|
95
95
|
|
96
96
|
Tempfile.open('test.db') do |f|
|
97
|
-
Fbe::Middleware::SqliteStore.new(f.path).then do |s|
|
97
|
+
Fbe::Middleware::SqliteStore.new(f.path, '0.0.0').then do |s|
|
98
98
|
s.write('my_key', 'my_value')
|
99
99
|
s.read('my_key')
|
100
100
|
end
|
@@ -109,6 +109,79 @@ class SqliteStoreTest < Fbe::Test
|
|
109
109
|
assert_match('closed sqlite after process exit', out)
|
110
110
|
end
|
111
111
|
|
112
|
+
def test_different_versions
|
113
|
+
with_tmpfile('d.db') do |f|
|
114
|
+
Fbe::Middleware::SqliteStore.new(f, '0.0.1').then do |store|
|
115
|
+
store.write('kkk1', 'some value')
|
116
|
+
store.write('kkk2', 'another value')
|
117
|
+
end
|
118
|
+
Fbe::Middleware::SqliteStore.new(f, '0.0.1').then do |store|
|
119
|
+
assert_equal('some value', store.read('kkk1'))
|
120
|
+
assert_equal('another value', store.read('kkk2'))
|
121
|
+
end
|
122
|
+
Fbe::Middleware::SqliteStore.new(f, '0.0.2').then do |store|
|
123
|
+
assert_nil(store.read('kkk1'))
|
124
|
+
assert_nil(store.read('kkk2'))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_initialize_wrong_version
|
130
|
+
with_tmpfile('e.db') do |f|
|
131
|
+
msg = 'Version cannot be nil or empty'
|
132
|
+
assert_raises(ArgumentError) { Fbe::Middleware::SqliteStore.new(f, nil) }.then do |ex|
|
133
|
+
assert_match(msg, ex.message)
|
134
|
+
end
|
135
|
+
assert_raises(ArgumentError) { Fbe::Middleware::SqliteStore.new(f, '') }.then do |ex|
|
136
|
+
assert_match(msg, ex.message)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_skip_write_if_value_more_then_10k_bytes
|
142
|
+
with_tmpfile('a.db') do |f|
|
143
|
+
Fbe::Middleware::SqliteStore.new(f, '0.0.1').then do |store|
|
144
|
+
store.write('a', 'a' * 9_997)
|
145
|
+
store.write('b', 'b' * 9_998)
|
146
|
+
store.write('c', 'c' * 9_999)
|
147
|
+
store.write('d', 'd' * 10_000)
|
148
|
+
assert_equal('a' * 9_997, store.read('a'))
|
149
|
+
assert_equal('b' * 9_998, store.read('b'))
|
150
|
+
assert_nil(store.read('c'))
|
151
|
+
assert_nil(store.read('d'))
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_shrink_cache_if_more_then_10_mb
|
157
|
+
with_tmpfile('large.db') do |f|
|
158
|
+
Fbe::Middleware::SqliteStore.new(f, '0.0.1').then do |store|
|
159
|
+
key = 'aaa'
|
160
|
+
alpha = ('a'..'z').to_a
|
161
|
+
store.write('a', 'aa')
|
162
|
+
Time.stub(:now, (Time.now - (5 * 60 * 60)).round) do
|
163
|
+
store.write('b', 'bb')
|
164
|
+
store.write('c', 'cc')
|
165
|
+
end
|
166
|
+
assert_equal('cc', store.read('c'))
|
167
|
+
Time.stub(:now, rand((Time.now - (5 * 60 * 60))..Time.now).round) do
|
168
|
+
10_240.times do
|
169
|
+
value = alpha.sample * rand(1024..2048)
|
170
|
+
store.write(key, value)
|
171
|
+
key = key.next
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
assert_operator(File.size(f), :>, 10 * 1024 * 1024)
|
176
|
+
Fbe::Middleware::SqliteStore.new(f, '0.0.1').then do |store|
|
177
|
+
assert_equal('aa', store.read('a'))
|
178
|
+
assert_nil(store.read('b'))
|
179
|
+
assert_equal('cc', store.read('c'))
|
180
|
+
assert_operator(File.size(f), :<=, 10 * 1024 * 1024)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
112
185
|
private
|
113
186
|
|
114
187
|
def with_tmpfile(name = 'test.db', &)
|
data/test/fbe/test_octo.rb
CHANGED
@@ -405,4 +405,45 @@ class TestOcto < Fbe::Test
|
|
405
405
|
assert_path_exists(file)
|
406
406
|
end
|
407
407
|
end
|
408
|
+
|
409
|
+
def test_sqlite_store_for_use_in_different_versions
|
410
|
+
WebMock.disable_net_connect!
|
411
|
+
Dir.mktmpdir do |dir|
|
412
|
+
global = {}
|
413
|
+
stub =
|
414
|
+
stub_request(:get, 'https://api.github.com/user/42')
|
415
|
+
.to_return(
|
416
|
+
status: 200,
|
417
|
+
body: { login: 'user1' }.to_json,
|
418
|
+
headers: {
|
419
|
+
'Content-Type' => 'application/json',
|
420
|
+
'Cache-Control' => 'public, max-age=60, s-maxage=60',
|
421
|
+
'Etag' => 'W/"2ff9dd4c3153f006830b2b8b721f6a4bb400a1eb81a2e1fa0a3b846ad349b9ec"',
|
422
|
+
'Last-Modified' => 'Wed, 01 May 2025 20:00:00 GMT'
|
423
|
+
}
|
424
|
+
)
|
425
|
+
sqlite_cache = File.expand_path('test.db', dir)
|
426
|
+
Fbe.stub_const(:VERSION, '0.0.1') do
|
427
|
+
o = Fbe.octo(loog: Loog::NULL, global:, options: Judges::Options.new({ 'sqlite_cache' => sqlite_cache }))
|
428
|
+
assert_equal('user1', o.user_name_by_id(42))
|
429
|
+
end
|
430
|
+
WebMock.remove_request_stub(stub)
|
431
|
+
stub_request(:get, 'https://api.github.com/user/42')
|
432
|
+
.to_return(
|
433
|
+
status: 200,
|
434
|
+
body: { login: 'user2' }.to_json,
|
435
|
+
headers: {
|
436
|
+
'Content-Type' => 'application/json',
|
437
|
+
'Cache-Control' => 'public, max-age=60, s-maxage=60',
|
438
|
+
'Etag' => 'W/"2ff9dd4c3153f006830b2b8b721f6a4bb400a1eb81a2e1fa0a3b846ad349b9ec"',
|
439
|
+
'Last-Modified' => 'Wed, 01 May 2025 20:00:00 GMT'
|
440
|
+
}
|
441
|
+
)
|
442
|
+
global = {}
|
443
|
+
Fbe.stub_const(:VERSION, '0.0.2') do
|
444
|
+
o = Fbe.octo(loog: Loog::NULL, global:, options: Judges::Options.new({ 'sqlite_cache' => sqlite_cache }))
|
445
|
+
assert_equal('user2', o.user_name_by_id(42))
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
408
449
|
end
|
data/test/test__helper.rb
CHANGED