large_object_store 1.3.4 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![
|
49
|
+
[![CI](https://github.com/anamartinez/large_object_store/actions/workflows/actions.yml/badge.svg)](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
|