hammerspace-fork 0.1.5.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5a9b6b90f1ae4489a0e5b2a7673a5717b455f80e
4
+ data.tar.gz: efa0776dff153dff8f4adffbb3f1123fbc8b1d1f
5
+ SHA512:
6
+ metadata.gz: 363aca651c54fa6de62c6e7e3b6db799510b34472a35ebd2cde4a98864b0d4dfa12c1777dc871e6c3151aa9066bebcfcbadb96d6ebcb26342ebaded59a5b73a6
7
+ data.tar.gz: 504af3c3e04f05b3b4507f41a588d88a5c8a918bf789eca2601f946542fe64f177776703b3de0f71ffcaa04b2ba8dd5e12f17c3d90444a6a2360be2f178b95c5
@@ -0,0 +1,7 @@
1
+ default.hammerspace_development.user = 'vagrant'
2
+ default.hammerspace_development.hammerspace.home = '/home/vagrant/hammerspace'
3
+ default.hammerspace_development.hammerspace.gem_home = '/home/vagrant/.gems'
4
+ default.hammerspace_development.hammerspace.root = '/var/lib/hammerspace'
5
+
6
+ default.hammerspace_development.ruby.version = '1.9.1'
7
+ default.hammerspace_development.sparkey.version = 'daa9941221584d89da68803303854ef7e3a8f68d'
@@ -0,0 +1,6 @@
1
+ default.hammerspace_development.essential.packages = [
2
+ 'build-essential',
3
+ 'autoconf',
4
+ 'libtool',
5
+ 'git',
6
+ ]
@@ -0,0 +1,7 @@
1
+ default.hammerspace_development.sparkey.home = "/home/#{default.hammerspace_development.user}/sparkey"
2
+ default.hammerspace_development.sparkey.source_file = "https://github.com/spotify/sparkey/archive/#{node.hammerspace_development.sparkey.version}.tar.gz"
3
+ default.hammerspace_development.sparkey.local_dir = File.join(default.hammerspace_development.sparkey.home, "sparkey-#{node.hammerspace_development.sparkey.version}")
4
+
5
+ default.hammerspace_development.sparkey.packages = [
6
+ 'libsnappy-dev',
7
+ ]
@@ -0,0 +1,32 @@
1
+ include_recipe "hammerspace-development::essential"
2
+ include_recipe "hammerspace-development::sparkey"
3
+ include_recipe "hammerspace-development::ruby"
4
+
5
+ template "/home/#{node.hammerspace_development.user}/.bash_profile" do
6
+ owner node.hammerspace_development.user
7
+ group node.hammerspace_development.user
8
+ mode '0755'
9
+ end
10
+
11
+ directory node.hammerspace_development.hammerspace.gem_home do
12
+ owner node.hammerspace_development.user
13
+ group node.hammerspace_development.user
14
+ mode '0755'
15
+ recursive true
16
+ action :create
17
+ end
18
+
19
+ execute "hammerspace-bundle-install" do
20
+ cwd node.hammerspace_development.hammerspace.home
21
+ user node.hammerspace_development.user
22
+ group node.hammerspace_development.user
23
+ command "bundle install --path #{node.hammerspace_development.hammerspace.gem_home}"
24
+ end
25
+
26
+ directory node.hammerspace_development.hammerspace.root do
27
+ owner node.hammerspace_development.user
28
+ group node.hammerspace_development.user
29
+ mode '0755'
30
+ recursive true
31
+ action :create
32
+ end
@@ -0,0 +1,9 @@
1
+ execute "first-apt-get-update" do
2
+ command "apt-get update"
3
+ end
4
+
5
+ node.hammerspace_development.essential.packages.each do |p|
6
+ package p do
7
+ action :upgrade
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ package "ruby#{node.hammerspace_development.ruby.version}" do
2
+ action :upgrade
3
+ options "--force-yes"
4
+ end
5
+
6
+ package "ruby#{node.hammerspace_development.ruby.version}-dev" do
7
+ action :upgrade
8
+ options "--force-yes"
9
+ end
10
+
11
+ # other common packages needed by ruby gems
12
+ ["libxslt-dev", "libxml2-dev"].each do |p|
13
+ package p do
14
+ action :upgrade
15
+ end
16
+ end
17
+
18
+ gem_package 'bundler' do
19
+ action :install
20
+ end
21
+
@@ -0,0 +1,56 @@
1
+ sparkey_local_file = File.join(
2
+ node.hammerspace_development.sparkey.home,
3
+ File.basename(node.hammerspace_development.sparkey.source_file))
4
+
5
+ node.hammerspace_development.sparkey.packages.each do |p|
6
+ package p do
7
+ action :upgrade
8
+ end
9
+ end
10
+
11
+ directory node.hammerspace_development.sparkey.home do
12
+ owner node.hammerspace_development.user
13
+ group node.hammerspace_development.user
14
+ mode '0755'
15
+ recursive true
16
+ action :create
17
+ end
18
+
19
+ remote_file sparkey_local_file do
20
+ source node.hammerspace_development.sparkey.source_file
21
+ owner node.hammerspace_development.user
22
+ group node.hammerspace_development.user
23
+ mode "644"
24
+
25
+ action :create_if_missing
26
+ notifies :run, "execute[extract-sparkey-#{node.hammerspace_development.sparkey.version}]", :immediately
27
+ end
28
+
29
+ execute "extract-sparkey-#{node.hammerspace_development.sparkey.version}" do
30
+ cwd node.hammerspace_development.sparkey.home
31
+ user node.hammerspace_development.user
32
+ command "tar -xvzf #{sparkey_local_file}"
33
+
34
+ action :nothing
35
+ notifies :run, "bash[build-sparkey-#{node.hammerspace_development.sparkey.version}]", :immediately
36
+ end
37
+
38
+ bash "build-sparkey-#{node.hammerspace_development.sparkey.version}" do
39
+ cwd node.hammerspace_development.sparkey.local_dir
40
+ user node.hammerspace_development.user
41
+ code <<-EOS
42
+ autoreconf --install
43
+ ./configure
44
+ make
45
+ EOS
46
+
47
+ action :nothing
48
+ notifies :run, "execute[install-sparkey-#{node.hammerspace_development.sparkey.version}]", :immediately
49
+ end
50
+
51
+ execute "install-sparkey-#{node.hammerspace_development.sparkey.version}" do
52
+ cwd node.hammerspace_development.sparkey.local_dir
53
+ command "make install && sudo ldconfig"
54
+
55
+ action :nothing
56
+ end
@@ -0,0 +1,2 @@
1
+ export HAMMERSPACE_ROOT=<%= node.hammerspace_development.hammerspace.root %>
2
+ cd <%= node.hammerspace_development.hammerspace.home %>
@@ -0,0 +1,6 @@
1
+ name "hammerspace-development"
2
+ description "development virtual machine for hammerspace"
3
+
4
+ run_list(
5
+ "recipe[hammerspace-development]",
6
+ )
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .rvmrc
4
+ .DS_Store
5
+ /coverage
6
+
7
+ # IDEs
8
+ .idea
@@ -0,0 +1,30 @@
1
+ # v0.1.4
2
+ * Upgrade to gnista 0.0.5.
3
+ * Remove work around for gnista bug.
4
+
5
+ # v0.1.3
6
+ * Work around gnista bug that causes ruby crashes on OS X.
7
+ * Upgrade to sparkey 0.2.0 in vagrant.
8
+
9
+ # v0.1.2
10
+ * Support vagrant for local development.
11
+ * Remove dependency on colored gem.
12
+ * Add MIT license.
13
+ * Documentation updates.
14
+
15
+ # v0.1.1
16
+ * Expose the uid of the directory that the current reader is reading from.
17
+ * Documentation updates.
18
+
19
+ # v0.1.0
20
+ * Change semantics of block passed to constructor, now used to specify default_proc.
21
+ * Add support for most Ruby Hash methods.
22
+ * Major internal refactor, new HashMethods module allows new backends to be written more easily.
23
+ * Add documentation.
24
+
25
+ # v0.0.2
26
+ * Add support for multiple writers with last-write-wins semantics.
27
+ * Implement `clear` method.
28
+
29
+ # v0.0.1
30
+ * Initial release.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'trollop', '~> 2.0'
5
+
6
+ group :test do
7
+ gem 'rspec', '~> 2.13.0'
8
+ gem 'rspec-instafail', '~> 0.2'
9
+ gem 'simplecov', :require => false, :group => :test
10
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Airbnb, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,523 @@
1
+ Hammerspace
2
+ ===========
3
+
4
+ Hash-like interface to persistent, concurrent, off-heap storage
5
+
6
+ **Notice:** This is a forked release of hammerspace, which has been released to RubyGems as `hammerspace-fork`. It is [upstream hammerspace](https://github.com/airbnb/hammerspace) plus [my PR to correct the gnista dependency](https://github.com/airbnb/hammerspace/pull/8)
7
+
8
+ ## What is Hammerspace?
9
+
10
+ _[Hammerspace](http://en.wikipedia.org/wiki/Hammerspace) ... is a
11
+ fan-envisioned extradimensional, instantly accessible storage area in fiction,
12
+ which is used to explain how animated, comic, and game characters can produce
13
+ objects out of thin air._
14
+
15
+ This gem provides persistent, concurrently-accessible off-heap storage of
16
+ strings with a familiar hash-like interface. It is optimized for bulk writes
17
+ and random reads.
18
+
19
+
20
+ ## Motivation
21
+
22
+ Applications often use data that never changes or changes very infrequently. In
23
+ many cases, some latency is acceptable when accessing this data. For example, a
24
+ user's profile may be loaded from a web service, a database, or an external
25
+ shared cache like memcache. In other cases, latency is much more sensitive. For
26
+ example, translations may be used many times and incurring even a ~2ms delay to
27
+ access them from an external cache would be prohibitively slow.
28
+
29
+ To work around the performance issue, this type of data is often loaded into
30
+ the application at startup. Unfortunately, this means the data is stored on the
31
+ heap, where the garbage collector must scan over the objects on every run (at
32
+ least in the case of Ruby MRI). Further, for application servers that utilize
33
+ multiple processes, each process has its own copy of the data which is an
34
+ inefficient use of memory.
35
+
36
+ Hammerspace solves these problems by moving the data off the heap onto disk.
37
+ Leveraging libraries and data structures optimized for bulk writes and random
38
+ reads allows an acceptable level of performance to be maintained. Because the
39
+ data is persistent, it does not need to be reloaded from an external cache or
40
+ service on application startup unless the data has changed.
41
+
42
+ Unfortunately, these low-level libraries don't always support concurrent
43
+ writers. Hammerspace adds concurrency control to allow multiple processes to
44
+ update and read from a single shared copy of the data safely. Finally,
45
+ hammerspace's interface is designed to mimic Ruby's `Hash` to make integrating
46
+ with existing applications simple and straightforward. Different low-level
47
+ libraries can be used by implementing a new backend that uses the library.
48
+ (Currently, only [Sparkey](https://github.com/spotify/sparkey) is supported.)
49
+ Backends only need to implement a small set of methods (`[]`, `[]=`, `close`,
50
+ `delete`, `each`, `uid`), but can override the default implementation of other
51
+ methods if the underlying library supports more efficient implementations.
52
+
53
+ ## Installation
54
+
55
+ ### Requirements
56
+
57
+ * [Gnista](https://github.com/emnl/gnista), Ruby bindings for Sparkey
58
+ * [Sparkey](https://github.com/spotify/sparkey), constant key/value storage library
59
+ * [Snappy](https://code.google.com/p/snappy/), compression/decompression library (unused, but required to compile Sparkey)
60
+ * A filesystem that supports `flock(2)` and unlinking files/directories with outstanding file descriptors (ext3/4 will do just fine)
61
+
62
+
63
+ ### Installation
64
+
65
+ Add the following line to your Gemfile:
66
+
67
+ gem 'hammerspace'
68
+
69
+ Then run:
70
+
71
+ bundle
72
+
73
+ ### Vagrant
74
+
75
+ To make development easier, the source tree contains a Vagrantfile and a small
76
+ cookbook to install all the prerequisites. The vagrant environment also serves
77
+ as a consistent environment to run the test suite.
78
+
79
+ To use it, make sure you have vagrant installed, then:
80
+
81
+ vagrant up
82
+ vagrant ssh
83
+ bundle exec rspec
84
+
85
+
86
+ ## Usage
87
+
88
+ ### Getting Started
89
+
90
+ For the most part, hammerspace acts like a Ruby hash. But since it's a hash
91
+ that persists on disk, you have to tell it where to store the files. The
92
+ enclosing directory and any parent directories are created if they don't
93
+ already exist.
94
+
95
+ ```ruby
96
+ h = Hammerspace.new("/tmp/hammerspace")
97
+
98
+ h["cartoons"] = "mallets"
99
+ h["games"] = "inventory"
100
+ h["rubyists"] = "data"
101
+
102
+ h.size #=> 3
103
+ h["cartoons"] #=> "mallets"
104
+
105
+ h.map { |k,v| "#{k.capitalize} use hammerspace to store #{v}." }
106
+
107
+ h.close
108
+ ```
109
+
110
+ You should call `close` on the hammerspace object when you're done with it.
111
+ This flushes any pending writes to disk and closes any open file handles.
112
+
113
+
114
+ ### Options
115
+
116
+ The constructor takes a hash of options as an optional second argument.
117
+ Currently the only option supported is `:backend` which specifies which backend
118
+ class to use. Since there is only one backend supported at this time, there is
119
+ currently no reason to pass this argument.
120
+
121
+ ```ruby
122
+ h = Hammerspace.new("/tmp/hammerspace", {:backend => Hammerspace::Backend::Sparkey})
123
+ ```
124
+
125
+
126
+ ### Default Values
127
+
128
+ The constructor takes a default value as an optional third argument. This
129
+ functions the same as Ruby's `Hash`, except with `Hash` it is the first
130
+ argument.
131
+
132
+ ```ruby
133
+ h = Hammerspace.new("/tmp/hammerspace", {}, "default")
134
+ h["foo"] = "bar"
135
+ h["foo"] #=> "bar"
136
+ h["new"] #=> "default"
137
+ h.close
138
+ ```
139
+
140
+ The constructor also takes a block to specify a default Proc, which works the
141
+ same way as Ruby's `Hash`. As with `Hash`, it is the block's responsibility to
142
+ store the value in the hash if required.
143
+
144
+ ```ruby
145
+ h = Hammerspace.new("/tmp/hammerspace") { |hash, key| hash[key] = "#{key} (default)" }
146
+ h["new"] #=> "new (default)"
147
+ h.has_key?("new") #=> true
148
+ h.close
149
+ ```
150
+
151
+
152
+ ### Supported Data Types
153
+
154
+ Only string keys and values are supported.
155
+
156
+ ```ruby
157
+ h = Hammerspace.new("/tmp/hammerspace")
158
+ h[1] = "foo" #=> TypeError
159
+ h["fixnum"] = 8 #=> TypeError
160
+ h["nil"] = nil #=> TypeError
161
+ h.close
162
+ ```
163
+
164
+ Ruby hashes store references to objects, but hammerspace stores raw bytes. A
165
+ new Ruby `String` object is created from those bytes when a key is accessed.
166
+
167
+ ```ruby
168
+ value = "bar"
169
+
170
+ hash = {"foo" => value}
171
+ hash["foo"] == value #=> true
172
+ hash["foo"].equal?(value) #=> true
173
+
174
+ hammerspace = Hammerspace.new("/tmp/hammerspace")
175
+ hammerspace["foo"] = value
176
+ hammerspace["foo"] == value #=> true
177
+ hammerspace["foo"].equal?(value) #=> false
178
+ hammerspace.close
179
+ ```
180
+
181
+ Since every access results in a new `String` object, mutating values doesn't
182
+ work unless you create an explicit reference to the string.
183
+
184
+ ```ruby
185
+ h = Hammerspace.new("/tmp/hammerspace")
186
+ h["foo"] = "bar"
187
+
188
+ # This doesn't work like Ruby's Hash because every access creates a new object
189
+ h["foo"].upcase!
190
+ h["foo"] #=> "bar"
191
+
192
+ # An explicit reference is required
193
+ value = h["foo"]
194
+ value.upcase!
195
+ value #=> "BAR"
196
+
197
+ # Another access, another a new object
198
+ h["foo"] #=> "bar"
199
+
200
+ h.close
201
+ ```
202
+
203
+ This also imples that strings "lose" their encoding when retrieved from
204
+ hammerspace.
205
+
206
+ ```ruby
207
+ value = "bar"
208
+ value.encoding #=> #<Encoding:UTF-8>
209
+
210
+ h = Hammerspace.new("/tmp/hammerspace")
211
+ h["foo"] = value
212
+ h["foo"].encoding #=> #<Encoding:ASCII-8BIT>
213
+ h.close
214
+ ```
215
+
216
+ If you require strings in UTF-8, make sure strings are encoded as UTF-8 when
217
+ storing the key, then force the encoding to be UTF-8 when accessing the key.
218
+
219
+ ```ruby
220
+ h[key] = value.encode('utf-8')
221
+ value = h[key].force_encoding('utf-8')
222
+ ```
223
+
224
+
225
+ ### Persistence
226
+
227
+ Hammerspace objects are backed by files on disk, so even a new object may
228
+ already have data in it.
229
+
230
+ ```ruby
231
+ h = Hammerspace.new("/tmp/hammerspace")
232
+ h["foo"] = "bar"
233
+ h.close
234
+
235
+ h = Hammerspace.new("/tmp/hammerspace")
236
+ h["foo"] #=> "bar"
237
+ h.close
238
+ ```
239
+
240
+ Calling `clear` deletes the data files on disk. The parent directory is not
241
+ removed, nor is it guaranteed to be empty. Some files containing metadata may
242
+ still be present, e.g., lock files.
243
+
244
+
245
+ ### Concurrency
246
+
247
+ Multiple concurrent readers are supported. Readers are isolated from writers,
248
+ i.e., reads are consistent to the time that the reader was opened. Note that
249
+ the reader opens its files lazily on first read, not when the hammerspace
250
+ object is created.
251
+
252
+ ```ruby
253
+ h = Hammerspace.new("/tmp/hammerspace")
254
+ h["foo"] = "bar"
255
+ h.close
256
+
257
+ reader1 = Hammerspace.new("/tmp/hammerspace")
258
+ reader1["foo"] #=> "bar"
259
+
260
+ writer = Hammerspace.new("/tmp/hammerspace")
261
+ writer["foo"] = "updated"
262
+ writer.close
263
+
264
+ # Still "bar" because reader1 opened its files before the write
265
+ reader1["foo"] #=> "bar"
266
+
267
+ # Updated key is visible because reader2 opened its files after the write
268
+ reader2 = Hammerspace.new("/tmp/hammerspace")
269
+ reader2["foo"] #=> "updated"
270
+ reader2.close
271
+
272
+ reader1.close
273
+ ```
274
+
275
+ A new hammerspace object does not necessarily need to be created. Calling
276
+ `close` will close the files, then the reader will open them lazily again on
277
+ the next read.
278
+
279
+ ```ruby
280
+ h = Hammerspace.new("/tmp/hammerspace")
281
+ h["foo"] = "bar"
282
+ h.close
283
+
284
+ reader = Hammerspace.new("/tmp/hammerspace")
285
+ reader["foo"] #=> "bar"
286
+
287
+ writer = Hammerspace.new("/tmp/hammerspace")
288
+ writer["foo"] = "updated"
289
+ writer.close
290
+
291
+ reader["foo"] #=> "bar"
292
+
293
+ # Close files now, re-open lazily on next read
294
+ reader.close
295
+
296
+ reader["foo"] #=> "updated"
297
+ reader.close
298
+ ```
299
+
300
+ If no hammerspace files exist on disk yet, the reader will fail to open the
301
+ files. It will try again on next read.
302
+
303
+ ```ruby
304
+ reader = Hammerspace.new("/tmp/hammerspace")
305
+ reader.has_key?("foo") #=> false
306
+
307
+ writer = Hammerspace.new("/tmp/hammerspace")
308
+ writer["foo"] = "bar"
309
+ writer.close
310
+
311
+ # Files are opened here
312
+ reader.has_key?("foo") #=> true
313
+ reader.close
314
+ ```
315
+
316
+ You can call `uid` to get a unique id that identifies the version of the files
317
+ being read. `uid` will be `nil` if no hammerspace files exist on disk yet.
318
+
319
+ ```ruby
320
+ reader = Hammerspace.new("/tmp/hammerspace")
321
+ reader.uid #=> nil
322
+
323
+ writer = Hammerspace.new("/tmp/hammerspace")
324
+ writer["foo"] = "bar"
325
+ writer.close
326
+
327
+ reader.close
328
+ reader.uid #=> "24913_53943df0-e784-4873-ade6-d1cccc848a70"
329
+
330
+ # The uid changes on every write, even if the content is the same, i.e., it's
331
+ # an identifier, not a checksum
332
+ writer["foo"] = "bar"
333
+ writer.close
334
+
335
+ reader.close
336
+ reader.uid #=> "24913_9371024e-8c80-477b-8558-7c292bfcbfc1"
337
+
338
+ reader.close
339
+ ```
340
+
341
+ Multiple concurrent writers are also supported. When a writer flushes its
342
+ changes it will overwrite any previous versions of the hammerspace.
343
+
344
+ In practice, this works because hammerspace is designed to hold data that is
345
+ bulk-loaded from some authoritative external source. Rather than block writers
346
+ to enforce consistency, it is simpler to allow writers to concurrently attempt
347
+ to load the data. The last writer to finish loading the data and flush its
348
+ writes will have its data persisted.
349
+
350
+ ```ruby
351
+ writer1 = Hammerspace.new("/tmp/hammerspace")
352
+ writer1["color"] = "red"
353
+
354
+ # Can start while writer1 is still open
355
+ writer2 = Hammerspace.new("/tmp/hammerspace")
356
+ writer2["color"] = "blue"
357
+ writer2["fruit"] = "banana"
358
+ writer2.close
359
+
360
+ # Reads at this point see writer2's data
361
+ reader1 = Hammerspace.new("/tmp/hammerspace")
362
+ reader1["color"] #=> "blue"
363
+ reader1["fruit"] #=> "banana"
364
+ reader1.close
365
+
366
+ # Replaces writer2's data
367
+ writer1.close
368
+
369
+ # Reads at this point see writer1's data; note that "fruit" key is absent
370
+ reader2 = Hammerspace.new("/tmp/hammerspace")
371
+ reader2["color"] #=> "red"
372
+ reader2["fruit"] #=> nil
373
+ reader2.close
374
+ ```
375
+
376
+
377
+ ### Flushing Writes
378
+
379
+ Flushing a write incurs some overhead to build the on-disk hash structures that
380
+ allows fast lookup later. To avoid the overhead of rebuilding the hash after
381
+ every write, most write operations do not implicitly flush. Writes can be
382
+ flushed explicitly by calling `close`.
383
+
384
+ Delaying flushing of writes has the side effect of allowing "transactions" --
385
+ all unflushed writes are private to the hammerspace object doing the writing.
386
+
387
+ One exception is the `clear` method which deletes the files on disk. If a
388
+ reader attempts to open the files immediately after they are deleted, it will
389
+ perceive the hammerspace to be empty.
390
+
391
+ ```ruby
392
+ h = Hammerspace.new("/tmp/hammerspace")
393
+ h["yesterday"] = "foo"
394
+ h["today"] = "bar"
395
+ h.close
396
+
397
+ reader1 = Hammerspace.new("/tmp/hammerspace")
398
+ reader1.keys #=> ["yesterday", "today"]
399
+ reader1.close
400
+
401
+ # Writer wants to remove everything except "today"
402
+ writer = Hammerspace.new("/tmp/hammerspace")
403
+ writer.clear
404
+
405
+ # Effect of clear is immediately visible to readers
406
+ reader2 = Hammerspace.new("/tmp/hammerspace")
407
+ reader2.keys #=> []
408
+ reader2.close
409
+
410
+ writer["today"] = "bar"
411
+ writer.close
412
+
413
+ reader3 = Hammerspace.new("/tmp/hammerspace")
414
+ reader3.keys #=> ["today"]
415
+ reader3.close
416
+ ```
417
+
418
+ If you want to replace the existing data with new data without flushing in
419
+ between (i.e., in a "transaction"), use `replace` instead.
420
+
421
+ ```ruby
422
+ h = Hammerspace.new("/tmp/hammerspace")
423
+ h["yesterday"] = "foo"
424
+ h["today"] = "bar"
425
+ h.close
426
+
427
+ reader1 = Hammerspace.new("/tmp/hammerspace")
428
+ reader1.keys #=> ["yesterday", "today"]
429
+ reader1.close
430
+
431
+ # Writer wants to remove everything except "today"
432
+ writer = Hammerspace.new("/tmp/hammerspace")
433
+ writer.replace({"today" => "bar"})
434
+
435
+ # Old keys still present because writer has not flushed yet
436
+ reader2 = Hammerspace.new("/tmp/hammerspace")
437
+ reader2.keys #=> ["yesterday", "today"]
438
+ reader2.close
439
+
440
+ writer.close
441
+
442
+ reader3 = Hammerspace.new("/tmp/hammerspace")
443
+ reader3.keys #=> ["today"]
444
+ reader3.close
445
+ ```
446
+
447
+
448
+ ### Interleaving Reads and Writes
449
+
450
+ To ensure writes are available to subsequent reads, every read operation
451
+ implicitly flushes any previous writes.
452
+
453
+ ```ruby
454
+ h = Hammerspace.new("/tmp/hammerspace")
455
+ h["foo"] = "bar"
456
+
457
+ # Implicitly flushes write (builds on-disk hash for fast lookup), then opens
458
+ # newly written on-disk hash for reading
459
+ h["foo"] #=> "bar"
460
+
461
+ h.close
462
+ ```
463
+
464
+ While batch reads or writes are relatively fast, interleaved reads and writes
465
+ are slow because the hash is rebuilt very often.
466
+
467
+ ```ruby
468
+ # One flush, fast
469
+ h = Hammerspace.new("/tmp/hammerspace")
470
+ h["a"] = "100"
471
+ h["b"] = "200"
472
+ h["c"] = "300"
473
+ h["a"] #=> "100"
474
+ h["b"] #=> "200"
475
+ h["c"] #=> "300"
476
+ h.close
477
+
478
+ # Three flushes, slow
479
+ h = Hammerspace.new("/tmp/hammerspace")
480
+ h["a"] = "100"
481
+ h["a"] #=> "100"
482
+ h["b"] = "200"
483
+ h["b"] #=> "200"
484
+ h["c"] = "300"
485
+ h["c"] #=> "300"
486
+ h.close
487
+ ```
488
+
489
+ To avoid this overhead, and to ensure consistency during iteration, the `each`
490
+ method opens its own private reader for the duration of the iteration. This is
491
+ also true for any method that uses `each`, including all methods provided by
492
+ `Enumerable`.
493
+
494
+ ```ruby
495
+ h = Hammerspace.new("/tmp/hammerspace")
496
+ h["a"] = "100"
497
+ h["b"] = "200"
498
+ h["c"] = "300"
499
+
500
+ # Flushes the above writes, then opens a private reader for the each call
501
+ h.each do |key, value|
502
+ # Writes are done in bulk without flushing in between
503
+ h[key] = value[0]
504
+ end
505
+
506
+ # Flushes the above writes, then opens the reader
507
+ h.to_hash #=> {"a"=>"1", "b"=>"2", "c"=>"3"}
508
+
509
+ h.close
510
+ ```
511
+
512
+
513
+ ### Unsupported Methods
514
+
515
+ Besides the incompatibilities with Ruby's `Hash` discussed above, there are
516
+ some `Hash` methods that are not supported.
517
+
518
+ * Methods that return a copy of the hash: `invert`, `merge`, `reject`, `select`
519
+ * `rehash` is not needed, since hammerspace only supports string keys, and keys are effectively `dup`d
520
+ * `delete` does not return the value deleted, and it does not support block usage
521
+ * `hash` and `to_s` are not overriden, so the behavior is that of `Object#hash` and `Object#to_s`
522
+ * `compare_by_identity`, `compare_by_identity?`
523
+ * `pretty_print`, `pretty_print_cycle`