rkh-stored_hash 0.1.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.
- data/lib/stored_hash.rb +126 -0
- data/spec/stored_hash_spec.rb +105 -0
- metadata +54 -0
data/lib/stored_hash.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
# This is a drop in replacement for Hash.
|
5
|
+
# It does act just the same but reads/writes from a yaml
|
6
|
+
# file whenever the hash or the file changes.
|
7
|
+
# This is much slower than a hash, as you might guess.
|
8
|
+
# A StoredHash is thread-safe.
|
9
|
+
class StoredHash
|
10
|
+
|
11
|
+
attr_reader :file
|
12
|
+
|
13
|
+
class << self
|
14
|
+
alias load new
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize file, *args, &block
|
18
|
+
@data = Hash.new(*args, &block)
|
19
|
+
@file = file
|
20
|
+
@mutex = Mutex.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def == other
|
24
|
+
super or @data == other
|
25
|
+
end
|
26
|
+
|
27
|
+
[:to_yaml, :inspect, :eql?, :hash].each do |name|
|
28
|
+
define_method(name) do |*args|
|
29
|
+
method_missing(name, *args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
[:dup, :clone].each do |name|
|
34
|
+
define_method(name) do
|
35
|
+
@mutex.synchronize do
|
36
|
+
super.instance_eval do
|
37
|
+
@mutex = Mutex.new
|
38
|
+
@data = @data.send name
|
39
|
+
self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Here is where the magic happens. Before any method is called on the hash
|
46
|
+
# we'll check the file for changes (of course only reading the file if it
|
47
|
+
# has been changed). After the method terminated we check whether the hash
|
48
|
+
# has been changed and if so, we write it to the file.
|
49
|
+
def method_missing(*args, &blk)
|
50
|
+
synchronize do |file|
|
51
|
+
update file if should_update? file
|
52
|
+
@old_data ||= duplicate_data
|
53
|
+
result = @data.send(*args, &blk)
|
54
|
+
write if should_write?
|
55
|
+
return self if result == @data
|
56
|
+
result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_hash
|
61
|
+
@data.dup
|
62
|
+
end
|
63
|
+
|
64
|
+
def is_a? whatever
|
65
|
+
super whatever or @data.is_a? whatever
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def should_update? file
|
71
|
+
not @mtime or file.mtime >= @mtime
|
72
|
+
end
|
73
|
+
|
74
|
+
def update file
|
75
|
+
@data.replace YAML.load(file.read)
|
76
|
+
@mtime = file.mtime
|
77
|
+
@old_data = duplicate_data
|
78
|
+
end
|
79
|
+
|
80
|
+
def duplicate_data
|
81
|
+
@data.respond_to?(:deep_clone) ? @data.deep_clone : @data.dup
|
82
|
+
end
|
83
|
+
|
84
|
+
def should_write?
|
85
|
+
@data != @old_data
|
86
|
+
end
|
87
|
+
|
88
|
+
def write
|
89
|
+
yaml = @data.to_yaml
|
90
|
+
begin
|
91
|
+
YAML.load yaml
|
92
|
+
rescue ArgumentError
|
93
|
+
@data.replace @old_data
|
94
|
+
raise ArgumentError, "Unable to store object as yaml."
|
95
|
+
end
|
96
|
+
File.open(@file, "w") { |f| f.write(yaml) }
|
97
|
+
@mtime = File.mtime(@file)
|
98
|
+
@old_data = duplicate_data
|
99
|
+
end
|
100
|
+
|
101
|
+
# Not only do we want to synchronize threads, but processes, too.
|
102
|
+
def synchronize
|
103
|
+
@mutex.synchronize do
|
104
|
+
unless File.exists?(@file)
|
105
|
+
File.open(@file, "w") { |f| f.write(Hash.new.to_yaml) }
|
106
|
+
end
|
107
|
+
File.open(@file, "r") do |file|
|
108
|
+
begin
|
109
|
+
file.flock(File::LOCK_EX)
|
110
|
+
result = yield(file)
|
111
|
+
ensure
|
112
|
+
file.flock(File::LOCK_UN)
|
113
|
+
end
|
114
|
+
result
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
class Hash
|
122
|
+
def store file
|
123
|
+
StoredHash.new(file).replace self
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
$: << File.join(File.dirname(__FILE__), "..", "lib")
|
2
|
+
|
3
|
+
require 'stored_hash'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
describe StoredHash do
|
7
|
+
|
8
|
+
before :each do
|
9
|
+
@stored_hash = StoredHash.new "/tmp/test#{Time.now.to_i}.yml"
|
10
|
+
@some_hashes = [ {}, {0 => 1, 2 => 3, 4 => 5}, {"foo" => [{:bar => "blah"}]} ]
|
11
|
+
def with_some_hash
|
12
|
+
@some_hashes.each do |a_hash|
|
13
|
+
@stored_hash.replace a_hash
|
14
|
+
yield a_hash
|
15
|
+
@stored_hash.replace Hash.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
after :each do
|
21
|
+
FileUtils.rm @stored_hash.file
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should write and read correctly" do
|
25
|
+
10.times do |i|
|
26
|
+
["i", "i.to_s", "i.to_s.to_sym", "'abc ' * i"].each do |code|
|
27
|
+
value = eval(code)
|
28
|
+
[i, code, value].each do |key|
|
29
|
+
@stored_hash[key] = value
|
30
|
+
@stored_hash[key].should == value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should behave like a Hash#inspect" do
|
37
|
+
with_some_hash { |a_hash| @stored_hash.inspect.should == a_hash.inspect }
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should behave like a Hash#to_yaml" do
|
41
|
+
with_some_hash { |a_hash| @stored_hash.to_yaml.should == a_hash.to_yaml }
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should behave like a Hash#inspect" do
|
45
|
+
with_some_hash { |a_hash| @stored_hash.inspect.should == a_hash.inspect }
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should behave like a Hash#hash" do
|
49
|
+
with_some_hash { |a_hash| @stored_hash.hash.should == a_hash.hash }
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should behave like a Hash#eql?" do
|
53
|
+
with_some_hash do |a_hash|
|
54
|
+
@some_hashes.each do |another_hash|
|
55
|
+
@stored_hash.eql?(another_hash).should == a_hash.eql?(another_hash)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should behave like a Hash#== for other hashes" do
|
61
|
+
with_some_hash do |a_hash|
|
62
|
+
@some_hashes.each do |another_hash|
|
63
|
+
@stored_hash.==(another_hash).should == a_hash.==(another_hash)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should return true for #is_a?(Hash)" do
|
69
|
+
with_some_hash { @stored_hash.is_a?(Hash).should == true }
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should equal an empty hash after #clear" do
|
73
|
+
with_some_hash do
|
74
|
+
@stored_hash.clear
|
75
|
+
@stored_hash.should == {}
|
76
|
+
@stored_hash.size.should == 0
|
77
|
+
@stored_hash.empty?.should == true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should properly delete items" do
|
82
|
+
with_some_hash do
|
83
|
+
a_key = @stored_hash.keys.first
|
84
|
+
@stored_hash.delete(a_key).should @stored_hash[a_key]
|
85
|
+
@stored_hash.include?(a_key).should == false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should be thread safe" do
|
90
|
+
thread_number = 5
|
91
|
+
max_count = 10
|
92
|
+
threads = []
|
93
|
+
thread_number.times do |tnum|
|
94
|
+
threads << Thread.new(tnum) do |t|
|
95
|
+
max_count.times do |i|
|
96
|
+
@stored_hash["#{t} #{i}"] = 0
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
threads.each { |t| t.join }
|
101
|
+
@stored_hash.size.should == (thread_number * max_count)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rkh-stored_hash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Konstantin Haase
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-11 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: konstantin.mailinglists@googlemail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- lib/stored_hash.rb
|
26
|
+
- spec/stored_hash_spec.rb
|
27
|
+
has_rdoc: true
|
28
|
+
homepage: http://github.com/rkh/stored_hash
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: "0"
|
39
|
+
version:
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
requirements: []
|
47
|
+
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 1.2.0
|
50
|
+
signing_key:
|
51
|
+
specification_version: 2
|
52
|
+
summary: synchronize a hash with a yaml-file
|
53
|
+
test_files: []
|
54
|
+
|