large_object_store 1.3.4 → 1.7.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 +5 -5
- data/Readme.md +14 -1
- data/lib/large_object_store/version.rb +1 -1
- data/lib/large_object_store.rb +65 -21
- metadata +19 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e72d3b11da68461fa95e282ad1f8727c0c397f87602a01557754a438c111f88d
|
4
|
+
data.tar.gz: a79ab553fd00bb63b89c584dda277c4faba58ce82d4095a7a73a16407d4995ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2eab6d7bf39128dda4096fc96dc583128e08d0b08acbce8ea879611be48c03024aacc2f0cb55d27827988ee024c1d291495f8c85bdeb12ba55becd34a634407
|
7
|
+
data.tar.gz: b991e5e5337333eeb39daade26edad8957819f8de768f2632df09f77873d9f351f5913d7dac19d5595115d3624369c88a8ddc9fd90a6967028db522221cadd96
|
data/Readme.md
CHANGED
@@ -26,6 +26,19 @@ store.write("a" * 1000, compress: true, compress_limit: 100) # compress when gre
|
|
26
26
|
store.write("a" * 1000, raw: true) # store as string to avoid marshaling overhead
|
27
27
|
```
|
28
28
|
|
29
|
+
zstd
|
30
|
+
====
|
31
|
+
|
32
|
+
[zstd compression](https://engineering.fb.com/2016/08/31/core-data/smaller-and-faster-data-compression-with-zstandard/), a modern improvement over the venerable zlib compression algorithm, is supported by passing the `zstd` flag when writing items:
|
33
|
+
|
34
|
+
```
|
35
|
+
store.write("a" * 10_000_000, compress: true, zstd: true)
|
36
|
+
```
|
37
|
+
|
38
|
+
For backwards compatibility and to enable safe roll-out of the change in working systems, the `zstd` flag defaults to `false`.
|
39
|
+
|
40
|
+
zstd decompression is used when the zstd magic number is detected at the beginning of compressed data, so `zstd: true` does not need to be passed when reading/fetching items.
|
41
|
+
|
29
42
|
Author
|
30
43
|
======
|
31
44
|
[Ana Martinez](https://github.com/anamartinez)<br/>
|
@@ -33,4 +46,4 @@ acemacu@gmail.com<br/>
|
|
33
46
|
[Michael Grosser](https://github.com/grosser)<br/>
|
34
47
|
michael@grosser.it<br/>
|
35
48
|
License: MIT<br/>
|
36
|
-
[](https://github.com/anamartinez/large_object_store/actions/workflows/actions.yml)
|
data/lib/large_object_store.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
require "large_object_store/version"
|
2
2
|
require "zlib"
|
3
|
+
require "zstd-ruby"
|
3
4
|
require "securerandom"
|
4
5
|
|
5
6
|
module LargeObjectStore
|
6
7
|
UUID_BYTES = 16
|
7
8
|
UUID_SIZE = UUID_BYTES * 2
|
8
|
-
CACHE_VERSION =
|
9
|
+
CACHE_VERSION = 4
|
9
10
|
MAX_OBJECT_SIZE = 1024**2
|
10
11
|
ITEM_HEADER_SIZE = 100
|
11
12
|
DEFAULT_COMPRESS_LIMIT = 16*1024
|
@@ -13,35 +14,40 @@ module LargeObjectStore
|
|
13
14
|
COMPRESSED = 1
|
14
15
|
RAW = 2
|
15
16
|
FLAG_RADIX = 32 # we can store 32 different states
|
17
|
+
ZSTD_MAGIC = "\x28\xB5\x2F\xFD".force_encoding('ASCII-8BIT')
|
18
|
+
ZSTD_COMPRESS_LEVEL = 3 # Default level recommended by zstd authors
|
16
19
|
|
17
|
-
def self.wrap(
|
18
|
-
RailsWrapper.new(
|
20
|
+
def self.wrap(*args)
|
21
|
+
RailsWrapper.new(*args)
|
22
|
+
end
|
23
|
+
class << self
|
24
|
+
ruby2_keywords :wrap if respond_to?(:ruby2_keywords, true)
|
19
25
|
end
|
20
26
|
|
21
27
|
class RailsWrapper
|
22
28
|
attr_reader :store
|
23
29
|
|
24
|
-
def initialize(store)
|
30
|
+
def initialize(store, serializer: Marshal, max_slice_size: MAX_OBJECT_SIZE)
|
25
31
|
@store = store
|
32
|
+
@serializer = serializer
|
33
|
+
@max_slice_size = [max_slice_size, MAX_OBJECT_SIZE].min
|
34
|
+
@namespace = (store.respond_to?(:options) && store.options[:namespace]) || ""
|
26
35
|
end
|
27
36
|
|
28
|
-
def write(key, value, options
|
37
|
+
def write(key, value, **options)
|
29
38
|
options = options.dup
|
30
39
|
value = serialize(value, options)
|
31
40
|
|
32
|
-
|
33
|
-
# the key is stored on the same slab page as the value
|
34
|
-
slice_size = MAX_OBJECT_SIZE - ITEM_HEADER_SIZE - UUID_SIZE - key.bytesize
|
35
|
-
|
41
|
+
slice_size = safe_slice_size(key)
|
36
42
|
# store number of pages
|
37
43
|
pages = (value.size / slice_size.to_f).ceil
|
38
44
|
|
39
45
|
if pages == 1
|
40
|
-
!!@store.write(key(key, 0), value, options)
|
46
|
+
!!@store.write(key(key, 0), value, **options)
|
41
47
|
else
|
42
48
|
# store meta
|
43
49
|
uuid = SecureRandom.hex(UUID_BYTES)
|
44
|
-
return false unless @store.write(key(key, 0), [pages, uuid], options) # invalidates the old cache
|
50
|
+
return false unless @store.write(key(key, 0), [pages, uuid], **options) # invalidates the old cache
|
45
51
|
|
46
52
|
# store object
|
47
53
|
page = 1
|
@@ -49,7 +55,7 @@ module LargeObjectStore
|
|
49
55
|
slice = value.slice!(0, slice_size)
|
50
56
|
break if slice.size == 0
|
51
57
|
|
52
|
-
return false unless @store.write(key(key, page), slice.prepend(uuid),
|
58
|
+
return false unless @store.write(key(key, page), slice.prepend(uuid), raw: true, **options)
|
53
59
|
page += 1
|
54
60
|
end
|
55
61
|
true
|
@@ -83,11 +89,11 @@ module LargeObjectStore
|
|
83
89
|
deserialize(data)
|
84
90
|
end
|
85
91
|
|
86
|
-
def fetch(key, options
|
92
|
+
def fetch(key, **options)
|
87
93
|
value = read(key)
|
88
94
|
return value unless value.nil?
|
89
95
|
value = yield
|
90
|
-
write(key, value, options)
|
96
|
+
write(key, value, **options)
|
91
97
|
value
|
92
98
|
end
|
93
99
|
|
@@ -101,6 +107,19 @@ module LargeObjectStore
|
|
101
107
|
|
102
108
|
private
|
103
109
|
|
110
|
+
# calculate slice size; note that key length is a factor because
|
111
|
+
# the key is stored on the same slab page as the value
|
112
|
+
def safe_slice_size(key)
|
113
|
+
namespace_length = @namespace.empty? ? 0 : @namespace.size + 1
|
114
|
+
overhead = ITEM_HEADER_SIZE + UUID_SIZE + key.bytesize + namespace_length
|
115
|
+
slice_size = @max_slice_size - overhead
|
116
|
+
if slice_size <= 0
|
117
|
+
MAX_OBJECT_SIZE - overhead
|
118
|
+
else
|
119
|
+
slice_size
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
104
123
|
# convert a object to a string
|
105
124
|
# modifies options
|
106
125
|
def serialize(value, options)
|
@@ -110,31 +129,56 @@ module LargeObjectStore
|
|
110
129
|
flag |= RAW
|
111
130
|
value = value.to_s
|
112
131
|
else
|
113
|
-
value =
|
132
|
+
value = @serializer.dump(value)
|
114
133
|
end
|
115
134
|
|
116
135
|
if compress?(value, options)
|
117
136
|
flag |= COMPRESSED
|
118
|
-
value =
|
137
|
+
value = compress(value, options)
|
119
138
|
end
|
120
139
|
|
121
140
|
value.prepend(flag.to_s(FLAG_RADIX))
|
122
141
|
end
|
123
142
|
|
143
|
+
def compress(value, options)
|
144
|
+
if options[:zstd]
|
145
|
+
Zstd.compress(value, ZSTD_COMPRESS_LEVEL)
|
146
|
+
else
|
147
|
+
Zlib::Deflate.deflate(value)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def decompress(data)
|
152
|
+
if data.start_with?(ZSTD_MAGIC)
|
153
|
+
Zstd.decompress(data)
|
154
|
+
else
|
155
|
+
Zlib::Inflate.inflate(data)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
124
159
|
# opposite operations and order of serialize
|
125
160
|
def deserialize(raw_data)
|
126
161
|
data = raw_data.dup
|
127
162
|
flag = data.slice!(0, 1).to_i(FLAG_RADIX)
|
128
|
-
data =
|
129
|
-
data =
|
163
|
+
data = decompress(data) if flag & COMPRESSED == COMPRESSED
|
164
|
+
data = @serializer.load(data) if flag & RAW != RAW
|
130
165
|
data
|
131
166
|
end
|
132
167
|
|
133
168
|
# Don't pass compression on to Rails, we're doing it ourselves.
|
134
169
|
def compress?(value, options)
|
135
|
-
return unless options
|
136
|
-
|
137
|
-
|
170
|
+
return unless options[:compress]
|
171
|
+
|
172
|
+
compress_limit = options[:compress_limit] || DEFAULT_COMPRESS_LIMIT
|
173
|
+
should_compress = value.bytesize > compress_limit
|
174
|
+
|
175
|
+
if should_compress
|
176
|
+
# Pass compress: false to Rails in case the default is true
|
177
|
+
options[:compress] = false
|
178
|
+
options.delete(:compress_limit)
|
179
|
+
end
|
180
|
+
|
181
|
+
should_compress
|
138
182
|
end
|
139
183
|
|
140
184
|
def key(key, i)
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: large_object_store
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ana Martinez
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
11
|
+
date: 2024-01-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: zstd-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.5.5
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.5.5
|
13
27
|
description:
|
14
28
|
email: acemacu@gmail.com
|
15
29
|
executables: []
|
@@ -31,15 +45,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
31
45
|
requirements:
|
32
46
|
- - ">="
|
33
47
|
- !ruby/object:Gem::Version
|
34
|
-
version: '2.
|
48
|
+
version: '2.6'
|
35
49
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
50
|
requirements:
|
37
51
|
- - ">="
|
38
52
|
- !ruby/object:Gem::Version
|
39
53
|
version: '0'
|
40
54
|
requirements: []
|
41
|
-
|
42
|
-
rubygems_version: 2.6.14
|
55
|
+
rubygems_version: 3.5.3
|
43
56
|
signing_key:
|
44
57
|
specification_version: 4
|
45
58
|
summary: Store large objects in memcache or others
|