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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df9755e6d4d7bf5ae3a716031216786929ddfcdf328a3f026b308e9bbfc93f92
4
- data.tar.gz: ed1cfe9f9877a3e3e1b77f30f4b9c8ca525885306d100058d5397019ca1005e6
3
+ metadata.gz: 8c33c559724c991d4aabc2ad9c097170e53de0002e362dcfadfe956d77eed7ec
4
+ data.tar.gz: 787dbc8ce12ebacbd0b96cd8174ced19aa11665f29d3d410fb93c397dbb80d87
5
5
  SHA512:
6
- metadata.gz: 3bfc015d2f0c47035fae903627404da6fafcd823c14abdcdc8a37694fea73bbb4edd1f1467497379fff51e62388b741a205b28bcd0ee8a4b5c28aceaf0f6c22c
7
- data.tar.gz: dc92c946e0d1bd14deb3d29a73b36f9317d7e06597ac2ec4fc05030ed9afea415830a71f101eb54ddd2793a02a3c6b480d2a2b95482f3deb0d62903230b1432e
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.0)
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.0)
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 { _1.execute('SELECT value FROM cache WHERE key = ? LIMIT 1', [key]) }.dig(0, 0)
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 { _1.execute 'DELETE FROM cache;' }
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 'CREATE TABLE IF NOT EXISTS cache(key TEXT UNIQUE NOT NULL, value TEXT);'
66
- t.execute 'CREATE INDEX IF NOT EXISTS key_idx ON cache(key);'
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
@@ -10,5 +10,5 @@
10
10
  # License:: MIT
11
11
  module Fbe
12
12
  # Current version of the gem (changed by +.rultor.yml+ on every release)
13
- VERSION = '0.19.3' unless const_defined?(:VERSION)
13
+ VERSION = '0.20.0' unless const_defined?(:VERSION)
14
14
  end
@@ -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', &)
@@ -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
@@ -31,6 +31,7 @@ end
31
31
  require 'minitest/reporters'
32
32
  Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
33
33
 
34
+ require 'minitest/stub_const'
34
35
  require 'minitest/autorun'
35
36
  require 'webmock/minitest'
36
37
  require_relative '../lib/fbe'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fbe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.3
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko