Spec-Zone.ru › Ruby 3.4

класс PStore

Родительский класс:
Object

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); это также может быть коллекция (например, массив, хеш, множество, диапазон и т. д.). Такая коллекция, в свою очередь, может содержать вложенные объекты, в том числе коллекции, на любую глубину; эти объекты тоже должны поддерживать Marshal. См. Иерархические значения.

Транзакции

Блок транзакции

Блок, переданный при вызове метода 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.

  • Метод commit применяет изменения к хранилищу и завершает блок:

    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
    
  • Метод abort отменяет изменения в хранилище и завершает блок:

    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.

Работа с хранилищем

Создание хранилища

Для создания хранилища используйте метод 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

Атрибуты

ultra_safe [RW]

Указывает, следует ли PStore прилагать все усилия для предотвращения повреждения файла, даже если произойдёт маловероятная ошибка (например, ошибка памяти или файловой системы):

  • true: изменения применяются путём создания временного файла, записи в него обновлённых данных и последующего переименования файла в указанный path. Целостность File сохраняется. Примечание: действует только в том случае, если файловая система поддерживает атомарное переименование файлов (как в POSIX-системах Linux, MacOS, FreeBSD и других).

  • false (по умолчанию): изменения применяются путём перемотки открытого файла в начало и записи обновлённых данных. Целостность File сохраняется, если файловая система не выдаёт непредвиденных ошибок ввода-вывода; если такая ошибка возникает во время записи в хранилище, файл может быть повреждён.

Открытые методы класса

new (file, thread_safe = false) Показать исходный код
# 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)

Открытые методы экземпляра

[] (key) Показать исходный код
# 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, если такого ключа нет.

См. также раздел Иерархические значения.

Вызывает исключение при вызове вне блока транзакции.

[]= (key, value) Показать исходный код
# 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

См. также раздел Иерархические значения.

Вызывает исключение при вызове вне блока транзакции.

abort () Показать исходный код
# File lib/pstore.rb, line 535
def abort
  in_transaction
  @abort = true
  throw :pstore_abort_transaction
end

Завершает текущий блок транзакции, отменяя все изменения, указанные в блоке транзакции.

Вызывает исключение при вызове вне блока транзакции.

commit () Показать исходный код
# File lib/pstore.rb, line 524
def commit
  in_transaction
  @abort = false
  throw :pstore_abort_transaction
end

Завершает текущий блок транзакции, применяя все изменения, указанные в блоке транзакции.

Вызывает исключение при вызове вне блока транзакции.

delete (key) Показать исходный код
# 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, если такого ключа нет.

Вызывает исключение при вызове вне блока транзакции.

fetch (key, default=PStore::Error) Показать исходный код
# 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
    

Вызывает исключение при вызове вне блока транзакции.

key? (key) Показать исходный код
# File lib/pstore.rb, line 505
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

Вызывает исключение при вызове вне блока транзакции.

Также имеет псевдоним: root?
keys () Показать исходный код
# File lib/pstore.rb, line 490
def keys
  in_transaction
  @table.keys
end

Возвращает массив существующих ключей:

example_store do |store|
  store.transaction do
    store.keys # => [:foo, :bar, :baz]
  end
end

Вызывает исключение при вызове вне блока транзакции.

Также имеет псевдоним: roots
path () Показать исходный код
# File lib/pstore.rb, line 515
def path
  @filename
end

Возвращает строку с путём к файлу, использованному для создания хранилища:

store.path # => "flat.store"
root? (key)
Псевдоним для: key?
roots ()
Псевдоним для: keys
transaction (read_only = false) { |pstore| ... } Показать исходный код
# File lib/pstore.rb, line 551
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.

Вызывает исключение при вызове внутри блока транзакции.

Закрытые методы экземпляра

empty_marshal_checksum () Показать исходный код
# File lib/pstore.rb, line 728
def empty_marshal_checksum
  EMPTY_MARSHAL_CHECKSUM
end
empty_marshal_data () Показать исходный код
# File lib/pstore.rb, line 725
def empty_marshal_data
  EMPTY_MARSHAL_DATA
end
in_transaction () Показать исходный код
# File lib/pstore.rb, line 388
def in_transaction
  raise PStore::Error, "not in transaction" unless @lock.locked?
end

Вызывает PStore::Error, если вызывающий код не находится внутри PStore#transaction.

in_transaction_wr () Показать исходный код
# 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 только для чтения.

load_data (file, read_only) Показать исходный код
# File lib/pstore.rb, line 639
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, контрольная сумма данных и размер данных.

on_windows? () Показать исходный код
# File lib/pstore.rb, line 667
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) Показать исходный код
# File lib/pstore.rb, line 614
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.

Все исключения передаются вызывающему коду.

save_data (original_checksum, original_file_size, file) Показать исходный код
# File lib/pstore.rb, line 675
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
save_data_with_atomic_file_rename_strategy (data, file) Показать исходный код
# File lib/pstore.rb, line 690
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) Показать исходный код
# File lib/pstore.rb, line 706
def save_data_with_fast_strategy(data, file)
  file.rewind
  file.write(data)
  file.truncate(data.bytesize)
end

Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API