diff --git a/src/dodb/index.cr b/src/dodb/index.cr index 3842305..abf5eaf 100644 --- a/src/dodb/index.cr +++ b/src/dodb/index.cr @@ -3,6 +3,29 @@ require "file_utils" require "./exceptions.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) property name : 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) + # 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) Dir.mkdir_p indexing_directory end - def check!(key, value, old_value) + # :inherit: + def check!(key : String, value : V, old_value : V?) index_key = key_proc.call value return if index_key.is_a? NoIndex @@ -33,7 +60,8 @@ class DODB::Index(V) < DODB::Indexer(V) end end - def index(key, value) + # :inherit: + def index(key : String, value : V) index_key = key_proc.call value 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 end - def deindex(key, value) + # :inherit: + def deindex (key : String, value : V) index_key = key_proc.call value return if index_key.is_a? NoIndex @@ -56,23 +85,51 @@ class DODB::Index(V) < DODB::Indexer(V) end end - # Get the key (ex: 343) for an entry in the DB. - # Without caching, it translates to walk the file-system in `db/indices/by_#{name}/`. + # Gets the key (ex: 343) for an entry in the DB from an indexed value. + # + # Reads the link in `db/indices/by_#{name}/`. + # + # 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 get_key_on_fs index 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 @storage[get_key index] 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? get index rescue MissingEntry nil 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 @storage.request_lock @name, index 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 @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 + # 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 safe_get index, &block rescue MissingEntry @@ -151,6 +220,31 @@ class DODB::Index(V) < DODB::Indexer(V) 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) # This hash contains the relation between the index key and the data key. property data = Hash(String, Int32).new @@ -206,9 +300,16 @@ class DODB::CachedIndex(V) < DODB::Index(V) end end -# `DODB::RAMOnlyIndex` enables the flexibility of indexes without a file-system representation. -# Absolute efficiency, exactly as easy to use as the other index implementations. - +# Basic indexes for 1-to-1 relations. +# 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) def index(key, value) index_key = key_proc.call value diff --git a/src/dodb/indexer.cr b/src/dodb/indexer.cr index 2d821e3..c12103e 100644 --- a/src/dodb/indexer.cr +++ b/src/dodb/indexer.cr @@ -2,24 +2,36 @@ # the indexes (basic indexes, partitions, tags, etc.). 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) - # 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) - # 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?) # Name of the index, such as *id* or *color* for example. # This is an arbitrary value, mostly to create the index directory. + # + # NOTE: used for internal operations. abstract def name : String # Directory where the values will be written. + # + # NOTE: used for internal operations. abstract def indexing_directory : String - # Removes all the index entries. - # By default, removes the `#indexing_directory`. + # Removes all the index entries, removes the `#indexing_directory` by default. + # + # NOTE: used for internal operations. def nuke_index FileUtils.rm_rf indexing_directory end