Documentation, again.

This commit is contained in:
Philippe PITTOLI 2024-05-20 20:55:07 +02:00
parent 78d6db2cc4
commit 3c0e1b7608
2 changed files with 127 additions and 14 deletions

View File

@ -3,6 +3,29 @@ require "file_utils"
require "./exceptions.cr" require "./exceptions.cr"
require "./indexer.cr" require "./indexer.cr"
# Basic indexes for 1-to-1 relations.
# Uncached version.
#
# This index provides a file-system representation, enabling the administrators to
# select a value based on its index. The following example presents an index named "id"
# with some data indexed by an UUID attribute.
#
# ```plain
# storage
# ├── data
# │   ├── 0000000000
# │   ├── 0000000001
# │   └── 0000000002
# ├── indices
# │   └── by_id <- this is an example of index named "id"
# │   ├── 6e109b82-25de-4250-9c67-e7e8415ad5a7 -> ../../data/0000000000
# │   ├── 2080131b-97d7-4300-afa9-55b93cdfd124 -> ../../data/0000000001
# │   └── 8b4e83e3-ef95-40dc-a6e5-e6e697ce6323 -> ../../data/0000000002
# ```
#
# NOTE: no cache, thus considered as *slow* for creation, deletion **and retrieval**.
# NOTE: see `CachedIndex` for a cached version, faster for retrieval.
# NOTE: for fast operations without fs representation, see `RAMOnlyIndex`.
class DODB::Index(V) < DODB::Indexer(V) class DODB::Index(V) < DODB::Indexer(V)
property name : String property name : String
property key_proc : Proc(V, String | NoIndex) | Proc(V, String) property key_proc : Proc(V, String | NoIndex) | Proc(V, String)
@ -10,11 +33,15 @@ class DODB::Index(V) < DODB::Indexer(V)
@storage : DODB::Storage(V) @storage : DODB::Storage(V)
# To create an index from a database, use `DODB::Storage#new_index` to create
# a cached index, `DODB::Storage#new_uncached_index` for an uncached index or
# `DODB::Storage#new_RAM_index` for a RAM-only index.
def initialize(@storage, @storage_root, @name, @key_proc) def initialize(@storage, @storage_root, @name, @key_proc)
Dir.mkdir_p indexing_directory Dir.mkdir_p indexing_directory
end end
def check!(key, value, old_value) # :inherit:
def check!(key : String, value : V, old_value : V?)
index_key = key_proc.call value index_key = key_proc.call value
return if index_key.is_a? NoIndex return if index_key.is_a? NoIndex
@ -33,7 +60,8 @@ class DODB::Index(V) < DODB::Indexer(V)
end end
end end
def index(key, value) # :inherit:
def index(key : String, value : V)
index_key = key_proc.call value index_key = key_proc.call value
return if index_key.is_a? NoIndex return if index_key.is_a? NoIndex
@ -44,7 +72,8 @@ class DODB::Index(V) < DODB::Indexer(V)
::File.symlink get_data_symlink_index(key), symlink ::File.symlink get_data_symlink_index(key), symlink
end end
def deindex(key, value) # :inherit:
def deindex (key : String, value : V)
index_key = key_proc.call value index_key = key_proc.call value
return if index_key.is_a? NoIndex return if index_key.is_a? NoIndex
@ -56,23 +85,51 @@ class DODB::Index(V) < DODB::Indexer(V)
end end
end end
# Get the key (ex: 343) for an entry in the DB. # Gets the key (ex: 343) for an entry in the DB from an indexed value.
# Without caching, it translates to walk the file-system in `db/indices/by_#{name}/<index>`. #
# Reads the link in `db/indices/by_#{name}/<index>`.
#
# Useful for internal purposes, to retrieve a value use `#get`.
#
# ```
# internal_database_key_for_the_corvet = cars_by_name.get_key "Corvet"
# ```
#
# NOTE: used for internal operations.
def get_key(index : String) : Int32 def get_key(index : String) : Int32
get_key_on_fs index get_key_on_fs index
end end
# Gets data from an indexed value (throws an exception on a missing entry).
#
# ```
# corvet = cars_by_name.get "Corvet"
# ```
#
# WARNING: throws an exception if the value isn't found.
# NOTE: for a safe version, use `#get?`.
def get(index : String) : V def get(index : String) : V
@storage[get_key index] @storage[get_key index]
end end
# Gets data from an indexed value without throwing an exception on a missing entry.
#
# ```
# corvet = cars_by_name.get? "Corvet"
# ```
#
# NOTE: safe version of `#get`, returns a *nil* value in case of a missing entry instead of an exception.
def get?(index : String) : V? def get?(index : String) : V?
get index get index
rescue MissingEntry rescue MissingEntry
nil nil
end end
# FIXME: Unlock on exception. # Gets data from an indexed value (thread-safe via two file locks) and gives it to a provided block of code.
#
# WARNING: should be thread-safe only between other `#safe_get` and `#safe_get?` calls,
# index creations and deletions do not use the same locks!
# NOTE: on exception, releases all locks.
def safe_get(index : String) : Nil def safe_get(index : String) : Nil
@storage.request_lock @name, index @storage.request_lock @name, index
internal_key = get_key(index).to_s internal_key = get_key(index).to_s
@ -82,8 +139,20 @@ class DODB::Index(V) < DODB::Indexer(V)
@storage.release_lock internal_key @storage.release_lock internal_key
@storage.release_lock @name, index @storage.release_lock @name, index
rescue e
# On exception, returns the exception after releasing locks.
@storage.release_lock internal_key
@storage.release_lock @name, index
raise e
end end
# Same as `#safe_get` but doesn't throw an exception on a missing value
# (provided block of code receives a *nil* value).
#
# WARNING: should be thread-safe only between other `#safe_get` and `#safe_get?` calls,
# index creations and deletions do not use the same locks!
# NOTE: on exception, releases all locks.
def safe_get?(index : String, &block : Proc(V | Nil, Nil)) : Nil def safe_get?(index : String, &block : Proc(V | Nil, Nil)) : Nil
safe_get index, &block safe_get index, &block
rescue MissingEntry rescue MissingEntry
@ -151,6 +220,31 @@ class DODB::Index(V) < DODB::Indexer(V)
end end
end end
# Basic indexes for 1-to-1 relations.
# Cached version.
#
# The cache makes this index fast and since the index doesn't contain
# the full value but just an attribute and a key, memory usage is still reasonable.
#
# A few file-system operations are required on index creation and deletion,
# thus this version still is slow for both these operations.
#
# ```plain
# storage
# ├── data
# │   ├── 0000000000
# │   ├── 0000000001
# │   └── 0000000002
# ├── indices
# │   └── by_id <- this is an example of index named "id"
# │   ├── 6e109b82-25de-4250-9c67-e7e8415ad5a7 -> ../../data/0000000000
# │   ├── 2080131b-97d7-4300-afa9-55b93cdfd124 -> ../../data/0000000001
# │   └── 8b4e83e3-ef95-40dc-a6e5-e6e697ce6323 -> ../../data/0000000002
# ```
#
# NOTE: cached, reasonable amount of memory used since it's just an index.
# NOTE: fast for retrieval, slow for index creation and deletion (fs operations).
# NOTE: for fast operations without fs representation, see `RAMOnlyIndex`.
class DODB::CachedIndex(V) < DODB::Index(V) class DODB::CachedIndex(V) < DODB::Index(V)
# This hash contains the relation between the index key and the data key. # This hash contains the relation between the index key and the data key.
property data = Hash(String, Int32).new property data = Hash(String, Int32).new
@ -206,9 +300,16 @@ class DODB::CachedIndex(V) < DODB::Index(V)
end end
end end
# `DODB::RAMOnlyIndex` enables the flexibility of indexes without a file-system representation. # Basic indexes for 1-to-1 relations.
# Absolute efficiency, exactly as easy to use as the other index implementations. # RAM-only version, no file-system representation.
#
# Since there is no file-system operations, all the operations are fast.
# `DODB::RAMOnlyIndex` enables the flexibility of indexes without a file-system representation
# for absolute efficiency.
# Exactly as easy to use as the other index implementations.
#
# NOTE: reasonable amount of memory used since it's just an index.
# NOTE: fast for all operations, but no file-system representation.
class DODB::RAMOnlyIndex(V) < DODB::CachedIndex(V) class DODB::RAMOnlyIndex(V) < DODB::CachedIndex(V)
def index(key, value) def index(key, value)
index_key = key_proc.call value index_key = key_proc.call value

View File

@ -2,24 +2,36 @@
# the indexes (basic indexes, partitions, tags, etc.). # the indexes (basic indexes, partitions, tags, etc.).
abstract class DODB::Indexer(V) abstract class DODB::Indexer(V)
# Indexes a value. # Indexes a value, used for **internal operations**.
#
# NOTE: used for internal operations.
abstract def index (key : String, value : V) abstract def index (key : String, value : V)
# Removes the index of a value. # Removes the index of a value, used for **internal operations**.
#
# NOTE: used for internal operations.
abstract def deindex (key : String, value : V) abstract def deindex (key : String, value : V)
# Verifies whether a new value will create a collision with the index of currently stored value. # Verifies whether a new value will create a collision with the index of
# currently stored value, used for **internal operations**.
#
# NOTE: used for internal operations.
abstract def check! (key : String, value : V, old_value : V?) abstract def check! (key : String, value : V, old_value : V?)
# Name of the index, such as *id* or *color* for example. # Name of the index, such as *id* or *color* for example.
# This is an arbitrary value, mostly to create the index directory. # This is an arbitrary value, mostly to create the index directory.
#
# NOTE: used for internal operations.
abstract def name : String abstract def name : String
# Directory where the values will be written. # Directory where the values will be written.
#
# NOTE: used for internal operations.
abstract def indexing_directory : String abstract def indexing_directory : String
# Removes all the index entries. # Removes all the index entries, removes the `#indexing_directory` by default.
# By default, removes the `#indexing_directory`. #
# NOTE: used for internal operations.
def nuke_index def nuke_index
FileUtils.rm_rf indexing_directory FileUtils.rm_rf indexing_directory
end end