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.
Files changed (3) hide show
  1. data/lib/stored_hash.rb +126 -0
  2. data/spec/stored_hash_spec.rb +105 -0
  3. metadata +54 -0
@@ -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
+