rkh-stored_hash 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+