Spec-Zone.ru › Ruby 3.3

класс 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.

  • Method 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
    
  • Method 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
END_OF_DOCUMENT_MARKER

Константы

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

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

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

END_OF_DOCUMENT_MARKER

Методы частного экземпляра

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 истинно, будет возвращён распакованный Hash. Если read_only ложно, будет возвращён кортеж из 3 элементов: распакованный 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

Открыть указанный файл filename (в режиме только для чтения или чтения/записи) и заблокировать его для чтения или записи.

Будет возвращён объект открытого File файла. Если read_only истинен, а файл не существует, то будет возвращено 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–2022 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

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