class PStore

PStore implements a file based persistence mechanism based on a Hash. User code can store hierarchies of Ruby objects (values) into the data store file by name (keys). An object hierarchy may be just a single object. User code may later read values back from the data store or even update data, as needed.

The transactional behavior ensures that any changes succeed or fail together. This can be used to ensure that the data store is not left in a transitory state, where some values were updated but others were not.

Behind the scenes, Ruby objects are stored to the data store file with Marshal. That carries the usual limitations. Proc objects cannot be marshalled, for example.

Usage example:

require "pstore"
# a mock wiki object...
class WikiPage
 def initialize( page_name, author, contents )
 @page_name = page_name
 @revisions = Array.new
 add_revision(author, contents)
 end
 attr_reader :page_name
 def add_revision( author, contents )
 @revisions << { :created => Time.now,
 :author => author,
 :contents => contents }
 end
 def wiki_page_references
 [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/)
 end
 # ...
end
# create a new page...
home_page = WikiPage.new( "HomePage", "James Edward Gray II",
 "A page about the JoysOfDocumentation..." )
# then we want to update page data and the index together, or not at all...
wiki = PStore.new("wiki_pages.pstore")
wiki.transaction do # begin transaction; do all of this or none of it
 # store page...
 wiki[home_page.page_name] = home_page
 # ensure that an index has been created...
 wiki[:wiki_index] ||= Array.new
 # update wiki index...
 wiki[:wiki_index].push(*home_page.wiki_page_references)
end # commit changes to wiki data store file
### Some time later... ###
# read wiki data...
wiki.transaction(true) do # begin read-only transaction, no changes allowed
 wiki.roots.each do |data_root_name|
 p data_root_name
 p wiki[data_root_name]
 end
end

Transaction modes

By default, file integrity is only ensured as long as the operating system (and the underlying hardware) doesn't raise any unexpected I/O errors. If an I/O error occurs while PStore is writing to its file, then the file will become corrupted.

You can prevent this by setting pstore.ultra_safe = true. However, this results in a minor performance loss, and only works on platforms that support atomic file renames. Please consult the documentation for ultra_safe for details.

Needless to say, if you're storing valuable data with PStore, then you should backup the PStore files from time to time.

Constants

EMPTY_MARSHAL_CHECKSUM
EMPTY_MARSHAL_DATA
EMPTY_STRING

Constant for relieving Ruby's garbage collector.

RDWR_ACCESS
RD_ACCESS
WR_ACCESS

Attributes

ultra_safe[RW]

Whether PStore should do its best to prevent file corruptions, even when under unlikely-to-occur error conditions such as out-of-space conditions and other unusual OS filesystem errors. Setting this flag comes at the price in the form of a performance loss.

This flag only has effect on platforms on which file renames are atomic (e.g. all POSIX platforms: Linux, MacOS X, FreeBSD, etc). The default value is false.

Public Class Methods

new(file, thread_safe = false) click to toggle source

To construct a PStore object, pass in the file path where you would like the data to be stored.

PStore objects are always reentrant. But if thread_safe is set to true, then it will become thread-safe at the cost of a minor performance hit.

# File lib/pstore.rb, line 118
def initialize(file, thread_safe = false)
 dir = File::dirname(file)
 unless File::directory? dir
 raise PStore::Error, format("directory %s does not exist", dir)
 end
 if File::exist? file and not File::readable? file
 raise PStore::Error, format("file %s not readable", file)
 end
 @filename = file
 @abort = false
 @ultra_safe = false
 @thread_safe = thread_safe
 @lock = Mutex.new
end

Public Instance Methods

[](name) click to toggle source

Retrieves a value from the PStore file data, by name. The hierarchy of Ruby objects stored under that root name will be returned.

WARNING: This method is only valid in a #transaction. It will raise PStore::Error if called at any other time.

# File lib/pstore.rb, line 154
def [](name)
 in_transaction
 @table[name]
end
[]=(name, value) click to toggle source

Stores an individual Ruby object or a hierarchy of Ruby objects in the data store file under the root name. Assigning to a name already in the data store clobbers the old data.

Example:

require "pstore"
store = PStore.new("data_file.pstore")
store.transaction do # begin transaction
 # load some data into the store...
 store[:single_object] = "My data..."
 store[:obj_heirarchy] = { "Kev Jackson" => ["rational.rb", "pstore.rb"],
 "James Gray" => ["erb.rb", "pstore.rb"] }
end # commit changes to data store file

WARNING: This method is only valid in a #transaction and it cannot be read-only. It will raise PStore::Error if called at any other time.

# File lib/pstore.rb, line 199
def []=(name, value)
 in_transaction_wr
 @table[name] = value
end
abort() click to toggle source

Ends the current #transaction, discarding any changes to the data store.

Example:

require "pstore"
store = PStore.new("data_file.pstore")
store.transaction do # begin transaction
 store[:one] = 1 # this change is not applied, see below...
 store[:two] = 2 # this change is not applied, see below...
 store.abort # end transaction here, discard all changes
 store[:three] = 3 # this change is never reached
end

WARNING: This method is only valid in a #transaction. It will raise PStore::Error if called at any other time.

# File lib/pstore.rb, line 287
def abort
 in_transaction
 @abort = true
 throw :pstore_abort_transaction
end
commit() click to toggle source

Ends the current #transaction, committing any changes to the data store immediately.

Example:

require "pstore"
store = PStore.new("data_file.pstore")
store.transaction do # begin transaction
 # load some data into the store...
 store[:one] = 1
 store[:two] = 2
 store.commit # end transaction here, committing changes
 store[:three] = 3 # this change is never reached
end

WARNING: This method is only valid in a #transaction. It will raise PStore::Error if called at any other time.

# File lib/pstore.rb, line 261
def commit
 in_transaction
 @abort = false
 throw :pstore_abort_transaction
end
delete(name) click to toggle source

Removes an object hierarchy from the data store, by name.

WARNING: This method is only valid in a #transaction and it cannot be read-only. It will raise PStore::Error if called at any other time.

# File lib/pstore.rb, line 209
def delete(name)
 in_transaction_wr
 @table.delete name
end
fetch(name, default=PStore::Error) click to toggle source

This method is just like #[], save that you may also provide a default value for the object. In the event the specified name is not found in the data store, your default will be returned instead. If you do not specify a default, PStore::Error will be raised if the object is not found.

WARNING: This method is only valid in a #transaction. It will raise PStore::Error if called at any other time.

# File lib/pstore.rb, line 168
def fetch(name, default=PStore::Error)
 in_transaction
 unless @table.key? name
 if default == PStore::Error
 raise PStore::Error, format("undefined root name `%s'", name)
 else
 return default
 end
 end
 @table[name]
end
path() click to toggle source

Returns the path to the data store file.

# File lib/pstore.rb, line 235
def path
 @filename
end
root?(name) click to toggle source

Returns true if the supplied name is currently in the data store.

WARNING: This method is only valid in a #transaction. It will raise PStore::Error if called at any other time.

# File lib/pstore.rb, line 230
def root?(name)
 in_transaction
 @table.key? name
end
roots() click to toggle source

Returns the names of all object hierarchies currently in the store.

WARNING: This method is only valid in a #transaction. It will raise PStore::Error if called at any other time.

# File lib/pstore.rb, line 220
def roots
 in_transaction
 @table.keys
end
transaction(read_only = false) { |pstore| ... } click to toggle source

Opens a new transaction for the data store. Code executed inside a block passed to this method may read and write data to and from the data store file.

At the end of the block, changes are committed to the data store automatically. You may exit the transaction early with a call to either #commit or #abort. See those methods for details about how changes are handled. Raising an uncaught Exception in the block is equivalent to calling #abort.

If read_only is set to true, you will only be allowed to read from the data store during the transaction and any attempts to change the data will raise a PStore::Error.

Note that PStore does not support nested transactions.

# File lib/pstore.rb, line 310
def transaction(read_only = false) # :yields: pstore
 value = nil
 if !@thread_safe
 raise PStore::Error, "nested transaction" unless @lock.try_lock
 else
 begin
 @lock.lock
 rescue ThreadError
 raise PStore::Error, "nested transaction"
 end
 end
 begin
 @rdonly = read_only
 @abort = false
 file = open_and_lock_file(@filename, read_only)
 if file
 begin
 @table, checksum, original_data_size = load_data(file, read_only)
 catch(:pstore_abort_transaction) do
 value = yield(self)
 end
 if !@abort && !read_only
 save_data(checksum, original_data_size, file)
 end
 ensure
 file.close if !file.closed?
 end
 else
 # This can only occur if read_only == true.
 @table = {}
 catch(:pstore_abort_transaction) do
 value = yield(self)
 end
 end
 ensure
 @lock.unlock
 end
 value
end

Private Instance Methods

empty_marshal_checksum() click to toggle source
# File lib/pstore.rb, line 481
def empty_marshal_checksum
 EMPTY_MARSHAL_CHECKSUM
end
empty_marshal_data() click to toggle source
# File lib/pstore.rb, line 478
def empty_marshal_data
 EMPTY_MARSHAL_DATA
end
in_transaction() click to toggle source

Raises PStore::Error if the calling code is not in a #transaction.

# File lib/pstore.rb, line 134
def in_transaction
 raise PStore::Error, "not in transaction" unless @lock.locked?
end
in_transaction_wr() click to toggle source

Raises PStore::Error if the calling code is not in a #transaction or if the code is in a read-only #transaction.

# File lib/pstore.rb, line 141
def in_transaction_wr
 in_transaction
 raise PStore::Error, "in read-only transaction" if @rdonly
end
load_data(file, read_only) click to toggle source

Load the given PStore file. If read_only is true, the unmarshalled Hash will be returned. If read_only is false, a 3-tuple will be returned: the unmarshalled Hash, an MD5 checksum of the data, and the size of the data.

# File lib/pstore.rb, line 392
def load_data(file, read_only)
 if read_only
 begin
 table = load(file)
 raise Error, "PStore file seems to be corrupted." unless table.is_a?(Hash)
 rescue EOFError
 # This seems to be a newly-created file.
 table = {}
 end
 table
 else
 data = file.read
 if data.empty?
 # This seems to be a newly-created file.
 table = {}
 checksum = empty_marshal_checksum
 size = empty_marshal_data.bytesize
 else
 table = load(data)
 checksum = Digest::MD5.digest(data)
 size = data.bytesize
 raise Error, "PStore file seems to be corrupted." unless table.is_a?(Hash)
 end
 data.replace(EMPTY_STRING)
 [table, checksum, size]
 end
end
on_windows?() click to toggle source
# File lib/pstore.rb, line 420
def on_windows?
 is_windows = RUBY_PLATFORM =~ /mswin|mingw|bccwin|wince/
 self.class.__send__(:define_method, :on_windows?) do
 is_windows
 end
 is_windows
end
open_and_lock_file(filename, read_only) click to toggle source

Open the specified filename (either in read-only mode or in read-write mode) and lock it for reading or writing.

The opened File object will be returned. If read_only is true, and the file does not exist, then nil will be returned.

All exceptions are propagated.

# File lib/pstore.rb, line 367
def open_and_lock_file(filename, read_only)
 if read_only
 begin
 file = File.new(filename, RD_ACCESS)
 begin
 file.flock(File::LOCK_SH)
 return file
 rescue
 file.close
 raise
 end
 rescue Errno::ENOENT
 return nil
 end
 else
 file = File.new(filename, RDWR_ACCESS)
 file.flock(File::LOCK_EX)
 return file
 end
end
save_data(original_checksum, original_file_size, file) click to toggle source
# File lib/pstore.rb, line 428
def save_data(original_checksum, original_file_size, file)
 new_data = dump(@table)
 if new_data.bytesize != original_file_size || Digest::MD5.digest(new_data) != original_checksum
 if @ultra_safe && !on_windows?
 # Windows doesn't support atomic file renames.
 save_data_with_atomic_file_rename_strategy(new_data, file)
 else
 save_data_with_fast_strategy(new_data, file)
 end
 end
 new_data.replace(EMPTY_STRING)
end
save_data_with_atomic_file_rename_strategy(data, file) click to toggle source
# File lib/pstore.rb, line 443
def save_data_with_atomic_file_rename_strategy(data, file)
 temp_filename = "#{@filename}.tmp.#{Process.pid}.#{rand 1000000}"
 temp_file = File.new(temp_filename, WR_ACCESS)
 begin
 temp_file.flock(File::LOCK_EX)
 temp_file.write(data)
 temp_file.flush
 File.rename(temp_filename, @filename)
 rescue
 File.unlink(temp_file) rescue nil
 raise
 ensure
 temp_file.close
 end
end
save_data_with_fast_strategy(data, file) click to toggle source
# File lib/pstore.rb, line 459
def save_data_with_fast_strategy(data, file)
 file.rewind
 file.write(data)
 file.truncate(data.bytesize)
end