* io.c (rb_open_file): encoding in mode string was ignored if perm is
[ruby-svn.git] / lib / pstore.rb
blobfdc518eaec19a99bc0ab8ab7f9c192d02a104147
1 # = PStore -- Transactional File Storage for Ruby Objects
3 # pstore.rb -
4 #   originally by matz
5 #   documentation by Kev Jackson and James Edward Gray II
6 #   improved by Hongli Lai
8 # See PStore for documentation.
11 require "fileutils"
12 require "digest/md5"
13 require "thread"
16 # PStore implements a file based persistence mechanism based on a Hash.  User
17 # code can store hierarchies of Ruby objects (values) into the data store file
18 # by name (keys).  An object hierarchy may be just a single object.  User code 
19 # may later read values back from the data store or even update data, as needed.
20
21 # The transactional behavior ensures that any changes succeed or fail together.
22 # This can be used to ensure that the data store is not left in a transitory
23 # state, where some values were updated but others were not.
24
25 # Behind the scenes, Ruby objects are stored to the data store file with 
26 # Marshal.  That carries the usual limitations.  Proc objects cannot be 
27 # marshalled, for example.
29 # == Usage example:
30
31 #  require "pstore"
32 #  
33 #  # a mock wiki object...
34 #  class WikiPage
35 #    def initialize( page_name, author, contents )
36 #      @page_name = page_name
37 #      @revisions = Array.new
38 #      
39 #      add_revision(author, contents)
40 #    end
41 #    
42 #    attr_reader :page_name
43 #    
44 #    def add_revision( author, contents )
45 #      @revisions << { :created  => Time.now,
46 #                      :author   => author,
47 #                      :contents => contents }
48 #    end
49 #    
50 #    def wiki_page_references
51 #      [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/)
52 #    end
53 #    
54 #    # ...
55 #  end
56 #  
57 #  # create a new page...
58 #  home_page = WikiPage.new( "HomePage", "James Edward Gray II",
59 #                            "A page about the JoysOfDocumentation..." )
60 #  
61 #  # then we want to update page data and the index together, or not at all...
62 #  wiki = PStore.new("wiki_pages.pstore")
63 #  wiki.transaction do  # begin transaction; do all of this or none of it
64 #    # store page...
65 #    wiki[home_page.page_name] = home_page
66 #    # ensure that an index has been created...
67 #    wiki[:wiki_index] ||= Array.new
68 #    # update wiki index...
69 #    wiki[:wiki_index].push(*home_page.wiki_page_references)
70 #  end                   # commit changes to wiki data store file
71 #  
72 #  ### Some time later... ###
73 #  
74 #  # read wiki data...
75 #  wiki.transaction(true) do  # begin read-only transaction, no changes allowed
76 #    wiki.roots.each do |data_root_name|
77 #      p data_root_name
78 #      p wiki[data_root_name]
79 #    end
80 #  end
82 # == Transaction modes
84 # By default, file integrity is only ensured as long as the operating system
85 # (and the underlying hardware) doesn't raise any unexpected I/O errors. If an
86 # I/O error occurs while PStore is writing to its file, then the file will
87 # become corrupted.
89 # You can prevent this by setting <em>pstore.ultra_safe = true</em>.
90 # However, this results in a minor performance loss, and only works on platforms
91 # that support atomic file renames. Please consult the documentation for
92 # +ultra_safe+ for details.
94 # Needless to say, if you're storing valuable data with PStore, then you should
95 # backup the PStore files from time to time.
96 class PStore
97   binmode = defined?(File::BINARY) ? File::BINARY : 0
98   RDWR_ACCESS = File::RDWR | File::CREAT | binmode
99   RD_ACCESS = File::RDONLY | binmode
100   WR_ACCESS = File::WRONLY | File::CREAT | File::TRUNC | binmode
102   # The error type thrown by all PStore methods.
103   class Error < StandardError
104   end
105   
106   # Whether PStore should do its best to prevent file corruptions, even when under
107   # unlikely-to-occur error conditions such as out-of-space conditions and other
108   # unusual OS filesystem errors. Setting this flag comes at the price in the form
109   # of a performance loss.
110   #
111   # This flag only has effect on platforms on which file renames are atomic (e.g.
112   # all POSIX platforms: Linux, MacOS X, FreeBSD, etc). The default value is false.
113   attr_accessor :ultra_safe
115   # 
116   # To construct a PStore object, pass in the _file_ path where you would like 
117   # the data to be stored.
118   #
119   # PStore objects are always reentrant. But if _thread_safe_ is set to true,
120   # then it will become thread-safe at the cost of a minor performance hit.
121   # 
122   def initialize(file, thread_safe = false)
123     dir = File::dirname(file)
124     unless File::directory? dir
125       raise PStore::Error, format("directory %s does not exist", dir)
126     end
127     if File::exist? file and not File::readable? file
128       raise PStore::Error, format("file %s not readable", file)
129     end
130     @transaction = false
131     @filename = file
132     @abort = false
133     @ultra_safe = false
134     if @thread_safe
135       @lock = Mutex.new
136     else
137       @lock = DummyMutex.new
138     end
139   end
141   # Raises PStore::Error if the calling code is not in a PStore#transaction.
142   def in_transaction
143     raise PStore::Error, "not in transaction" unless @transaction
144   end
145   # 
146   # Raises PStore::Error if the calling code is not in a PStore#transaction or
147   # if the code is in a read-only PStore#transaction.
148   # 
149   def in_transaction_wr()
150     in_transaction()
151     raise PStore::Error, "in read-only transaction" if @rdonly
152   end
153   private :in_transaction, :in_transaction_wr
155   #
156   # Retrieves a value from the PStore file data, by _name_.  The hierarchy of 
157   # Ruby objects stored under that root _name_ will be returned.
158   # 
159   # *WARNING*:  This method is only valid in a PStore#transaction.  It will
160   # raise PStore::Error if called at any other time.
161   #
162   def [](name)
163     in_transaction
164     @table[name]
165   end
166   #
167   # This method is just like PStore#[], save that you may also provide a 
168   # _default_ value for the object.  In the event the specified _name_ is not 
169   # found in the data store, your _default_ will be returned instead.  If you do 
170   # not specify a default, PStore::Error will be raised if the object is not 
171   # found.
172   # 
173   # *WARNING*:  This method is only valid in a PStore#transaction.  It will
174   # raise PStore::Error if called at any other time.
175   #
176   def fetch(name, default=PStore::Error)
177     in_transaction
178     unless @table.key? name
179       if default == PStore::Error
180         raise PStore::Error, format("undefined root name `%s'", name)
181       else
182         return default
183       end
184     end
185     @table[name]
186   end
187   #
188   # Stores an individual Ruby object or a hierarchy of Ruby objects in the data
189   # store file under the root _name_.  Assigning to a _name_ already in the data
190   # store clobbers the old data.
191   # 
192   # == Example:
193   # 
194   #  require "pstore"
195   #  
196   #  store = PStore.new("data_file.pstore")
197   #  store.transaction do  # begin transaction
198   #    # load some data into the store...
199   #    store[:single_object] = "My data..."
200   #    store[:obj_heirarchy] = { "Kev Jackson" => ["rational.rb", "pstore.rb"],
201   #                              "James Gray"  => ["erb.rb", "pstore.rb"] }
202   #  end                   # commit changes to data store file
203   # 
204   # *WARNING*:  This method is only valid in a PStore#transaction and it cannot
205   # be read-only.  It will raise PStore::Error if called at any other time.
206   #
207   def []=(name, value)
208     in_transaction_wr()
209     @table[name] = value
210   end
211   #
212   # Removes an object hierarchy from the data store, by _name_.
213   # 
214   # *WARNING*:  This method is only valid in a PStore#transaction and it cannot
215   # be read-only.  It will raise PStore::Error if called at any other time.
216   #
217   def delete(name)
218     in_transaction_wr()
219     @table.delete name
220   end
222   #
223   # Returns the names of all object hierarchies currently in the store.
224   # 
225   # *WARNING*:  This method is only valid in a PStore#transaction.  It will
226   # raise PStore::Error if called at any other time.
227   #
228   def roots
229     in_transaction
230     @table.keys
231   end
232   #
233   # Returns true if the supplied _name_ is currently in the data store.
234   # 
235   # *WARNING*:  This method is only valid in a PStore#transaction.  It will
236   # raise PStore::Error if called at any other time.
237   #
238   def root?(name)
239     in_transaction
240     @table.key? name
241   end
242   # Returns the path to the data store file.
243   def path
244     @filename
245   end
247   #
248   # Ends the current PStore#transaction, committing any changes to the data
249   # store immediately.
250   # 
251   # == Example:
252   # 
253   #  require "pstore"
254   #   
255   #  store = PStore.new("data_file.pstore")
256   #  store.transaction do  # begin transaction
257   #    # load some data into the store...
258   #    store[:one] = 1
259   #    store[:two] = 2
260   #  
261   #    store.commit        # end transaction here, committing changes
262   #  
263   #    store[:three] = 3   # this change is never reached
264   #  end
265   # 
266   # *WARNING*:  This method is only valid in a PStore#transaction.  It will
267   # raise PStore::Error if called at any other time.
268   #
269   def commit
270     in_transaction
271     @abort = false
272     throw :pstore_abort_transaction
273   end
274   #
275   # Ends the current PStore#transaction, discarding any changes to the data
276   # store.
277   # 
278   # == Example:
279   # 
280   #  require "pstore"
281   #   
282   #  store = PStore.new("data_file.pstore")
283   #  store.transaction do  # begin transaction
284   #    store[:one] = 1     # this change is not applied, see below...
285   #    store[:two] = 2     # this change is not applied, see below...
286   #  
287   #    store.abort         # end transaction here, discard all changes
288   #  
289   #    store[:three] = 3   # this change is never reached
290   #  end
291   # 
292   # *WARNING*:  This method is only valid in a PStore#transaction.  It will
293   # raise PStore::Error if called at any other time.
294   #
295   def abort
296     in_transaction
297     @abort = true
298     throw :pstore_abort_transaction
299   end
301   #
302   # Opens a new transaction for the data store.  Code executed inside a block
303   # passed to this method may read and write data to and from the data store 
304   # file.
305   # 
306   # At the end of the block, changes are committed to the data store
307   # automatically.  You may exit the transaction early with a call to either 
308   # PStore#commit or PStore#abort.  See those methods for details about how
309   # changes are handled.  Raising an uncaught Exception in the block is 
310   # equivalent to calling PStore#abort.
311   # 
312   # If _read_only_ is set to +true+, you will only be allowed to read from the
313   # data store during the transaction and any attempts to change the data will
314   # raise a PStore::Error.
315   # 
316   # Note that PStore does not support nested transactions.
317   #
318   def transaction(read_only = false, &block)  # :yields:  pstore
319     value = nil
320     raise PStore::Error, "nested transaction" if @transaction
321     @lock.synchronize do
322       @rdonly = read_only
323       @transaction = true
324       @abort = false
325       file = open_and_lock_file(@filename, read_only)
326       if file
327         begin
328           @table, checksum, original_data_size = load_data(file, read_only)
329           
330           catch(:pstore_abort_transaction) do
331             value = yield(self)
332           end
333           
334           if !@abort && !read_only
335             save_data(checksum, original_data_size, file)
336           end
337         ensure
338           file.close if !file.closed?
339         end
340       else
341         # This can only occur if read_only == true.
342         @table = {}
343         catch(:pstore_abort_transaction) do
344           value = yield(self)
345         end
346       end
347     end
348     value
349   ensure
350     @transaction = false
351   end
352   
353   private
354   # Constant for relieving Ruby's garbage collector.
355   EMPTY_STRING = ""
356   EMPTY_MARSHAL_DATA = Marshal.dump({})
357   EMPTY_MARSHAL_CHECKSUM = Digest::MD5.digest(EMPTY_MARSHAL_DATA)
358   
359   class DummyMutex
360     def synchronize
361       yield
362     end
363   end
364   
365   #
366   # Open the specified filename (either in read-only mode or in
367   # read-write mode) and lock it for reading or writing.
368   #
369   # The opened File object will be returned. If _read_only_ is true,
370   # and the file does not exist, then nil will be returned.
371   #
372   # All exceptions are propagated.
373   #
374   def open_and_lock_file(filename, read_only)
375     if read_only
376       begin
377         file = File.new(filename, RD_ACCESS)
378         begin
379           file.flock(File::LOCK_SH)
380           return file
381         rescue
382           file.close
383           raise
384         end
385       rescue Errno::ENOENT
386         return nil
387       end
388     else
389       file = File.new(filename, RDWR_ACCESS)
390       file.flock(File::LOCK_EX)
391       return file
392     end
393   end
394   
395   # Load the given PStore file.
396   # If +read_only+ is true, the unmarshalled Hash will be returned.
397   # If +read_only+ is false, a 3-tuple will be returned: the unmarshalled
398   # Hash, an MD5 checksum of the data, and the size of the data.
399   def load_data(file, read_only)
400     if read_only
401       begin
402         table = load(file)
403         if !table.is_a?(Hash)
404           raise Error, "PStore file seems to be corrupted."
405         end
406       rescue EOFError
407         # This seems to be a newly-created file.
408         table = {}
409       end
410       table
411     else
412       data = file.read
413       if data.empty?
414         # This seems to be a newly-created file.
415         table = {}
416         checksum = empty_marshal_checksum
417         size = empty_marshal_data.size
418       else
419         table = load(data)
420         checksum = Digest::MD5.digest(data)
421         size = data.size
422         if !table.is_a?(Hash)
423           raise Error, "PStore file seems to be corrupted."
424         end
425       end
426       data.replace(EMPTY_STRING)
427       [table, checksum, size]
428     end
429   end
430   
431   def on_windows?
432     is_windows = RUBY_PLATFORM =~ /mswin/  ||
433                  RUBY_PLATFORM =~ /mingw/  ||
434                  RUBY_PLATFORM =~ /bbcwin/ ||
435                  RUBY_PLATFORM =~ /wince/
436     self.class.__send__(:define_method, :on_windows?) do
437       is_windows
438     end
439     is_windows
440   end
441   
442   # Check whether Marshal.dump supports the 'canonical' option. This option
443   # makes sure that Marshal.dump always dumps data structures in the same order.
444   # This is important because otherwise, the checksums that we generate may differ.
445   def marshal_dump_supports_canonical_option?
446     begin
447       Marshal.dump(nil, -1, true)
448       result = true
449     rescue
450       result = false
451     end
452     self.class.__send__(:define_method, :marshal_dump_supports_canonical_option?) do
453       result
454     end
455     result
456   end
457   
458   def save_data(original_checksum, original_file_size, file)
459     # We only want to save the new data if the size or checksum has changed.
460     # This results in less filesystem calls, which is good for performance.
461     if marshal_dump_supports_canonical_option?
462       new_data = Marshal.dump(@table, -1, true)
463     else
464       new_data = dump(@table)
465     end
466     new_checksum = Digest::MD5.digest(new_data)
467     
468     if new_data.size != original_file_size || new_checksum != original_checksum
469       if @ultra_safe && !on_windows?
470         # Windows doesn't support atomic file renames.
471         save_data_with_atomic_file_rename_strategy(new_data, file)
472       else
473         save_data_with_fast_strategy(new_data, file)
474       end
475     end
476     
477     new_data.replace(EMPTY_STRING)
478   end
479   
480   def save_data_with_atomic_file_rename_strategy(data, file)
481     temp_filename = "#{@filename}.tmp.#{Process.pid}.#{rand 1000000}"
482     temp_file = File.new(temp_filename, WR_ACCESS)
483     begin
484       temp_file.flock(File::LOCK_EX)
485       temp_file.write(data)
486       temp_file.flush
487       File.rename(temp_filename, @filename)
488     rescue
489       File.unlink(temp_file) rescue nil
490       raise
491     ensure
492       temp_file.close
493     end
494   end
495   
496   def save_data_with_fast_strategy(data, file)
497     file.rewind
498     file.truncate(0)
499     file.write(data)
500   end
503   # This method is just a wrapped around Marshal.dump
504   # to allow subclass overriding used in YAML::Store.
505   def dump(table)  # :nodoc:
506     Marshal::dump(table)
507   end
509   # This method is just a wrapped around Marshal.load.
510   # to allow subclass overriding used in YAML::Store.
511   def load(content)  # :nodoc:
512     Marshal::load(content)
513   end
515   def empty_marshal_data
516     EMPTY_MARSHAL_DATA
517   end
518   def empty_marshal_checksum
519     EMPTY_MARSHAL_CHECKSUM
520   end
523 # :enddoc:
525 if __FILE__ == $0
526   db = PStore.new("/tmp/foo")
527   db.transaction do
528     p db.roots
529     ary = db["root"] = [1,2,3,4]
530     ary[1] = [1,1.5]
531   end
533   1000.times do
534     db.transaction do
535       db["root"][0] += 1
536       p db["root"][0]
537     end
538   end
540   db.transaction(true) do
541     p db["root"]
542   end