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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7330eb4955ee45e4ff4fcad4580a6fcd2f55ad48
4
- data.tar.gz: e61edd740a283e28ec308600a271a095d199a44b
2
+ SHA256:
3
+ metadata.gz: e72d3b11da68461fa95e282ad1f8727c0c397f87602a01557754a438c111f88d
4
+ data.tar.gz: a79ab553fd00bb63b89c584dda277c4faba58ce82d4095a7a73a16407d4995ca
5
5
  SHA512:
6
- metadata.gz: 5a9e62798f1fcc24a298e5d36cf8896c99aed6d3070a0d62dfec72a2c2279949acf003030ed5bbdcd9bd442b30eef9fb56cb9d44b98821f1365024383345403c
7
- data.tar.gz: dfc7717490b507a35ad4020aca46d3dd07dc1d2d773b1b6245ea4da1d06d61de78e4f85420cff6fa47e3d8f112f6cb70b4e57211b6be94fbceec099e2c66cf29
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
- [![Build Status](https://travis-ci.org/anamartinez/large_object_store.png)](https://travis-ci.org/anamartinez/large_object_store)
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)
@@ -1,3 +1,3 @@
1
1
  module LargeObjectStore
2
- VERSION = "1.3.4"
2
+ VERSION = "1.7.0"
3
3
  end
@@ -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 = 3
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(store)
18
- RailsWrapper.new(store)
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
- # calculate slice size; note that key length is a factor because
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), options.merge(raw: true))
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 = Marshal.dump(value)
132
+ value = @serializer.dump(value)
114
133
  end
115
134
 
116
135
  if compress?(value, options)
117
136
  flag |= COMPRESSED
118
- value = Zlib::Deflate.deflate(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 = Zlib::Inflate.inflate(data) if flag & COMPRESSED == COMPRESSED
129
- data = Marshal.load(data) if flag & RAW != RAW
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.delete(:compress)
136
- compress_limit = options.delete(:compress_limit) || DEFAULT_COMPRESS_LIMIT
137
- value.bytesize > compress_limit
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.3.4
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: 2018-01-27 00:00:00.000000000 Z
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.0'
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
- rubyforge_project:
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