класс PStore
PStore реализует механизм персистенции файлов, основанный на Hash. Код пользователя может хранить иерархии Ruby-объектов (значений) в хранилище по имени (ключам). Иерархия объектов может состоять всего из одного объекта. Впоследствии, код пользователя может читать значения из хранилища или даже обновлять данные по мере необходимости.
Транзакционное поведение гарантирует, что все изменения происходят успешно или полностью проваливаются. Это позволяет гарантировать, что хранилище данных не остаётся в переходном состоянии, где некоторые значения были обновлены, а другие — нет.
Внутри, Ruby-объекты сохраняются в файле хранилища с помощью Marshal. Это влечёт за собой стандартные ограничения. Например, объекты Proc не могут быть сериализованы.
Здесь три важных понятия (подробности см. в ссылках):
-
Хранилище: хранилище — это экземпляр PStore.
-
Элементы: хранилище похоже на хэш; каждый элемент — ключ сохранённого объекта.
-
Транзакции: каждая транзакция — это набор предполагаемых изменений в хранилище; транзакция определяется в блоке, переданном с вызовом
PStore#transaction.
О примерах
Примеры на этой странице нуждаются в хранилище с известными свойствами. Они могут получить новое (и заполненное) хранилище, вызвав:
example_store do |store| # Example code using store goes here. end
Всё, что нам нужно знать о example_store — это то, что оно возвращает свежее хранилище с известным набором элементов; его реализация:
require 'pstore'
require 'tempfile'
# Yield a pristine store for use in examples.
def example_store
# Create the store in a temporary file.
Tempfile.create do |file|
store = PStore.new(file)
# Populate the store.
store.transaction do
store[:foo] = 0
store[:bar] = 1
store[:baz] = 2
end
yield store
end
end
Хранилище
Содержимое хранилища поддерживается в файле, путь к которому указан при создании хранилища (см. PStore.new). Объекты сохраняются и извлекаются с помощью модуля Marshal, что означает, что некоторые объекты не могут быть добавлены в хранилище; см. Marshal::dump.
Элементы
Хранилище может содержать любое количество элементов. Каждый элемент имеет ключ и значение, как и в хэше:
-
Ключ: как и в хэше, ключ может быть (почти) любым объектом; см. Ключи хэша. Для простоты рекомендуется использовать символы или строки в качестве ключей.
-
Значение: значение может быть любым объектом, который может быть сериализован Marshal (см. Marshal::dump), и фактически может быть коллекцией (например, массивом, хэшем, множеством, диапазоном и т. д.). Эта коллекция может, в свою очередь, содержать вложенные объекты, включая коллекции, на любой глубине; эти объекты также должны быть сериализуемы. См. Иерархические значения.
Транзакции
Блок транзакции
Блок, переданный с вызовом метода transaction, содержит транзакцию, которая состоит из вызовов методов PStore, которые читают или записывают в хранилище (то есть, все методы PStore, кроме transaction самого, path, и Pstore.new):
example_store do |store|
store.transaction do
store.keys # => [:foo, :bar, :baz]
store[:bat] = 3
store.keys # => [:foo, :bar, :baz, :bat]
end
end
Выполнение транзакции откладывается до выхода из блока, и выполняется атомарно (все или ничего): либо все вызовы транзакции выполняются, либо ни один. Это поддерживает целостность хранилища.
Другой код в блоке (включая даже вызовы path и PStore.new) выполняется немедленно, не откладывается.
Блок транзакции:
-
Не может содержать вложенный вызов
transaction. -
Является единственным контекстом, где разрешены методы, которые читают или записывают в хранилище.
Как видно выше, изменения в транзакции автоматически вносятся при выходе из блока. Блок может быть завершён досрочно путём вызова метода commit или abort.
-
Methodcommitзапускает обновление хранилища и завершает блок:example_store do |store| store.transaction do store.keys # => [:foo, :bar, :baz] store[:bat] = 3 store.commit fail 'Cannot get here' end store.transaction do # Update was completed. store.keys # => [:foo, :bar, :baz, :bat] end end -
Methodabortотбрасывает обновление хранилища и завершает блок:example_store do |store| store.transaction do store.keys # => [:foo, :bar, :baz] store[:bat] = 3 store.abort fail 'Cannot get here' end store.transaction do # Update was not completed. store.keys # => [:foo, :bar, :baz] end end
Только чтение транзакций
По умолчанию транзакция позволяет как чтение, так и запись в хранилище:
store.transaction do # Read-write transaction. # Any code except a call to #transaction is allowed here. end
Если аргумент read_only передаётся как true, разрешено только чтение:
store.transaction(true) do # Read-only transaction: # Calls to #transaction, #[]=, and #delete are not allowed here. end
Иерархические значения
Значение для элемента может быть простым объектом (как показано выше). Оно также может быть иерархией объектов, вложенных на любой глубине:
deep_store = PStore.new('deep.store')
deep_store.transaction do
array_of_hashes = [{}, {}, {}]
deep_store[:array_of_hashes] = array_of_hashes
deep_store[:array_of_hashes] # => [{}, {}, {}]
hash_of_arrays = {foo: [], bar: [], baz: []}
deep_store[:hash_of_arrays] = hash_of_arrays
deep_store[:hash_of_arrays] # => {:foo=>[], :bar=>[], :baz=>[]}
deep_store[:hash_of_arrays][:foo].push(:bat)
deep_store[:hash_of_arrays] # => {:foo=>[:bat], :bar=>[], :baz=>[]}
end
И помните, что вы можете использовать методы dig methods в возвращаемой иерархии объектов.
Работа с хранилищем
Создание хранилища
Используйте метод PStore.new для создания хранилища. Новое хранилище создаёт или открывает содержащий его файл:
store = PStore.new('t.store')
Изменение хранилища
Используйте метод []= для обновления или создания элемента:
example_store do |store|
store.transaction do
store[:foo] = 1 # Update.
store[:bam] = 1 # Create.
end
end
Используйте метод delete для удаления элемента:
example_store do |store|
store.transaction do
store.delete(:foo)
store[:foo] # => nil
end
end
Получение значений
Используйте метод fetch (разрешает значение по умолчанию) или [] (значение по умолчанию nil) для получения элемента:
example_store do |store|
store.transaction do
store[:foo] # => 0
store[:nope] # => nil
store.fetch(:baz) # => 2
store.fetch(:nope, nil) # => nil
store.fetch(:nope) # Raises exception.
end
end
Запрос к хранилищу
Используйте метод key? для определения наличия заданного ключа:
example_store do |store|
store.transaction do
store.key?(:foo) # => true
end
end
Используйте метод keys для получения ключей:
example_store do |store|
store.transaction do
store.keys # => [:foo, :bar, :baz]
end
end
Используйте метод path для получения пути к основному файлу хранилища; этот метод можно вызвать вне блока транзакции:
store = PStore.new('t.store')
store.path # => "t.store"
Безопасность транзакций
Для безопасности транзакций см.:
-
Дополнительный аргумент
thread_safeв методеPStore.new. -
Атрибут
ultra_safe.
Само собой разумеется, если вы храните ценные данные с помощью PStore, вам следует периодически создавать резервные копии файла PStore.
Пример хранилища
require "pstore"
# A mock wiki object.
class WikiPage
attr_reader :page_name
def initialize(page_name, author, contents)
@page_name = page_name
@revisions = Array.new
add_revision(author, contents)
end
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 wiki page.
home_page = WikiPage.new("HomePage", "James Edward Gray II",
"A page about the JoysOfDocumentation..." )
wiki = PStore.new("wiki_pages.pstore")
# Update page data and the index together, or not at all.
wiki.transaction do
# Store page.
wiki[home_page.page_name] = home_page
# Create page index.
wiki[:wiki_index] ||= Array.new
# Update wiki index.
wiki[:wiki_index].push(*home_page.wiki_page_references)
end
# Read wiki data, setting argument read_only to true.
wiki.transaction(true) do
wiki.keys.each do |key|
puts key
puts wiki[key]
end
end
Константы
- CHECKSUM_ALGO
-
Константа для облегчения работы сборщика мусора Ruby.
- EMPTY_MARSHAL_CHECKSUM
- EMPTY_MARSHAL_DATA
- EMPTY_STRING
- RDWR_ACCESS
- RD_ACCESS
- VERSION
- WR_ACCESS
Атрибуты
Определяет, должен ли PStore прилагать все усилия для предотвращения повреждения файла, даже при возникновении маловероятных ошибок (таких как ошибка памяти или ошибка файловой системы):
-
true: изменения публикуются путем создания временного файла, записи обновленных данных в него, а затем переименования файла в указанныйpath. ЦелостностьFileсохраняется. Примечание: действует только если файловая система имеет атомарное переименование файлов (как в платформах POSIX Linux, MacOS, FreeBSD и др.). -
false(по умолчанию): изменения публикуются путем перемотки открытого файла и записи обновленных данных. ЦелостностьFileсохраняется, если файловая система не генерирует неожиданных ошибок ввода-вывода; если во время записи в хранилище произойдет такая ошибка, файл может стать поврежденным.
Общедоступные методы класса
# File lib/pstore.rb, line 372
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 = Thread::Mutex.new
end Возвращает новый объект PStore.
Аргумент file — путь к файлу, в котором будут храниться объекты; если файл существует, он должен быть создан PStore.
path = 't.store' store = PStore.new(path)
Объект PStore является реентерабельным. Если аргумент thread_safe задан как true, объект также является потокобезопасным (с небольшой потерей производительности):
store = PStore.new(path, true)
Общедоступные методы экземпляра
# File lib/pstore.rb, line 417 def [](key) in_transaction @table[key] end
Возвращает значение для данного key, если ключ существует. nil в противном случае; если не nil, возвращаемое значение — объект или иерархия объектов:
example_store do |store|
store.transaction do
store[:foo] # => 0
store[:nope] # => nil
end
end
Возвращает nil, если такого ключа нет.
См. также Иерархические значения.
Вызывает исключение, если вызывается вне блока транзакции.
# File lib/pstore.rb, line 459 def []=(key, value) in_transaction_wr @table[key] = value end
Создает или заменяет значение для данного key:
example_store do |store|
temp.transaction do
temp[:bat] = 3
end
end
См. также Иерархические значения.
Вызывает исключение, если вызывается вне блока транзакции.
# File lib/pstore.rb, line 539 def abort in_transaction @abort = true throw :pstore_abort_transaction end
Выходит из текущего блока транзакции, отбрасывая все изменения, указанные в блоке транзакции. См. Фиксация или откат.
Вызывает исключение, если вызывается вне блока транзакции.
# File lib/pstore.rb, line 528 def commit in_transaction @abort = false throw :pstore_abort_transaction end
Выходит из текущего блока транзакции, фиксируя все изменения, указанные в блоке транзакции. См. Фиксация или откат.
Вызывает исключение, если вызывается вне блока транзакции.
# File lib/pstore.rb, line 476 def delete(key) in_transaction_wr @table.delete key end
Удаляет и возвращает значение по key, если оно существует:
example_store do |store|
store.transaction do
store[:bat] = 3
store.delete(:bat)
end
end
Возвращает nil, если такого ключа нет.
Вызывает исключение, если вызывается вне блока транзакции.
# File lib/pstore.rb, line 436
def fetch(key, default=PStore::Error)
in_transaction
unless @table.key? key
if default == PStore::Error
raise PStore::Error, format("undefined key `%s'", key)
else
return default
end
end
@table[key]
end Как [], но принимает значение по умолчанию для хранилища. Если key не существует:
-
Вызывает исключение, если
defaultравноPStore::Error. -
В противном случае возвращает значение
default:example_store do |store| store.transaction do store.fetch(:nope, nil) # => nil store.fetch(:nope) # Raises an exception. end end
Вызывает исключение, если вызывается вне блока транзакции.
# File lib/pstore.rb, line 509 def key?(key) in_transaction @table.key? key end
Возвращает true, если key существует, false в противном случае:
example_store do |store|
store.transaction do
store.key?(:foo) # => true
end
end
Вызывает исключение, если вызывается вне блока транзакции.
PStore#root? — псевдоним для PStore#key?.
# File lib/pstore.rb, line 492 def keys in_transaction @table.keys end
Возвращает массив существующих ключей:
example_store do |store|
store.transaction do
store.keys # => [:foo, :bar, :baz]
end
end
Вызывает исключение, если вызывается вне блока транзакции.
PStore#roots — псевдоним для PStore#keys.
# File lib/pstore.rb, line 519 def path @filename end
Возвращает строку пути к файлу, используемому для создания хранилища:
store.path # => "flat.store"
# File lib/pstore.rb, line 555
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
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 Открывает блок транзакции для хранилища. См. Транзакции.
Если аргумент read_only равен false, блок может как читать, так и записывать в хранилище.
Если аргумент read_only равен true, блок не может включать вызовы transaction, []= или delete.
Вызывает исключение, если вызывается внутри блока транзакции.
Методы приватного экземпляра
# File lib/pstore.rb, line 732 def empty_marshal_checksum EMPTY_MARSHAL_CHECKSUM end
# File lib/pstore.rb, line 729 def empty_marshal_data EMPTY_MARSHAL_DATA end
# File lib/pstore.rb, line 388 def in_transaction raise PStore::Error, "not in transaction" unless @lock.locked? end
Вызывает PStore::Error, если вызывающий код не находится в PStore#transaction.
# File lib/pstore.rb, line 395 def in_transaction_wr in_transaction raise PStore::Error, "in read-only transaction" if @rdonly end
Вызывает PStore::Error, если вызывающий код не находится в PStore#transaction или если код находится в режиме только чтения PStore#transaction.
# File lib/pstore.rb, line 643
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 = CHECKSUM_ALGO.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 Загрузка указанного файла PStore. Если read_only равно true, будет возвращён распакованный Hash. Если read_only равно false, будет возвращена тройка: распакованный Hash, контрольная сумма данных и размер данных.
# File lib/pstore.rb, line 671
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 # File lib/pstore.rb, line 618
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 Открытие указанного файла (в режиме только чтения или чтения-записи) и его блокировка для чтения или записи.
Будет возвращен открытый объект File. Если read_only равно true, и файл не существует, будет возвращено значение nil.
Все исключения передаются вверх.
# File lib/pstore.rb, line 679
def save_data(original_checksum, original_file_size, file)
new_data = dump(@table)
if new_data.bytesize != original_file_size || CHECKSUM_ALGO.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 # File lib/pstore.rb, line 694
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 # File lib/pstore.rb, line 710 def save_data_with_fast_strategy(data, file) file.rewind file.write(data) file.truncate(data.bytesize) end
Ruby Core © 1993–2022 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.