fsdb 0.6.1 → 0.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.
@@ -117,7 +117,7 @@ end
117
117
  # this is like in test-concurrency.rb -- generalize?
118
118
  def cleanup dir
119
119
  @db.browse_dir dir do |child_path|
120
- if child_path[-1] == ?/ ## ugh!
120
+ if child_path =~ /\/$/ ## ugh!
121
121
  cleanup(child_path)
122
122
  else
123
123
  @db.delete(child_path)
@@ -0,0 +1,43 @@
1
+ require 'fsdb'
2
+
3
+ db = FSDB::Database.new '/tmp/fsdb-example'
4
+
5
+ db['foo.txt'] = "hello, world"
6
+ puts db['foo.txt']
7
+ puts
8
+
9
+ p1 = fork do
10
+ puts "*** starting fork #1 at #{Time.now}"
11
+ db.edit 'foo.txt' do |str|
12
+ str << "\n edited by fork #1 at #{Time.now}"
13
+ sleep 2 # give fork #2 a chance to try editing
14
+ str << "\n done editing in fork #1 at #{Time.now}"
15
+ end
16
+ end
17
+
18
+ sleep 1
19
+
20
+ p2 = fork do
21
+ puts "*** starting fork #2 at #{Time.now}"
22
+ db.edit 'foo.txt' do |str|
23
+ str << "\n edited by fork #2 at #{Time.now}"
24
+ str << "\n done editing in fork #2 at #{Time.now}"
25
+ end
26
+ end
27
+
28
+ Process.waitpid p1
29
+ Process.waitpid p2
30
+
31
+ puts db['foo.txt']
32
+
33
+ __END__
34
+
35
+ hello, world
36
+
37
+ *** starting fork #1 at Tue Nov 29 11:03:03 -0800 2011
38
+ *** starting fork #2 at Tue Nov 29 11:03:04 -0800 2011
39
+ hello, world
40
+ edited by fork #1 at Tue Nov 29 11:03:03 -0800 2011
41
+ done editing in fork #1 at Tue Nov 29 11:03:05 -0800 2011
42
+ edited by fork #2 at Tue Nov 29 11:03:05 -0800 2011
43
+ done editing in fork #2 at Tue Nov 29 11:03:05 -0800 2011
@@ -1,7 +1,5 @@
1
1
  require 'fileutils'
2
2
  require 'fsdb/platform'
3
- require 'fsdb/compat' if RUBY_VERSION.to_f < 1.7
4
- require 'fsdb/mutex'
5
3
  require 'fsdb/modex'
6
4
  require 'fsdb/file-lock'
7
5
  require 'fsdb/formats'
@@ -9,7 +7,7 @@ require 'fsdb/formats'
9
7
  module FSDB
10
8
  include Formats
11
9
 
12
- FSDB::VERSION = "0.6.1"
10
+ FSDB::VERSION = "0.7.0"
13
11
 
14
12
  # A thread-safe, process-safe object database class which uses the
15
13
  # native file system as its back end and allows multiple file formats.
@@ -108,14 +106,10 @@ class Database
108
106
 
109
107
  # Subclasses can change the defaults.
110
108
  DEFAULT_META_PREFIX = '..fsdb.meta.'
111
- ### if RUBY_PLATFORM =~ /darwin/
112
- ### DEFAULT_LOCK_TYPE = :fcntl_lock
113
- ### else
114
- DEFAULT_LOCK_TYPE = :flock
115
- ### end
109
+ DEFAULT_LOCK_TYPE = :flock
116
110
 
117
111
  # These must be methods of File.
118
- LOCK_TYPES = [:flock, :fcntl_lock]
112
+ LOCK_TYPES = [:flock] # obsolete: :fcntl_lock
119
113
 
120
114
  @cache = {} # maps <file id> to <CacheEntry>
121
115
  @cache_mutex = Mutex.new # protects access to @cache hash
@@ -130,15 +124,14 @@ class Database
130
124
  # The root directory of the db, to which paths are relative.
131
125
  attr_reader :dir
132
126
 
133
- # The lock type of the db, by default <tt>:flock</tt>, optionally
134
- # <tt>:fcntl_lock</tt>.
127
+ # The lock type of the db, by default <tt>:flock</tt>.
135
128
  attr_reader :lock_type
136
129
 
137
130
  # Create a new database object that accesses +dir+. Makes sure that the
138
131
  # directory exists on disk, but doesn't create or open any other files.
139
132
  # The +opts+ hash can include:
140
133
  #
141
- # <tt>:lock_type</tt>:: <tt>:flock</tt> by default, or <tt>:fcntl_lock</tt>
134
+ # <tt>:lock_type</tt>:: <tt>:flock</tt> by default
142
135
  #
143
136
  # <tt>:meta_prefix</tt>:: <tt>'..fsdb.meta.'</tt> by default
144
137
  #
@@ -152,10 +145,6 @@ class Database
152
145
  raise "Unknown lock type: #{lock_type}"
153
146
  end
154
147
 
155
- if @lock_type == :fcntl_lock
156
- require 'fcntl_lock' ## hack.
157
- end
158
-
159
148
  @meta_prefix = opts[:meta_prefix] || DEFAULT_META_PREFIX
160
149
 
161
150
  @formats = opts[:formats]
@@ -7,10 +7,6 @@ class File
7
7
  CAN_DELETE_OPEN_FILE = !FSDB::PLATFORM_IS_WINDOWS
8
8
  CAN_OPEN_DIR = !FSDB::PLATFORM_IS_WINDOWS
9
9
 
10
- LOCK_BLOCK_FIXED_VER = "1.8.2" # Hurray!
11
- LOCK_DOESNT_BLOCK = [RUBY_VERSION, LOCK_BLOCK_FIXED_VER].
12
- map {|s| s.split('.')}.sort[0].join('.') == LOCK_BLOCK_FIXED_VER
13
-
14
10
  if FSDB::PLATFORM_IS_WINDOWS_ME
15
11
  # no flock() on WinME
16
12
 
@@ -21,61 +17,22 @@ class File
21
17
  end
22
18
 
23
19
  else
24
- if LOCK_DOESNT_BLOCK
25
- # Get an exclusive (i.e., write) lock on the file.
26
- # If the lock is not available, wait for it without blocking other ruby
27
- # threads.
28
- def lock_exclusive lock_type
29
- send(lock_type, LOCK_EX)
30
- rescue Errno::EINTR
31
- retry
32
- end
33
-
34
- # Get a shared (i.e., read) lock on the file.
35
- # If the lock is not available, wait for it without blocking other ruby
36
- # threads.
37
- def lock_shared lock_type
38
- send(lock_type, LOCK_SH)
39
- rescue Errno::EINTR
40
- retry
41
- end
42
-
43
- else
44
- def lock_exclusive lock_type
45
- if Thread.list.size == 1
46
- begin
47
- send(lock_type, LOCK_EX)
48
- rescue Errno::EINTR
49
- retry
50
- end
51
- else
52
- # ugly hack because waiting for a lock in a Ruby thread blocks the
53
- # entire process
54
- period = 0.001
55
- until send(lock_type, LOCK_EX|LOCK_NB)
56
- sleep period
57
- period *= 2 if period < 1
58
- end
59
- end
60
- end
20
+ # Get an exclusive (i.e., write) lock on the file.
21
+ # If the lock is not available, wait for it without blocking other ruby
22
+ # threads.
23
+ def lock_exclusive lock_type
24
+ send(lock_type, LOCK_EX)
25
+ rescue Errno::EINTR
26
+ retry
27
+ end
61
28
 
62
- def lock_shared lock_type
63
- if Thread.list.size == 1
64
- begin
65
- send(lock_type, LOCK_SH)
66
- rescue Errno::EINTR
67
- retry
68
- end
69
- else
70
- # ugly hack because waiting for a lock in a Ruby thread blocks the
71
- # entire process
72
- period = 0.001
73
- until send(lock_type, LOCK_SH|LOCK_NB)
74
- sleep period
75
- period *= 2 if period < 1
76
- end
77
- end
78
- end
29
+ # Get a shared (i.e., read) lock on the file.
30
+ # If the lock is not available, wait for it without blocking other ruby
31
+ # threads.
32
+ def lock_shared lock_type
33
+ send(lock_type, LOCK_SH)
34
+ rescue Errno::EINTR
35
+ retry
79
36
  end
80
37
  end
81
38
 
@@ -1,30 +1,6 @@
1
- #!/usr/bin/env ruby
2
-
3
- unless defined? Thread.exclusive
4
- class Thread # :nodoc:
5
- def self.exclusive
6
- old = critical
7
- self.critical = true
8
- yield
9
- ensure
10
- self.critical = old
11
- end
12
- end
13
- end
14
-
15
- class Thread
16
- def self.nonexclusive
17
- old = critical
18
- self.critical = false
19
- yield
20
- ensure
21
- self.critical = old
22
- end
23
- end
24
-
25
1
  module FSDB
26
2
 
27
- # Modex is a modal exclusion semaphore, like in syncronizer.rb.
3
+ # Modex is a modal exclusion semaphore.
28
4
  # The two modes are shared (SH) and exclusive (EX).
29
5
  # Modex is not nestable.
30
6
  #
@@ -37,12 +13,13 @@ class Modex
37
13
  @locked = []
38
14
  @mode = nil
39
15
  @first = true
16
+ @m = Mutex.new
40
17
  end
41
18
 
42
19
  def try_lock mode
43
- Thread.exclusive do
20
+ @m.synchronize do
44
21
  thread = Thread.current
45
- raise ThreadError if @locked.include?(thread)
22
+ raise ThreadError, "nesting not allowed" if @locked.include?(thread)
46
23
 
47
24
  if @mode == mode and mode == SH and @waiting.empty? # strict queue
48
25
  @locked << thread
@@ -51,15 +28,17 @@ class Modex
51
28
  @mode = mode
52
29
  @locked << thread
53
30
  true
31
+ else
32
+ false
54
33
  end
55
34
  end
56
35
  end
57
36
 
58
37
  # the block is executed in the exclusive context
59
38
  def lock mode
60
- Thread.exclusive do
39
+ @m.synchronize do
61
40
  thread = Thread.current
62
- raise ThreadError if @locked.include?(thread)
41
+ raise ThreadError, "nesting not allowed" if @locked.include?(thread)
63
42
 
64
43
  if @mode == mode and mode == SH and @waiting.empty? # strict queue
65
44
  @locked << thread
@@ -68,8 +47,9 @@ class Modex
68
47
  @locked << thread
69
48
  else
70
49
  @waiting << thread << mode
50
+ @m.unlock
71
51
  Thread.stop
72
- Thread.critical = true
52
+ @m.lock
73
53
  end
74
54
 
75
55
  yield if block_given?
@@ -88,11 +68,20 @@ class Modex
88
68
 
89
69
  # the block is executed in the exclusive context
90
70
  def unlock
91
- raise ThreadError unless @mode
71
+ raise ThreadError, "already unlocked" unless @mode
92
72
 
93
- Thread.exclusive do
94
- yield if block_given?
95
- @locked.delete Thread.current
73
+ @m.synchronize do
74
+ if block_given?
75
+ begin
76
+ yield
77
+ ensure
78
+ @locked.delete Thread.current
79
+ end
80
+
81
+ else
82
+ @locked.delete Thread.current
83
+ end
84
+
96
85
  wake_next_waiter if @locked.empty?
97
86
  end
98
87
 
@@ -109,7 +98,7 @@ class Modex
109
98
  @mode = EX
110
99
  end
111
100
 
112
- Thread.nonexclusive { do_when_first[arg] }
101
+ nonexclusive { do_when_first[arg] }
113
102
 
114
103
  if mode == SH
115
104
  @mode = SH
@@ -126,7 +115,7 @@ class Modex
126
115
  if @locked.size == 1
127
116
  if do_when_last
128
117
  @mode = EX
129
- Thread.nonexclusive { do_when_last[arg] }
118
+ nonexclusive { do_when_last[arg] }
130
119
  end
131
120
  @first = true
132
121
  end
@@ -134,7 +123,7 @@ class Modex
134
123
  end
135
124
 
136
125
  def remove_dead # :nodoc:
137
- Thread.exclusive do
126
+ @m.synchronize do
138
127
  waiting = @waiting; @waiting = []
139
128
  until waiting.empty?
140
129
 
@@ -148,13 +137,21 @@ class Modex
148
137
  end
149
138
 
150
139
  private
140
+ def nonexclusive
141
+ raise ThreadError unless @m.locked?
142
+ @m.unlock
143
+ yield
144
+ ensure
145
+ @m.lock
146
+ end
147
+
151
148
  def wake_next_waiter
152
- first = @waiting.shift; @mode = @waiting.shift && EX
153
- if first
154
- first.wakeup
155
- @locked << first
149
+ first_waiter = @waiting.shift; @mode = @waiting.shift && EX
150
+ if first_waiter
151
+ first_waiter.wakeup
152
+ @locked << first_waiter
156
153
  end
157
- first
154
+ first_waiter
158
155
  rescue ThreadError
159
156
  retry
160
157
  end
@@ -295,7 +295,7 @@ class ConcurrencyTest
295
295
 
296
296
  @total_iters = process_count * thread_count * rep_count
297
297
 
298
- unless quiet or not $defout.isatty
298
+ unless quiet or not $stdout.isatty
299
299
  monitor_thread = Thread.new do
300
300
  loop do
301
301
  print "\r#{status_meter}"
@@ -356,7 +356,7 @@ class ConcurrencyTest
356
356
 
357
357
  def cleanup dir
358
358
  @db.browse_dir dir do |child_path|
359
- if child_path[-1] == ?/ ## ugh!
359
+ if child_path =~ /\/$/ ## ugh!
360
360
  cleanup(child_path)
361
361
  else
362
362
  @db.delete(child_path)
@@ -68,10 +68,6 @@ class ConcurrencyTest
68
68
  require 'profile'
69
69
  end
70
70
 
71
- opts.on("--fcntl-lock", "use fcntl version of flock") do
72
- # actually, this arg is already picked out by test.rb
73
- end
74
-
75
71
  opts.on("--nofork", "do not use fork, even if available") do
76
72
  params["nofork"] = true
77
73
  end
@@ -32,7 +32,7 @@ class Test_Formats < Test::Unit::TestCase
32
32
  # this is like in test-concurrency.rb -- generalize?
33
33
  def cleanup dir
34
34
  @db.browse_dir dir do |child_path|
35
- if child_path[-1] == ?/ ## ugh!
35
+ if child_path =~ /\/$/ ## ugh!
36
36
  cleanup(child_path)
37
37
  else
38
38
  @db.delete(child_path)
@@ -23,13 +23,15 @@ class Test_FSDB < Test::Unit::TestCase
23
23
  end
24
24
 
25
25
  def test_zzz_cleanup # Argh! Test::Unit is missing a few features....
26
+ @db['foo.txt'] = "abc" # something to clean up
27
+ ## really, cleanup should work when the dir is not there
26
28
  cleanup '/'
27
29
  end
28
30
 
29
31
  # this is like in test-concurrency.rb -- generalize?
30
32
  def cleanup dir
31
33
  @db.browse_dir dir do |child_path|
32
- if child_path[-1] == ?/ ## ugh!
34
+ if child_path =~ /\/$/ ## ugh!
33
35
  cleanup(child_path)
34
36
  else
35
37
  @db.delete(child_path)
@@ -4,28 +4,70 @@ require 'fsdb/modex'
4
4
 
5
5
  include FSDB
6
6
 
7
- modex = Modex.new
8
- counter = 0
7
+ DEBUG_MODEX = ARGV.delete '-d'
8
+ VERBOSE_MODEX = ARGV.delete '-v'
9
+
9
10
  thread_count = (ARGV.shift || 10).to_i
10
11
  rep_count = (ARGV.shift || 1000).to_i
11
12
 
13
+ def tabbed a
14
+ a.map {|s| s.sub(/^(\d+)/) {|n| " " * n.to_i * 20 + n}}
15
+ end
16
+
17
+ def thread_pass
18
+ if rand < 0.1
19
+ sleep rand/1000
20
+ else
21
+ Thread.pass
22
+ end
23
+ end
24
+
25
+ modex = Modex.new
26
+ counter = 0
27
+
12
28
  out = []
13
29
  sharers = 0
14
30
  excluders = 0
15
31
  dumper = proc do |str|
16
32
  puts "*** #{str} called when there were #{sharers} threads sharing modex ***"
17
- puts out[-20..-1].join("\n")
33
+ puts tabbed(out[-20..-1])
18
34
  exit!
19
35
  end
20
36
 
37
+ class TestThread < Thread
38
+ def initialize n, outdev
39
+ self[:id] = n
40
+ self[:writes] = 0
41
+ @outdev = outdev
42
+
43
+ super do
44
+ begin
45
+ yield
46
+ rescue => ex
47
+ puts "#{ex}\n #{ex.backtrace.join("\n ")}"
48
+ # let thread stop, but let process continue
49
+ end
50
+ end
51
+ end
52
+
53
+ def inspect
54
+ "#<test thread #{self[:id]}>"
55
+ end
56
+
57
+ if DEBUG_MODEX
58
+ def out s
59
+ @outdev << "#{self[:id]}: #{s}"
60
+ end
61
+ else
62
+ def out(*); end
63
+ end
64
+ end
65
+
21
66
  threads = (0...thread_count).map do |n|
22
- Thread.new do
67
+ TestThread.new(n, out) do
23
68
  thread = Thread.current
24
- thread[:id] = n
25
- thread[:writes] = 0
26
-
69
+
27
70
  do_when_first = proc do
28
- out << "#{thread[:id]}: do_when_first"
29
71
  sharers += 1
30
72
  if sharers > 1
31
73
  dumper["do_when_first"]
@@ -33,7 +75,6 @@ threads = (0...thread_count).map do |n|
33
75
  end
34
76
 
35
77
  do_when_last = proc do
36
- out << "#{thread[:id]}: do_when_last"
37
78
  if sharers > 1
38
79
  dumper["do_when_last"]
39
80
  end
@@ -44,37 +85,40 @@ threads = (0...thread_count).map do |n|
44
85
  x = rand(100)
45
86
  case
46
87
  when x < 50
47
- out << "#{thread[:id]}: trying SH"
88
+ thread.out "trying SH"
48
89
  modex.synchronize(Modex::SH, do_when_first, do_when_last) do
49
- out << "#{thread[:id]}: locked SH"
90
+ thread.out "begin SH"
50
91
  c_old = counter
51
- Thread.pass
92
+ thread_pass
52
93
  raise if excluders > 0
53
94
  raise unless counter == c_old
54
- out << "#{thread[:id]}: unlocked SH"
95
+ thread.out "end SH"
55
96
  end
97
+ thread_pass
98
+
56
99
  else
57
- out << "#{thread[:id]}: trying EX"
100
+ thread.out "trying EX"
58
101
  modex.synchronize(Modex::EX) do
59
- out << "#{thread[:id]}: locked EX"
102
+ thread.out "begin EX"
60
103
  excluders += 1
61
104
  counter += 1
62
- Thread.pass
105
+ thread_pass
63
106
  raise unless excluders == 1
107
+ raise unless sharers == 0
64
108
  excluders -= 1
65
- out << "#{thread[:id]}: unlocked EX"
109
+ thread.out "end EX"
66
110
  end
111
+ thread_pass
67
112
  thread[:writes] += 1
68
113
  end
69
114
  end
70
115
  end
71
116
  end
72
117
 
73
- expected = 0
74
- threads.each {|t| t.join; expected += t[:writes]}
118
+ expected = threads.inject(0) {|s, t| t.join; s + t[:writes]}
119
+ puts tabbed(out) if VERBOSE_MODEX
75
120
 
76
121
  actual = counter
77
-
78
122
  if expected == actual
79
123
  puts "Test passed: #{actual} write operations"
80
124
  else