diskhash 0.0.1.2 → 0.0.2.1
raw patch · 6 files changed
+123/−39 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- ChangeLog +9/−0
- README.md +25/−13
- diskhash.cabal +5/−5
- haskell/Data/DiskHash.hs +54/−13
- src/diskhash.c +20/−5
- src/diskhash.h +10/−3
ChangeLog view
@@ -1,3 +1,12 @@+Version 0.0.2.1 2017-10-12 by luispedro+ * Export reserve() in Python interface++Version 0.0.2.0 2017-10-05 by luispedro+ * Python improvement: support generic types through struct+ * Do not crash when attempting to write to RO tables+ * More flexible option checking+ * Better error messages+ Version 0.0.1.2 2017-06-27 by luispedro * Fix cabal release: include C header files in extra-source-files
README.md view
@@ -4,21 +4,26 @@ [](https://opensource.org/licenses/MIT) -A simple disk-based hash table.+A simple disk-based hash table (i.e., persistent hash table). +It is a hashtable implemented on memory-mapped disk, so that it can be loaded+with a single `mmap()` system call and used in memory directly (being as fast+as an in-memory hashtable once it is loaded from disk).+ The code is in C, wrappers are provided for Python, Haskell, and C++. The-wrappers follow similar APIs with variations to accomodate the language+wrappers follow similar APIs with variations to accommodate the language specificity. They all use the same underlying code, so you can open a hashtable-created in C from Haskell, modify it within your Haskell code, and open the-result in Python (although Python's version currently only deals with integers,-stored as longs).+created in C from Haskell, modify it within your Haskell code, and later open+the result in Python (although Python's version currently only deals with+integers, stored as longs). Cross-language functionality will work best for very simple types so that you can control their binary representation (64-bit integers, for example). Reading does not touch the disk representation at all and, thus, can be done on-top of read-only files or using multiple threads. Writing or modifying values-is, however, not thread-safe.+top of read-only files or using multiple threads (and different processes will+share the memory: the operating system does that for you). Writing or modifying+values is, however, not thread-safe. ## Examples @@ -88,13 +93,14 @@ ### Python -Python's interface is more limited and only integers are supported as values in-the hash table (they are stored as 64-bit integers).+Python's interface is based on the [struct+module](https://docs.python.org/3/library/struct.html). For example, `'ll'`+refers to a pair of 64-bit ints (_longs_): ```python import diskhash-tb = diskhash.Str2int("testing.dht", 15)-tb.insert("key", 9)+tb = diskhash.StructHash("testing.dht", 15, 'll', 'rw')+tb.insert("key", 1, 2) print(tb.lookup("key")) ``` @@ -135,11 +141,12 @@ } ``` -## Statibility+## Stability This is _beta_ software. It is good enough that I am using it, but the API can change in the future with little warning. The binary format is versioned (the-magic string encodes its version, so changes can be detected).+magic string encodes its version, so changes can be detected and you will get+an error message in the future rather than some silent misbehaviour. [Automated unit testing](https://travis-ci.org/luispedro/diskhash) ensures that basic mistakes will not go uncaught.@@ -155,6 +162,11 @@ "deleted" in place and recompacting when the hash table size changes or with an explicit `dht_gc()` call. It may also be important to add functionality to shrink hashtables so as to not waste disk space.++- The algorithm is a rather naïve implementation of linear addression. It would+ not be hard to switch to [robin hood+ hashing](https://www.sebastiansylvan.com/post/robin-hood-hashing-should-be-your-default-hash-table-implementation/)+ and this may indeed happen in the near future. License: MIT
diskhash.cabal view
@@ -1,5 +1,5 @@ name: diskhash-version: 0.0.1.2+version: 0.0.2.1 synopsis: Disk-based hash table description: Disk-based hash table category: Data@@ -20,8 +20,8 @@ Include-dirs: src/ ghc-options: -Wall build-depends:- base > 4 && < 5,- bytestring+ base > 4.8 && < 5,+ bytestring == 0.10.* Test-Suite diskhashtest default-language: Haskell2010@@ -32,8 +32,8 @@ hs-source-dirs: haskell/ include-dirs: src/ build-depends:- base > 4 && < 5,- bytestring,+ base > 4.8 && < 5,+ bytestring == 0.10.*, directory, diskhash, HUnit,
haskell/Data/DiskHash.hs view
@@ -1,4 +1,20 @@ {-# LANGUAGE FlexibleContexts, ScopedTypeVariables #-}+{-|++Disk based hash table++The Haskell interface has two types, distinguishing between read-only and+read-write hash tables. Operations on the RW variant are in the IO monad, while+operations on RO tables are all pure (after the 'htOpenRO' call, naturally).+Using read-write hashtables with more than one thread is undefined behaviour,+but the read-only variant is perfectly thread safe.++All data structures are strict (naturally: they write to disk).++The Haskell API can be used to access diskhashes created from other languages+as long as the types are compatible.+-}+ module Data.DiskHash ( DiskHashRO , DiskHashRW@@ -25,17 +41,12 @@ import Foreign.C.Types (CInt(..), CSize(..)) import Foreign.C.String (CString, peekCString) --- | Disk based hash table------ The Haskell interface has two types, distinguishing between read-only and--- read-write hash tables. Operations on the RW variant are in the IO monad,--- while operations on RO tables are all pure (after opening the table,--- naturally).------ The datastructures are all strict.- type HashTable_t = ForeignPtr ()++-- | Represents a read-only diskhash storing type 'a' newtype DiskHashRO a = DiskHashRO HashTable_t++-- | Represents a read-write diskhash storing type 'a' newtype DiskHashRW a = DiskHashRW HashTable_t foreign import ccall "dht_open2" c_dht_open2:: CString -> CInt -> CInt -> CInt -> Ptr CString -> IO (Ptr ())@@ -58,12 +69,25 @@ m <- peekCString err' free err' return m+ -- | open a hash table in read-write mode-htOpenRW :: forall a. (Storable a) => FilePath -> Int -> IO (DiskHashRW a)+htOpenRW :: forall a. (Storable a) => FilePath+ -- ^ file path+ -> Int+ -- ^ maximum key size+ -> IO (DiskHashRW a) htOpenRW fpath maxk = DiskHashRW <$> open' (undefined :: a) fpath maxk 66 -- | open a hash table in read-only mode-htOpenRO :: forall a. (Storable a) => FilePath -> Int -> IO (DiskHashRO a)+--+-- The 'maxk' argument can be 0, in which case the value of the maximum key+-- will be taken from the disk file. If not zero, then it is checked against+-- the value on disk and an exception is raised if there is a mismatch.+htOpenRO :: forall a. (Storable a) => FilePath+ -- ^ file path+ -> Int+ -- ^ maximum key size+ -> IO (DiskHashRO a) htOpenRO fpath maxk = DiskHashRO <$> open' (undefined :: a) fpath maxk 0 open' :: forall a. (Storable a) => a -> FilePath -> Int -> CInt -> IO HashTable_t@@ -81,7 +105,11 @@ -- | Open a hash table in read-write mode and pass it to an action -- -- Once the action is is complete, the hashtable is closed (and sync'ed to disk).-withDiskHashRW :: (Storable a) => FilePath -> Int -> (DiskHashRW a -> IO b) -> IO b+withDiskHashRW :: (Storable a) => FilePath+ -- ^ file path+ -> Int+ -- ^ maximum key size+ -> (DiskHashRW a -> IO b) -> IO b withDiskHashRW fp s act = do ht@(DiskHashRW ht') <- htOpenRW fp s r <- act ht@@ -102,6 +130,10 @@ -- -- Returns whether an insertion took place (if an object with that key already -- exists, no insertion is made).+--+-- This operation can fail (throwing an exception) if space could not be+-- allocated. You can pre-allocate space using 'htReserve'.+-- htInsert :: (Storable a) => B.ByteString -- ^ key -> a@@ -128,7 +160,12 @@ errmsg <- getError err throwIO $ userError ("Unexpected return from dht_insert: " ++ errmsg) -- | Lookup by key-htLookupRW :: (Storable a) => B.ByteString -> DiskHashRW a -> IO (Maybe a)+--+-- This is in the IO Monad to ensure ordering of operations.+htLookupRW :: (Storable a) => B.ByteString+ -- ^ key+ -> DiskHashRW a+ -> IO (Maybe a) htLookupRW key (DiskHashRW ht) = withForeignPtr ht $ \ht' -> B.useAsCString key $ \key' -> do@@ -138,6 +175,8 @@ else Just <$> peek (castPtr r) -- | Lookup by key+--+-- This is a pure operation htLookupRO :: (Storable a) => B.ByteString -> DiskHashRO a -> Maybe a htLookupRO key (DiskHashRO ht) = unsafeDupablePerformIO (htLookupRW key (DiskHashRW ht)) @@ -155,6 +194,8 @@ return True -- | Reserve space in the hash table+--+-- Reserving space can ensure that any subsequent 'htInsert' calls will not fail. -- -- If the operation fails, an exception is raised htReserve :: (Storable a) => Int -> DiskHashRW a -> IO Int
src/diskhash.c view
@@ -15,6 +15,10 @@ #include "diskhash.h" #include "primes.h" +enum {+ HT_FLAG_CAN_WRITE = 1,+};+ typedef struct HashTableHeader { char magic[16]; HashTableOpts opts_;@@ -176,10 +180,12 @@ return NULL; } }+ rp->flags_ = 0; const int prot = (flags == O_RDONLY) ? PROT_READ : PROT_READ|PROT_WRITE;- rp->data_ = mmap(NULL, + if (prot & PROT_WRITE) rp->flags_ |= HT_FLAG_CAN_WRITE;+ rp->data_ = mmap(NULL, rp->datasize_, prot, MAP_SHARED,@@ -208,9 +214,9 @@ } dht_free(rp); return 0;- } else if (header_of(rp)->opts_.key_maxlen != opts.key_maxlen- || header_of(rp)->opts_.object_datalen != opts.object_datalen) {- if (err) { *err = strdup("Options mismatch."); }+ } else if ((header_of(rp)->opts_.key_maxlen != opts.key_maxlen && opts.key_maxlen != 0)+ || (header_of(rp)->opts_.object_datalen != opts.object_datalen && opts.object_datalen != 0)) {+ if (err) { *err = strdup("Options mismatch (diskhash table on disk was not created with the same options used to open it)."); } dht_free(rp); return 0; }@@ -250,6 +256,10 @@ } size_t dht_reserve(HashTable* ht, size_t cap, char** err) {+ if (!(ht->flags_ & HT_FLAG_CAN_WRITE)) {+ if (err) { *err = strdup("Hash table is read-only. Cannot call dht_reserve."); }+ return -EACCES;+ } if (header_of(ht)->cursize_ / 2 > cap) { return header_of(ht)->cursize_ / 2; }@@ -281,12 +291,13 @@ return 0; } temp_ht->datasize_ = total_size;- temp_ht->data_ = mmap(NULL, + temp_ht->data_ = mmap(NULL, temp_ht->datasize_, PROT_READ|PROT_WRITE, MAP_SHARED, temp_ht->fd_, 0);+ temp_ht->flags_ = ht->flags_; if (temp_ht->data_ == MAP_FAILED) { if (err) { const int errorbufsize = 512;@@ -358,6 +369,10 @@ } int dht_insert(HashTable* ht, const char* key, const void* data, char** err) {+ if (!(ht->flags_ & HT_FLAG_CAN_WRITE)) {+ if (err) { *err = strdup("Hash table is read-only. Cannot insert."); }+ return -EACCES;+ } if (strlen(key) >= header_of(ht)->opts_.key_maxlen) { if (err) { *err = strdup("Key is too long"); } return -EINVAL;
src/diskhash.h view
@@ -28,6 +28,7 @@ const char* fname_; void* data_; size_t datasize_;+ int flags_; } HashTable; @@ -59,8 +60,10 @@ * * When opening an existing disk table, you can pass `{ 0, 0 }` (the return * value of `dht_zero_opts()`) as the options, in which case the values will be- * taken from the table on disk. If you do pass values, they are checked- * against the values on disk and it is an error if there is a mismatch.+ * taken from the table on disk. If you do pass values > 0, they are checked+ * against the values on disk and it is an error if there is a mismatch+ * (passing zero to one of the option fields and not the other is supported:+ * only the non-zero field is checked). * * The last argument is an error output argument. If it is set to a non-NULL * value, then the memory must be released with free(). Passing NULL is valid@@ -105,7 +108,8 @@ * 0 if the key was already present in the table. The hash table was * not modified. * -EINVAL : key is too long- * -ENOMEM : dht_reserve failed.+ * -EACCES : attempted to insert into a read-only table.+ * -ENOMEM : dht_reserve failed. * * The last argument is an error output argument. If it is set to a non-NULL * value, then the memory must be released with free(). Passing NULL is valid@@ -133,6 +137,9 @@ * The last argument is an error output argument. If it is set to a non-NULL * value, then the memory must be released with free(). Passing NULL is valid * (and no error message will be produced).+ *+ * Attempting to call this function on a read-only table will fail (return+ * value: -EACCES). */ size_t dht_reserve(HashTable*, size_t capacity, char** err);