lutaml-store 0.3.5 → 0.3.7
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/Rakefile +45 -0
- data/lib/lutaml/store/adapter/filesystem.rb +41 -2
- data/lib/lutaml/store/adapter/sqlite.rb +4 -3
- data/lib/lutaml/store/version.rb +1 -1
- 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: f523f159520c178e2855aa2070f1f7c19b39c4e6ddb21dd6efb9ee00dd786c55
|
|
4
|
+
data.tar.gz: 2184b3afe0a9fb1d697fd8fdf8f2f09f1d8664f30e389c3c41d7e33945f2f704
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b63ebe9a382678154f315a5716c49ecfe7481ba3ec8d08b703cd89870e50cec980af56ade9cddb7f368e0e5e8f5798aa61b53061f9348c9a2d2335852d553859
|
|
7
|
+
data.tar.gz: ac4351a3a80e045d254a057026309b2e2cd4228598adfff7ff20ddd45b247b9ecbab82b92413a7acb99e14f20dcaa45b5ab30d5c40a2bb7b4ede64f0e4533262
|
data/Rakefile
CHANGED
|
@@ -10,3 +10,48 @@ require "rubocop/rake_task"
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
|
12
12
|
task default: %i[spec rubocop]
|
|
13
|
+
|
|
14
|
+
desc "Run adapter benchmarks and print a comparison report"
|
|
15
|
+
task :"performance:compare" do
|
|
16
|
+
$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
|
|
17
|
+
require "lutaml/store"
|
|
18
|
+
require "tmpdir"
|
|
19
|
+
require "fileutils"
|
|
20
|
+
|
|
21
|
+
ops = 2000
|
|
22
|
+
data = Array.new(100) { |i| { "id" => i, "name" => "item-#{i}" } }
|
|
23
|
+
|
|
24
|
+
measure = lambda do |label, &operation|
|
|
25
|
+
GC.start
|
|
26
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
+
operation.call
|
|
28
|
+
format("%<label>-34s %<us>9.1fus/op", label: label,
|
|
29
|
+
us: (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1_000_000 / ops)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
results = []
|
|
33
|
+
Dir.mktmpdir do |dir|
|
|
34
|
+
memory = Lutaml::Store::Adapter::Memory.new
|
|
35
|
+
results << measure.call("memory set") { ops.times { |i| memory.set("k#{i}", data[i % 100]) } }
|
|
36
|
+
results << measure.call("memory get") { ops.times { |i| memory.get("k#{i % 500}") } }
|
|
37
|
+
|
|
38
|
+
fs = Lutaml::Store::Adapter::FileSystem.new(path: File.join(dir, "fs"))
|
|
39
|
+
results << measure.call("filesystem set") { ops.times { |i| fs.set("k#{i}", data[i % 100]) } }
|
|
40
|
+
results << measure.call("filesystem set (unchanged)") { ops.times { |i| fs.set("k#{i}", data[i % 100]) } }
|
|
41
|
+
results << measure.call("filesystem get") { ops.times { |i| fs.get("k#{i % 500}") } }
|
|
42
|
+
fs.close
|
|
43
|
+
FileUtils.rm_rf(File.join(dir, "fs"))
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
require "sqlite3"
|
|
47
|
+
sqlite = Lutaml::Store::Adapter::Sqlite.new(path: File.join(dir, "db.sqlite3"))
|
|
48
|
+
results << measure.call("sqlite set") { ops.times { |i| sqlite.set("k#{i}", data[i % 100]) } }
|
|
49
|
+
results << measure.call("sqlite get") { ops.times { |i| sqlite.get("k#{i % 500}") } }
|
|
50
|
+
sqlite.close
|
|
51
|
+
rescue LoadError
|
|
52
|
+
results << "sqlite3 gem unavailable, sqlite benchmarks skipped"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts results
|
|
57
|
+
end
|
|
@@ -53,6 +53,7 @@ module Lutaml
|
|
|
53
53
|
@integrity_algorithm = @config.fetch(:integrity_algorithm, "sha256")
|
|
54
54
|
@lock_path = File.expand_path(File.join(@root_path, LOCK_FILE))
|
|
55
55
|
@monitor = self.class.monitor_for(@lock_path)
|
|
56
|
+
@metadata_cache = {}
|
|
56
57
|
|
|
57
58
|
setup_directory_structure
|
|
58
59
|
end
|
|
@@ -72,7 +73,16 @@ module Lutaml
|
|
|
72
73
|
|
|
73
74
|
def set(key, value)
|
|
74
75
|
content, format = encode_value(value)
|
|
75
|
-
with_lock(:ex)
|
|
76
|
+
with_lock(:ex) do
|
|
77
|
+
# Writing costs two fsync-backed temp files plus renames, and
|
|
78
|
+
# idempotent rewrites (periodic model saves of unchanged data)
|
|
79
|
+
# gain nothing from them, so an entry whose stored bytes and
|
|
80
|
+
# metadata are both unchanged is left untouched
|
|
81
|
+
file_path = existing_path_for_key(key)
|
|
82
|
+
next value if file_path && entry_unchanged?(file_path, content, format)
|
|
83
|
+
|
|
84
|
+
write_entry(key, content, format)
|
|
85
|
+
end
|
|
76
86
|
value
|
|
77
87
|
end
|
|
78
88
|
|
|
@@ -283,7 +293,7 @@ module Lutaml
|
|
|
283
293
|
|
|
284
294
|
def read_verified_at(file_path, _key)
|
|
285
295
|
data = file_safe_read(file_path)
|
|
286
|
-
metadata =
|
|
296
|
+
metadata = cached_metadata(file_path)
|
|
287
297
|
|
|
288
298
|
begin
|
|
289
299
|
verify_data_integrity(data, metadata)
|
|
@@ -294,6 +304,11 @@ module Lutaml
|
|
|
294
304
|
[:ok, decode_value(data, metadata[:format])]
|
|
295
305
|
end
|
|
296
306
|
|
|
307
|
+
def entry_unchanged?(file_path, content, format)
|
|
308
|
+
File.binread(file_path) == content &&
|
|
309
|
+
read_metadata(metadata_path_for(file_path))[:format] == format
|
|
310
|
+
end
|
|
311
|
+
|
|
297
312
|
def write_entry(key, content, format)
|
|
298
313
|
file_path = path_for_key(key)
|
|
299
314
|
metadata_path = metadata_path_for(file_path)
|
|
@@ -434,6 +449,30 @@ module Lutaml
|
|
|
434
449
|
{}
|
|
435
450
|
end
|
|
436
451
|
|
|
452
|
+
# Reading and parsing the metadata sidecar costs a read and a JSON
|
|
453
|
+
# parse on every get; the cached copy is validated against the
|
|
454
|
+
# file's mtime, so rewrites by this or another process invalidate
|
|
455
|
+
# it. Bounded like every cache fontist/lutaml hosts live with.
|
|
456
|
+
METADATA_CACHE_MAX = 1024
|
|
457
|
+
|
|
458
|
+
def cached_metadata(file_path)
|
|
459
|
+
metadata_path = metadata_path_for(file_path)
|
|
460
|
+
return {} unless File.exist?(metadata_path)
|
|
461
|
+
|
|
462
|
+
mtime = File.mtime(metadata_path)
|
|
463
|
+
entry = @metadata_cache[metadata_path]
|
|
464
|
+
if entry&.fetch(:mtime, nil) == mtime
|
|
465
|
+
@metadata_cache.delete(metadata_path)
|
|
466
|
+
@metadata_cache[metadata_path] = entry
|
|
467
|
+
return entry[:metadata]
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
metadata = read_metadata(metadata_path)
|
|
471
|
+
@metadata_cache.shift while @metadata_cache.size >= METADATA_CACHE_MAX
|
|
472
|
+
@metadata_cache[metadata_path] = { mtime: mtime, metadata: metadata }
|
|
473
|
+
metadata
|
|
474
|
+
end
|
|
475
|
+
|
|
437
476
|
# ── Files ──
|
|
438
477
|
|
|
439
478
|
def ensure_directory_exists(dir_path)
|
|
@@ -111,10 +111,11 @@ module Lutaml
|
|
|
111
111
|
def bulk_set(key_value_pairs)
|
|
112
112
|
@db.transaction do
|
|
113
113
|
key_value_pairs.each do |key, value|
|
|
114
|
-
|
|
114
|
+
is_string = value.is_a?(String)
|
|
115
|
+
serialized_value = is_string ? value : JSON.generate(value)
|
|
115
116
|
execute_statement(
|
|
116
|
-
"INSERT OR REPLACE INTO #{@table_name} (key, value, updated_at) VALUES (?, ?, ?)",
|
|
117
|
-
[key, serialized_value, Time.now.to_f]
|
|
117
|
+
"INSERT OR REPLACE INTO #{@table_name} (key, value, is_json, updated_at) VALUES (?, ?, ?, ?)",
|
|
118
|
+
[key, serialized_value, is_string ? 0 : 1, Time.now.to_f]
|
|
118
119
|
)
|
|
119
120
|
end
|
|
120
121
|
end
|
data/lib/lutaml/store/version.rb
CHANGED