Guest
Public paste!

Untitled

By: a guest | Mar 26th, 2008 | Syntax: Ruby | Size: 15.21 KB | Hits: 593 | Expires: Never
Copy text to clipboard
  1. # = PStore -- Transactional File Storage for Ruby Objects
  2. #
  3. # pstore.rb -
  4. #   originally by matz
  5. #   documentation by Kev Jackson and James Edward Gray II
  6. #   improved by Hongli Lai
  7. #
  8. # See PStore for documentation.
  9.  
  10.  
  11. require "fileutils"
  12. require "digest/md5"
  13. require "thread"
  14.  
  15. #
  16. # PStore implements a file based persistance 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 upated 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.
  28. #
  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
  81. #
  82. # == Transaction limitations:
  83. #
  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.
  88. #
  89. # You can prevent this by setting <em>pstore.ultra_safe_transactions = 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_transactions+ for details.
  93. #
  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
  101.  
  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_transactions
  114.  
  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_transactions = false
  134.     if @thread_safe
  135.       @lock = Mutex.new
  136.     else
  137.       @lock = DummyMutex.new
  138.     end
  139.   end
  140.  
  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
  154.  
  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
  221.  
  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
  246.  
  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
  300.  
  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.         if @ultra_safe_transactions && file_renames_are_atomic?
  328.           value = run_transaction_with_atomic_file_rename_strategy(file, read_only, &block)
  329.         else
  330.           value = run_transaction_with_regular_strategy(file, read_only, &block)
  331.         end
  332.       else
  333.         # This can only occur if read_only == true.
  334.         @table = {}
  335.         catch(:pstore_abort_transaction) do
  336.           value = yield(self)
  337.         end
  338.       end
  339.     end
  340.   ensure
  341.     @transaction = false
  342.     value
  343.   end
  344.  
  345.   private
  346.   class DummyMutex
  347.     def synchronize
  348.       yield
  349.     end
  350.   end
  351.  
  352.   #
  353.   # Open the specified filename (either in read-only mode or in
  354.   # read-write mode) and lock it for reading or writing.
  355.   #
  356.   # The opened File object will be returned. If _read_only_ is true,
  357.   # and the file does not exist, then nil will be returned.
  358.   #
  359.   # All exceptions are propagated.
  360.   #
  361.   def open_and_lock_file(filename, read_only)
  362.     if read_only
  363.       begin
  364.         file = File.new(filename, RD_ACCESS)
  365.         begin
  366.           file.flock(File::LOCK_SH)
  367.           return file
  368.         rescue
  369.           file.close
  370.           raise
  371.         end
  372.       rescue Errno::ENOENT
  373.         return nil
  374.       end
  375.     else
  376.       file = File.new(filename, RDWR_ACCESS)
  377.       file.flock(File::LOCK_EX)
  378.       return file
  379.     end
  380.   end
  381.  
  382.   def file_renames_are_atomic?
  383.     # File renames are atomic on POSIX platforms. As far as I know, Windows is
  384.     # the only non-POSIX platform that Ruby runs on.
  385.     #
  386.     # See also the following URL for information on atomic file writes on Windows:
  387.     # http://blogs.msdn.com/adioltean/archive/2005/12/28/507866.aspx
  388.     # This looks way too complicated: performance would go down the drain if we
  389.     # implement this. Maybe it's a good idea to use TxF in the future, but at the
  390.     # moment very few people use Vista and we'd need to write yet-another native
  391.     # extension.
  392.     @@file_renames_are_atomic ||= \
  393.       RUBY_PLATFORM !~ /mswin/ &&
  394.       RUBY_PLATFORM !~ /mingw/ &&
  395.       RUBY_PLATFORM !~ /bbcwin/ &&
  396.       RUBY_PLATFORM !~ /wince/
  397.   end
  398.  
  399.   def run_transaction_with_atomic_file_rename_strategy(file, read_only)
  400.     value = nil
  401.    
  402.     # Load PStore data file.
  403.     begin
  404.       @table = Marshal.load(file)
  405.       if !@table.is_a?(Hash)
  406.         raise Error, "PStore file seems to be corrupted."
  407.       end
  408.     rescue EOFError
  409.       # This is a newly-created file.
  410.       @table = {}
  411.     end
  412.    
  413.     # Run the transaction block.
  414.     catch(:pstore_abort_transaction) do
  415.       value = yield(self)
  416.     end
  417.    
  418.     # Write new data back to PStore file, if necessary. We write to a temporary file,
  419.     # then rename that to the original file.
  420.     if !@abort && !read_only
  421.       temp_filename = "#{@filename}.tmp.#{Process.pid}.#{rand 1000000}"
  422.       temp_file = File.new(temp_filename, WR_ACCESS)
  423.       begin
  424.         Marshal.dump(@table, temp_file)
  425.         File.rename(temp_filename, @filename)
  426.       rescue
  427.         File.unlink(temp_file) rescue nil
  428.         raise
  429.       ensure
  430.         temp_file.close
  431.       end
  432.     end
  433.   ensure
  434.     file.close
  435.     value
  436.   end
  437.  
  438.   def run_transaction_with_regular_strategy(file, read_only)
  439.     value = nil
  440.    
  441.     # Load PStore data file.
  442.     begin
  443.       @table = Marshal.load(file)
  444.       if !@table.is_a?(Hash)
  445.         raise Error, "PStore file seems to be corrupted."
  446.       end
  447.     rescue EOFError
  448.       # This is a newly-created file.
  449.       @table = {}
  450.     end
  451.    
  452.     # Run the transaction block.
  453.     catch(:pstore_abort_transaction) do
  454.       value = yield(self)
  455.     end
  456.    
  457.     # Write new data back to PStore file, if necessary.
  458.     if !@abort && !read_only
  459.       # We must write the marshal data to a temporary buffer,
  460.       # because Marshal.dump might fail and we don't want to
  461.       # corrupt the file.
  462.       data = Marshal.dump(@table)
  463.       file.rewind
  464.       file.truncate(0)
  465.       file.write(data)
  466.       # Free the marshal data; this is good for the garbage collector.
  467.       data.replace("")
  468.     end
  469.   ensure
  470.     file.close
  471.     value
  472.   end
  473. end
  474.  
  475. # :enddoc:
  476.  
  477. if __FILE__ == $0
  478.   db = PStore.new("/tmp/foo")
  479.   db.transaction do
  480.     p db.roots
  481.     ary = db["root"] = [1,2,3,4]
  482.     ary[1] = [1,1.5]
  483.   end
  484.  
  485.   1000.times do
  486.     db.transaction do
  487.       db["root"][0] += 1
  488.       p db["root"][0]
  489.     end
  490.   end
  491.  
  492.   db.transaction(true) do
  493.     p db["root"]
  494.   end
  495. end