Spec-Zone.ru › Ruby 3.2

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

Атрибуты

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 539
def abort
  in_transaction
  @abort = true
  throw :pstore_abort_transaction
end

Выходит из текущего блока транзакции, отбрасывая все изменения, указанные в блоке транзакции. См. Фиксация или откат.

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

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

Также алиас: root?
keys() Показать исходный код
# 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.

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

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

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

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

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

empty_marshal_checksum() Показать исходный код
# File lib/pstore.rb, line 732
def empty_marshal_checksum
  EMPTY_MARSHAL_CHECKSUM
end
empty_marshal_data() Показать исходный код
# File lib/pstore.rb, line 729
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 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, контрольная сумма данных и размер данных.

on_windows?() Показать исходный код
# 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
open_and_lock_file(filename, read_only) Показать исходный код
# 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.

Все исключения передаются вверх.

save_data(original_checksum, original_file_size, file) Показать исходный код
# 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
save_data_with_atomic_file_rename_strategy(data, file) Показать исходный код
# 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
save_data_with_fast_strategy(data, file) Показать исходный код
# 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.

Spec-Zone.ru

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