Class: PStore

Inherits:
Object
  • Object
show all
Defined in:
lib/pstore.rb

Overview

PStore implements a file based persistance 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 upated 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

Defined Under Namespace

Classes: Error

Constant Summary collapse

RDWR_ACCESS =
File::RDWR | File::CREAT | binmode
RD_ACCESS =
File::RDONLY | binmode
WR_ACCESS =
File::WRONLY | File::CREAT | File::TRUNC | binmode

Instance Method Summary collapse

Constructor Details

#initialize(file) ⇒ PStore

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



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/pstore.rb', line 94

def initialize(file)
  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
  @transaction = false
  @filename = file
  @abort = false
end

Instance Method Details

#[](name) ⇒ Object

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 PStore#transaction. It will raise PStore::Error if called at any other time.



128
129
130
131
# File 'lib/pstore.rb', line 128

def [](name)
  in_transaction
  @table[name]
end

#[]=(name, value) ⇒ Object

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 PStore#transaction and it cannot be read-only. It will raise PStore::Error if called at any other time.



173
174
175
176
# File 'lib/pstore.rb', line 173

def []=(name, value)
  in_transaction_wr()
  @table[name] = value
end

#abortObject

Ends the current PStore#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 PStore#transaction. It will raise PStore::Error if called at any other time.



261
262
263
264
265
# File 'lib/pstore.rb', line 261

def abort
  in_transaction
  @abort = true
  throw :pstore_abort_transaction
end

#commitObject

Ends the current PStore#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 PStore#transaction. It will raise PStore::Error if called at any other time.



235
236
237
238
239
# File 'lib/pstore.rb', line 235

def commit
  in_transaction
  @abort = false
  throw :pstore_abort_transaction
end

#delete(name) ⇒ Object

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

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



183
184
185
186
# File 'lib/pstore.rb', line 183

def delete(name)
  in_transaction_wr()
  @table.delete name
end

#dump(table) ⇒ Object

This method is just a wrapped around Marshal.dump.



348
349
350
# File 'lib/pstore.rb', line 348

def dump(table)  # :nodoc:
  Marshal::dump(table)
end

#fetch(name, default = PStore::Error) ⇒ Object

This method is just like PStore#[], 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 PStore#transaction. It will raise PStore::Error if called at any other time.



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/pstore.rb', line 142

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

#load(content) ⇒ Object

This method is just a wrapped around Marshal.load.



353
354
355
# File 'lib/pstore.rb', line 353

def load(content)  # :nodoc:
  Marshal::load(content)
end

#load_file(file) ⇒ Object

This method is just a wrapped around Marshal.load.



358
359
360
# File 'lib/pstore.rb', line 358

def load_file(file)  # :nodoc:
  Marshal::load(file)
end

#pathObject

Returns the path to the data store file.



209
210
211
# File 'lib/pstore.rb', line 209

def path
  @filename
end

#root?(name) ⇒ Boolean

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

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

Returns:

  • (Boolean)


204
205
206
207
# File 'lib/pstore.rb', line 204

def root?(name)
  in_transaction
  @table.key? name
end

#rootsObject

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

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



194
195
196
197
# File 'lib/pstore.rb', line 194

def roots
  in_transaction
  @table.keys
end

#transaction(read_only = false) ⇒ Object

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 PStore#commit or PStore#abort. See those methods for details about how changes are handled. Raising an uncaught Exception in the block is equivalent to calling PStore#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.

Raises:



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/pstore.rb', line 284

def transaction(read_only=false)  # :yields:  pstore
  raise PStore::Error, "nested transaction" if @transaction
  begin
    @rdonly = read_only
    @abort = false
    @transaction = true
    value = nil
    new_file = @filename + ".new"

    content = nil
    unless read_only
      file = File.open(@filename, RDWR_ACCESS)
      file.flock(File::LOCK_EX)
      commit_new(file) if FileTest.exist?(new_file)
      content = file.read()
    else
      begin
        file = File.open(@filename, RD_ACCESS)
        file.flock(File::LOCK_SH)
        content = (File.open(new_file, RD_ACCESS) {|n| n.read} rescue file.read())
      rescue Errno::ENOENT
        content = ""
      end
    end

    if content != ""
	@table = load(content)
      if !read_only
        size = content.size
        md5 = Digest::MD5.digest(content)
      end
    else
	@table = {}
    end
    content = nil		# unreference huge data

    begin
	catch(:pstore_abort_transaction) do
 value = yield(self)
	end
    rescue Exception
	@abort = true
	raise
    ensure
	if !read_only and !@abort
        tmp_file = @filename + ".tmp"
 content = dump(@table)
 if !md5 || size != content.size || md5 != Digest::MD5.digest(content)
          File.open(tmp_file, WR_ACCESS) {|t| t.write(content)}
          File.rename(tmp_file, new_file)
          commit_new(file)
        end
        content = nil		# unreference huge data
	end
    end
  ensure
    @table = nil
    @transaction = false
    file.close if file
  end
  value
end