Class PStore
In: lib/pstore.rb
Parent: Object

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

Methods

[]   []=   abort   commit   commit_new   delete   fetch   in_transaction   in_transaction_wr   new   path   root?   roots   transaction  

Classes and Modules

Class PStore::Error

Constants

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

Public Class methods

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

[Source]

     # File lib/pstore.rb, line 94
 94:   def initialize(file)
 95:     dir = File::dirname(file)
 96:     unless File::directory? dir
 97:       raise PStore::Error, format("directory %s does not exist", dir)
 98:     end
 99:     if File::exist? file and not File::readable? file
100:       raise PStore::Error, format("file %s not readable", file)
101:     end
102:     @transaction = false
103:     @filename = file
104:     @abort = false
105:   end

Public Instance methods

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.

[Source]

     # File lib/pstore.rb, line 128
128:   def [](name)
129:     in_transaction
130:     @table[name]
131:   end

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.

[Source]

     # File lib/pstore.rb, line 173
173:   def []=(name, value)
174:     in_transaction_wr()
175:     @table[name] = value
176:   end

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.

[Source]

     # File lib/pstore.rb, line 261
261:   def abort
262:     in_transaction
263:     @abort = true
264:     throw :pstore_abort_transaction
265:   end

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.

[Source]

     # File lib/pstore.rb, line 235
235:   def commit
236:     in_transaction
237:     @abort = false
238:     throw :pstore_abort_transaction
239:   end

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.

[Source]

     # File lib/pstore.rb, line 183
183:   def delete(name)
184:     in_transaction_wr()
185:     @table.delete name
186:   end

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.

[Source]

     # File lib/pstore.rb, line 142
142:   def fetch(name, default=PStore::Error)
143:     in_transaction
144:     unless @table.key? name
145:       if default==PStore::Error
146:         raise PStore::Error, format("undefined root name `%s'", name)
147:       else
148:         return default
149:       end
150:     end
151:     @table[name]
152:   end

Returns the path to the data store file.

[Source]

     # File lib/pstore.rb, line 209
209:   def path
210:     @filename
211:   end

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.

[Source]

     # File lib/pstore.rb, line 204
204:   def root?(name)
205:     in_transaction
206:     @table.key? name
207:   end

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.

[Source]

     # File lib/pstore.rb, line 194
194:   def roots
195:     in_transaction
196:     @table.keys
197:   end

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.

[Source]

     # File lib/pstore.rb, line 284
284:   def transaction(read_only=false)  # :yields:  pstore
285:     raise PStore::Error, "nested transaction" if @transaction
286:     begin
287:       @rdonly = read_only
288:       @abort = false
289:       @transaction = true
290:       value = nil
291:       new_file = @filename + ".new"
292: 
293:       content = nil
294:       unless read_only
295:         file = File.open(@filename, RDWR_ACCESS)
296:         file.flock(File::LOCK_EX)
297:         commit_new(file) if FileTest.exist?(new_file)
298:         content = file.read()
299:       else
300:         begin
301:           file = File.open(@filename, RD_ACCESS)
302:           file.flock(File::LOCK_SH)
303:           content = (File.open(new_file, RD_ACCESS) {|n| n.read} rescue file.read())
304:         rescue Errno::ENOENT
305:           content = ""
306:         end
307:       end
308: 
309:       if content != ""
310:         @table = load(content)
311:         if !read_only
312:           size = content.size
313:           md5 = Digest::MD5.digest(content)
314:         end
315:       else
316:         @table = {}
317:       end
318:       content = nil             # unreference huge data
319: 
320:       begin
321:         catch(:pstore_abort_transaction) do
322:           value = yield(self)
323:         end
324:       rescue Exception
325:         @abort = true
326:         raise
327:       ensure
328:         if !read_only and !@abort
329:           tmp_file = @filename + ".tmp"
330:           content = dump(@table)
331:           if !md5 || size != content.size || md5 != Digest::MD5.digest(content)
332:             File.open(tmp_file, WR_ACCESS) {|t| t.write(content)}
333:             File.rename(tmp_file, new_file)
334:             commit_new(file)
335:           end
336:           content = nil         # unreference huge data
337:         end
338:       end
339:     ensure
340:       @table = nil
341:       @transaction = false
342:       file.close if file
343:     end
344:     value
345:   end

Private Instance methods

Commits changes to the data store file.

[Source]

     # File lib/pstore.rb, line 364
364:   def commit_new(f)
365:     f.truncate(0)
366:     f.rewind
367:     new_file = @filename + ".new"
368:     File.open(new_file, RD_ACCESS) do |nf|
369:       FileUtils.copy_stream(nf, f)
370:     end
371:     File.unlink(new_file)
372:   end

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

[Source]

     # File lib/pstore.rb, line 108
108:   def in_transaction
109:     raise PStore::Error, "not in transaction" unless @transaction
110:   end

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

[Source]

     # File lib/pstore.rb, line 115
115:   def in_transaction_wr()
116:     in_transaction()
117:     raise PStore::Error, "in read-only transaction" if @rdonly
118:   end

[Validate]