Spec-Zone.ru › Ruby 3.4

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

  • 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

Константы

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 истинно, будет возвращено распарсенное значение Hash. Если read_only ложно, будет возвращена тройка: распарсенное 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 истинно, и файл не существует, будет возвращено 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–2024 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

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