LUMIERA.clone/src/vault/mem/buffer-metadata.hpp
Ichthyostega abecd2cbde BufferProvider: implement locking and data synchronisation
...using the first usage situation as guideline,
and verify proper operation through unit-test

- the LocalBufferStage gets a set of thread-local BufferMetadata
- furthermore, it accesses the central hub via dependency-injection
- after each type registration ''from a worker thread'' data is published

As an asside: ensure most metadata registration can ''move'' the TypeHandler
2026-04-20 01:40:15 +02:00

963 lines
36 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
BUFFER-METADATA.hpp - internal metadata for data buffer providers
Copyright (C)
2011,2026 Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file buffer-metadata.hpp
** Metadata for managing and accessing buffers. The Lumiera Engine uses the
** Abstraction of an BufferProvider to handle various kinds of buffer organisation
** and access in a uniform way. Actually, buffers can be exposed and provided by several
** facilities, which might even be implemented through an external library. Thus the engine
** and the abstraction placed in between needs a common set of control data, to be able to
** expose the correct buffer for each request. Typically -- and independent of the actual
** implementation -- the following properties need to be tracked
** - that overall storage size available within the buffer
** - a pair of custom _creator_ and _destructor_ functions to use together with this buffer
** - an additional client key to distinguish otherwise otherwise identical client requests
** These three distinctions are applied in sequence, thus forming a type tree with 3 levels.
** Only the first distinguishing level (the size) is mandatory. The others are provided,
** because some of the foreseeable buffer providers allow to re-access the data placed
** into the buffer, by assigning an internally managed ID to the buffer. The most
** prominent example is the frame cache, which obviously needs to keep track of
** the buffers after the render engine is finished, while the engine code
** just accesses yet another buffer to place the results of calculations.
**
** These additional distinctions and properties are associated with the help of the
** [Buffer Descriptor](\ref BuffDescr), embedded into each BuffHandle. While the engine
** mostly uses these handles in the way of a pointer, the buffer descriptor acts as a
** configuration tag attached to the buffer access, allowing to re-access a context
** within the buffer provider implementation. Notably the Render Engine employs
** several BufferProvider services (Memory, Cache, Output) that share the metadata
** table, albeit with thread-local caches. For this reason, the registered types
** are anchored at a root-hash, allowing to keep those several clients apart.
**
** @todo 2/2026 After refactoring the BufferProvider and splitting up the implementation
** in two realms (state and storage), the purpose of this code her became much
** clearer. The problem remains however, that the BufferMetadata directly
** maintains its metadata in an embedded hashtable. This is problematic
** when operating within a massively concurrent environment.
** @todo 4/2026 After resolving the problems related to BufferProvidder and OutputSlot,
** the next task is to build a first draft implementation for a production-grade
** buffer allocator and manager. I decided to employ thread-local »satellite«
** tables, and a central EngineBufferMetadata hub. It remains to be seen
** if this design is sustainable.
**
** @warning BufferMetadata entails an intricate setup of chained hash keys, which relies
** on several implicit assumptions regarding consistency. These are largely unchecked
** (beyond the unit test) and guaranteed only through the call sequence in proper use.
** - there are Key entries that represent a _buffer type_ — these must not be mutated.
** - the hash value for these typeKeys is totally determined by their properties
** - a child entry can only vary _one_ of these properties, and the change should
** be applied in the fixed sequence as defined in BufferMetadata::key()
** - any violation to this scheme might lead to *duplicate entries* -> buffer corruption
** - active state entries should only added as leaves, and should be removed after use.
** - the hash of an active entry must reflect its associated memory address.
** - Warning: this is only ensured by invoking Key::forEntry, but not persisted in the
** Key itself. A directly computed hash of such a Key differs from the explicitly
** associated hashID_. If the associated buffer* is changed, the entry can not be
** found anymore, which might lead to double allocation and memory corruption.
**
** @see buffer-provider.hpp
** @see BufferMetadata_test
** @see BufferProviderProtocol_test
*/
#ifndef VAULT_MEM_BUFFR_METADATA_H
#define VAULT_MEM_BUFFR_METADATA_H
#include "lib/error.hpp"
#include "lib/hash-value.h"
#include "include/logging.h"
#include "vault/mem/buffhandle.hpp"
#include "vault/mem/type-handler.hpp"
#include "vault/mem/buffer-local-tag.hpp"
#include "lib/nocopy.hpp"
#include "lib/util.hpp"
#include <unordered_map>
#include <utility>
namespace vault{
namespace mem {
using std::move;
using std::forward;
using lib::HashVal;
using util::unConst;
namespace err = lumiera::error;
namespace metadata {
class Key;
class Entry;
}
class BufferMetadata;
/**
* Buffer states
* usable within BufferProvider
* and stored within the metadata
*/
enum BufferState
{ NIL, ///< abstract entry, not yet allocated
FREE, ///< allocated buffer, no longer in use
LOCKED, ///< allocated buffer actively in use
EMITTED, ///< allocated buffer, returned from client
BLOCKED ///< allocated buffer blocked by protocol failure
};
inline bool
isValidBufferTransition (BufferState oldState, BufferState newState)
{
return ( (oldState == FREE and newState == LOCKED)
or (oldState == LOCKED and newState == EMITTED)
or (oldState == LOCKED and newState == BLOCKED)
or (oldState == LOCKED and newState == FREE)
or (oldState == EMITTED and newState == BLOCKED)
or (oldState == EMITTED and newState == FREE)
or (oldState == BLOCKED and newState == FREE)
);
}
namespace { // internal constants to mark the default case
inline bool
nontrivial (TypeHandler const& toVerify)
{
return TypeHandler::RAW != toVerify;
}
inline bool
nontrivial (LocalTag const& toVerify)
{
return LocalTag::UNKNOWN != toVerify;
}
}
/* === Metadata Implementation === */
namespace metadata {
namespace { // details of hash calculation
template<typename VAL>
HashVal
chainedHash(HashVal accumulatedHash, VAL const& changedValue)
{
boost::hash_combine (accumulatedHash, changedValue);
return accumulatedHash;
}
}
/**
* Description of a Buffer-"type".
* Key elements will be used to generate hash IDs,
* to be embedded into a [Buffer Descriptor](\ref BuffDescr).
* Keys are chained hierarchically.
*/
class Key
{
HashVal parent_;
HashVal hashID_;
protected:
size_t storageSize_;
TypeHandler instanceFunc_;
LocalTag specifics_;
public:
/** Generic marker for an impossible / rejected type */
static const Key INVALID;
/** build a standard basic key describing a kind of Buffer.
* @param familyID basic hash seed value to distinguish
* families of buffer types managed by
* different BufferProvider instances
* @param storageSize fundamental info: buffer size
*/
Key (HashVal familyID, size_t storageSize)
: parent_(familyID)
, hashID_(chainedHash (familyID, storageSize))
, storageSize_(storageSize)
, instanceFunc_(TypeHandler::RAW)
, specifics_(LocalTag::UNKNOWN)
{ }
// standard copy operations permitted
/** create a derived buffer type description.
* Using a different storage size than the parent type,
* all else remaining the same
*/
Key (Key const& parent, size_t differingStorageSize)
: parent_(parent.hashID_)
, hashID_(chainedHash (parent_, differingStorageSize))
, storageSize_(differingStorageSize) // ◁────────────────────────────────┨ differing from the parent
, instanceFunc_(parent.instanceFunc_)
, specifics_(parent.specifics_)
{ }
/** create a derived buffer type description.
* Using different ctor and dtor functions,
* all else remaining the same as with parent
*/
Key (Key const& parent, TypeHandler differingTypeHandlerFunctions)
: parent_(parent.hashID_)
, hashID_(chainedHash (parent_, differingTypeHandlerFunctions))
, storageSize_(parent.storageSize_)
, instanceFunc_(move(differingTypeHandlerFunctions)) // ◁─────────────────┨ differing from the parent
, specifics_(parent.specifics_)
{ }
/** create a derived buffer type description.
* Using a different private ID than the parent type,
* all else remaining the same
*/
Key (Key const& parent, LocalTag anotherTypeSpecificInternalTag)
: parent_(parent.hashID_)
, hashID_(chainedHash (parent_, anotherTypeSpecificInternalTag))
, storageSize_(parent.storageSize_)
, instanceFunc_(parent.instanceFunc_)
, specifics_(anotherTypeSpecificInternalTag) // ◁───────────────────────┨ differing from the parent
{ }
/** build derived Key for a concrete buffer Entry
* @param parent type key to subsume this buffer
* @param bufferAddr pointer to the concrete buffer
* @return Child key with hashID based on the buffer address.
* For NULL buffer a copy of the parent is returned.
*/
static Key
forEntry (Key const& parent, const Buff* bufferAddr, LocalTag const& localTag =LocalTag::UNKNOWN)
{
Key newKey{parent}; // copy of parent as baseline
if (nontrivial(localTag))
{
if (nontrivial(parent.specifics_)
and localTag != parent.specifics_)
throw err::Logic{"Implementation defined local key should not be overridden. "
"Underlying buffer type already defines a nontrivial LocalTag"};
newKey.parent_ = HashVal(parent);
newKey.hashID_ = chainedHash(newKey.hashID_, localTag);
newKey.specifics_ = localTag;
}
if (bufferAddr)
{
newKey.parent_ = HashVal(parent);
newKey.hashID_ = chainedHash(newKey.hashID_, bufferAddr);
}
return newKey;
}
LocalTag const& localTag() const { return specifics_;}
size_t storageSize() const { return storageSize_; }
HashVal parentKey() const { return parent_;}
operator HashVal() const { return hashID_;}
friend bool
operator== (Key const& k1, Key const& k2)
{
return k1.hashID_ == k2.hashID_
and k1.parent_ == k2.parent_;
}
explicit
operator bool() const
{
return *this != INVALID;
}
};
/**
* A complete metadata Entry, based on a Key.
* This special Key element usually describes an actual Buffer.
* Entries are to be managed in a hashtable, which is "the metadata table".
* As a special case, an entry without a concrete buffer storage pointer
* can be created. This corresponds to a (plain) key and describes just
* a buffer type. Such type-only entries are fixed to the NIL state.
* All other entries allow for state transitions.
*
* The "metadata table" with its entries is maintained by an engine::BufferMetadata
* instance. For the latter, Entry serves as representation and access point
* to the individual metadata; this includes using the TypeHandler for
* building and destroying buffer structures.
*
* @remark 2/2026 the plan is to use this entry beyond metadata handling
* and especially to describe a buffer within a layered allocator
*/
class Entry
: public Key
{
BufferState state_;
Buff* buffer_;
protected:
Entry (Key const& parent
,Buff* bufferPtr =nullptr
,size_t actualSize =0
,LocalTag const& specialTag =LocalTag::UNKNOWN
)
: Key{Key::forEntry (parent, bufferPtr, specialTag)}
, state_{bufferPtr? LOCKED:NIL}
, buffer_{bufferPtr}
{
if (bufferPtr and actualSize)
Key::storageSize_ = actualSize;
ENSURE (Key::storageSize() >= parent.storageSize());
}
/// BufferMetadata is allowed to create
friend class vault::mem::BufferMetadata;
// standard copy operations permitted
public:
/** is this Entry currently associated to a
* concrete buffer? Is this buffer in use? */
bool
isLocked() const
{
ASSERT (!buffer_ or (NIL != state_ and FREE != state_));
return bool(buffer_);
}
/** is the client currently allowed to access the buffer memory?
* @remark implies #isLocked() but is more narrow */
bool
isAccessible() const
{
ENSURE (isLocked());
return bool(buffer_)
and LOCKED == state_;
}
/** is this Entry just an (abstract) placeholder for a type?
* @return false if it's a real entry corresponding to a concrete buffer
*/
bool
isTypeKey() const
{
return NIL == state_ and not buffer_;
}
BufferState
state() const
{
return state_;
}
Buff*
access()
{
__must_not_be_NIL();
__must_not_be_FREE();
ENSURE (buffer_);
return buffer_;
}
/** Buffer state machine */
Entry&
mark (BufferState newState)
{
__must_not_be_NIL();
if (isValidBufferTransition (state_, newState))
{ // perform transition
if (newState == FREE)
invokeEmbeddedDtor_and_clear();
if (newState == LOCKED)
invokeEmbeddedCtor();
state_ = newState;
return *this;
}
throw err::Fatal ("Invalid buffer state transition.");
}
Entry&
lock (Buff* newBuffer, size_t actualSize =0)
{
__must_be_FREE();
buffer_ = newBuffer;
storageSize_ = std::max (actualSize, storageSize_);
return mark (LOCKED);
}
Entry&
invalidate (bool invokeDtor =true)
{
if (buffer_ and invokeDtor)
invokeEmbeddedDtor_and_clear();
buffer_ = nullptr;
state_ = FREE;
return *this;
}
protected:
/** @internal maybe invoke a registered TypeHandler's
* constructor function, which typically builds some
* content object into the buffer by placement new. */
void
invokeEmbeddedCtor()
{
__buffer_required();
if (nontrivial (instanceFunc_))
instanceFunc_.createAttached (buffer_);
}
/** @internal maybe invoke a registered TypeHandler's
* destructor function, which typically clears up some
* content object living within the buffer */
void
invokeEmbeddedDtor_and_clear()
{
__buffer_required();
if (nontrivial (instanceFunc_))
instanceFunc_.destroyAttached (buffer_);
buffer_ = nullptr;
}
private:
void
__must_not_be_NIL() const
{
if (NIL == state_)
throw err::Fatal ("Buffer metadata entry with state==NIL encountered."
"State transition logic broken (programming error)"
, LERR_(LIFECYCLE));
}
void
__must_not_be_FREE() const
{
if (FREE == state_)
throw err::Logic ("Buffer is inaccessible (marked as free). "
"Need a new buffer pointer in order to lock an entry. "
"You should invoke markLocked(buffer) prior to access."
, LERR_(LIFECYCLE));
}
void
__must_be_FREE() const
{
if (FREE != state_)
throw err::Logic ("Buffer already in use"
, LERR_(LIFECYCLE));
REQUIRE (!buffer_, "Buffer marked as free, "
"but buffer pointer is set.");
}
void
__buffer_required() const
{
if (!buffer_)
throw err::Fatal ("Need concrete buffer for any further operations");
}
};
/**
* (Hash)Table to store and manage buffer metadata.
* Buffer metadata entries are comprised of a Key part and an extended
* Entry, holding the actual management and housekeeping metadata. The
* Keys are organised hierarchically and denote the "kind" of buffer.
* The hash values for lookup are based on the key part, chained with
* the actual memory location of the concrete buffer corresponding
* to the metadata entry to be retrieved.
*/
class Table
{
using MetadataStore = std::unordered_map<HashVal,Entry>;
MetadataStore entries_;
public:
~Table() { verify_all_buffers_freed(); }
size_t size() const { return entries_.size(); }
/** fetch metadata record, if any
* @param hashID for the Key part of the metadata entry
* @return pointer to the entry in the table or NULL
*/
Entry*
fetch (HashVal hashID)
{
MetadataStore::iterator pos = entries_.find (hashID);
if (pos != entries_.end())
return &(pos->second);
else
return NULL;
}
const Entry*
fetch (HashVal hashID) const
{
MetadataStore::const_iterator pos = entries_.find (hashID);
if (pos != entries_.end())
return &(pos->second);
else
return NULL;
}
/** store a copy of the given new metadata entry.
* The hash key for lookup is retrieved from the given Entry, by conversion to HashVal.
* Consequently, this will be the hashID of the parent Key (type), when the entry holds
* a NULL buffer (i.e a "pseudo entry"). Otherwise, it will be this parent Key hash,
* extended by hashing the actual buffer address.
* @return reference to relevant entry for this Key. This might be a copy
* of the new entry, or an already existing entry with the same Key
*/
Entry&
store (Entry const& newEntry)
{
REQUIRE (!fetch (newEntry), "duplicate buffer metadata entry");
MetadataStore::iterator pos = entries_.emplace (HashVal(newEntry), newEntry)
.first;
ENSURE (pos != entries_.end());
return pos->second;
}
void
remove (HashVal hashID)
{
uint cnt = entries_.erase (hashID);
ENSURE (cnt, "entry to remove didn't exist");
}
private:
void
verify_all_buffers_freed()
try
{
for (auto const& entry : entries_)
verify_is_free(entry);
}
ERROR_LOG_AND_IGNORE (engine,"Shutdown of BufferProvider metadata store")
static void
verify_is_free (std::pair<HashVal, Entry> const& e)
{
WARN_IF (e.second.isLocked(), engine,
"Buffer still in use while shutting down BufferProvider? ");
}
};
}//namespace metadata
/* ===== Buffer Metadata Frontend ===== */
/**
* Registry for managing buffer metadata.
* This is an implementation level service,
* used by the standard BufferProvider implementation.
* A metadata registry defines and maintains several families
* of "buffer types"; beyond the buffer storage size, the concrete
* meaning of those types is tied to the corresponding BufferProvider
* implementation and remains opaque. These types are represented as
* hierarchically linked hash keys. The implementation may bind a
* TypeHandler to a specific type, allowing automatic invocation
* of a "constructor" and "destructor" function on each buffer
* of this type, when _locking_ or _freeing_ the corresponding
* buffer. When creating or accessing a top-level (type)-Key,
* a hash-ID of the "family" must be provided; this is a
* root anchor and allows several clients to use a common
* metadata table
* @remark in the Render Engine there can be several
* BufferProvider services, while the BufferMetadata
* uses a common setup with thread-local caches.
* @note BufferMetadata is *not threadsafe*, thus the workers
* rely on thread-local instances, while the central
* EngineBufferMetadata is protected by a monitor.
*/
class BufferMetadata
: util::NonCopyable
{
metadata::Table table_;
public:
using Key = metadata::Key;
using Entry = metadata::Entry;
/** establish a metadata registry.
* Such will maintain families of chained buffer type entries
* and provide a service for storing and retrieving metadata
* for concrete buffer entries associated with these types.
*/
BufferMetadata()
: table_{}
{ }
/** combine the distinguishing properties
* into a single type key, which will be known/remembered
* from that point on. Properties are combined according to
* a fixed type specialisation order, with the buffer size
* forming the base level, possible TypeHandler functors the
* second level, and implementation defined LocalTag entries
* the third level. All these levels describe abstract type
* keys, not entries for concrete buffers. The latter are
* always created as children of a known type key.
* @param familyID root anchor to keep the mandators apart
*/
Key
key (HashVal familyID
,size_t storageSize
,TypeHandler instanceFunc =TypeHandler::RAW
,LocalTag specifics =LocalTag::UNKNOWN)
{
REQUIRE (storageSize);
Key typeKey = trackKey (familyID, storageSize);
if (nontrivial(instanceFunc))
typeKey = trackKey (typeKey, move(instanceFunc));
if (nontrivial(specifics))
typeKey = trackKey (typeKey, specifics);
return typeKey;
}
/** create a sub-type, using a different type/handler functor */
Key
key (Key const& parentKey, TypeHandler instanceFunc)
{
return trackKey (parentKey, move(instanceFunc));
}
/** create a sub-type,
* using a different private-ID (implementation defined) */
Key
key (Key const& parentKey, LocalTag specifics)
{
return trackKey (parentKey, specifics);
}
/** shortcut to access the Key part of a (probably new) Entry
* describing a concrete buffer at the given address
* @note might create/register a new Entry as a side-effect
*/
Key const&
key (Key const& parentKey, Buff* concreteBuffer, LocalTag const& specifics =LocalTag::UNKNOWN)
{
Key derivedKey = Key::forEntry (parentKey, concreteBuffer, specifics);
Entry* existing = table_.fetch (derivedKey);
return existing? *existing
: markLocked (parentKey,concreteBuffer,specifics);
}
/** access the metadata record registered with the given hash key.
* This might be a pseudo entry in case of a Key describing a buffer type.
* Otherwise, the entry associated with a concrete buffer pointer is returned
* by reference, an can be modified (e.g. state change)
* @param hashID which can be calculated from the Key
* @throw error::Invalid when there is no such entry
* @note use #isKnown to check existence
*/
Entry&
get (HashVal hashID)
{
Entry* entry = table_.fetch (hashID);
if (!entry)
throw err::Invalid ("Attempt to access an unknown buffer metadata entry");
return *entry;
}
Entry const&
get (HashVal hashID) const
{
return unConst(this)->get (hashID);
}
bool
isKnown (HashVal key) const
{
return bool(table_.fetch (key));
}
bool
isLocked (HashVal key) const
{
const Entry* entry = table_.fetch (key);
return entry
and entry->isLocked();
}
bool
isAccessible (HashVal key) const
{
const Entry* entry = table_.fetch (key);
return entry
and entry->isAccessible();
}
size_t
cntEntries() const
{
return table_.size();
}
/* == memory management operations == */
/** combine the type (Key) with a concrete buffer,
* thereby marking this buffer as locked. Store a concrete
* metadata Entry to account for this fact. This might include
* invoking a constructor function, in case the type (Key)
* defines a (nontrivial) TypeHandler.
* @param parentKey designates the _buffer type_ to base on
* @param buffer an actual memory address, which defines a distinct Entry
* @param size the size of the actual allocation. When size ≡ 0,
* the nominal size is assumed, as defined through parentKey.
* Otherwise, the actual size must be _larger_.
* @param specifics an optional qualification marker, which is managed
* and evaluated by the BufferStore implementation and treated as
* opaque data, as far as BufferMetadata is concerned. However,
* the LocalTag becomes part of the identity of the metadata record.
* Thus, either the parentKey must not include a LocalTag, or the
* LocalTag given here must be identical to what was associate
* with the _buffer type_ represented by parentKey.
* @throw error::Fatal when locking a NULL buffer
* @throw exceptions which might be raised by a TypeHandler's
* constructor function. In this case, the Entry remains
* created, but is marked as FREE
*/
Entry&
markLocked (Key const& parentKey
,Buff* buffer
,size_t actualSize =0
,LocalTag const& specifics =LocalTag::UNKNOWN)
{
if (not buffer)
throw err::Fatal{"Attempt to lock for a NULL buffer. Allocation floundered?"
, LERR_(BOTTOM_VALUE)};
if (not actualSize)
actualSize = parentKey.storageSize();
REQUIRE (parentKey.storageSize() <= actualSize);
return BufferMetadata::lock (parentKey, buffer, actualSize, specifics);
}
/** variation to use when a LocalTag is given, yet size is implicitly taken from parent */
Entry&
markLocked (Key const& parentKey, Buff* buffer, LocalTag const& specifics)
{
return markLocked (parentKey, buffer, size_t(0), specifics);
}
/** purge the bare metadata Entry from the metadata tables.
* @throw error::Logic if the entry isn't marked FREE already
*/
void
release (HashVal key)
{
Entry* entry = table_.fetch (key);
if (!entry) return;
ASSERT (entry and (key == HashVal(*entry)));
release (*entry);
}
void
release (Entry const& entry)
{
if (FREE != entry.state())
throw err::Logic{"Attempt to release a buffer still in use"
, LERR_(LIFECYCLE)};
table_.remove (HashVal(entry));
}
/** Synchronise metadata, including the key path up to root
* @param key start point of data exchange
* @param srcReg another BufferMetadata registry from which
* metadata shall be retrieved and imported
*/
void
import (HashVal key, BufferMetadata const& srcReg)
{
if (not srcReg.isKnown (key))
return;
Entry const& entry{srcReg.get(key)};
if (doSynchronise (entry))
import (entry.parentKey(), srcReg);
}
private:
/** Core low-level operation to access or create a buffer metadata entry.
* The hashID of the entry in question is built, based on the parentKey,
* which designates a buffer type, optionally an implementation defined
* LocalTag, together with the concrete buffer address and storage size.
* If yet unknown, a new concrete buffer metadata Entry is created and
* initialised immediately to LOCKED state. Otherwise just the matching
* Entry found in the metadata registry is fetched and locked.
* @note This function does the actual work to _activate_ the buffer.
* In case the type (Key) involves a TypeHandler (functor),
* its constructor function will be invoked, if actually a new
* entry gets created. Typically this mechanism will be used
* to placement-create an object into the buffer.
* @param parentKey a key describing the _type_ of the buffer
* @param concreteBuffer storage pointer, must not be NULL
* @param specifics an implementation defined tag
* @param onlyNew disallow fetching an existing entry
* @throw error::Logic when #onlyNew is set, but an equivalent entry
* was registered previously. This indicates a serious error
* in buffer lifecycle management.
* @throw error::Invalid when invoked with NULL buffer. Use the #key
* functions instead to register and track type keys.
* @return reference to the entry stored in the metadata table.
* @warning the exposed reference might become invalid when the
* buffer is released or re-used later.
*/
Entry&
lock (Key const& parentKey
,Buff* concreteBuffer
,size_t actualSize
,LocalTag const& specifics =LocalTag::UNKNOWN)
{
if (not concreteBuffer)
throw err::Invalid{"Attempt to lock a slot for a NULL buffer"
, LERR_(BOTTOM_VALUE)};
Entry newEntry{parentKey, concreteBuffer, actualSize, specifics};
Entry* existing = table_.fetch (newEntry);
if (existing and existing->isLocked())
throw err::Logic{"Attempt to re-lock a buffer still in use"
, LERR_(LIFECYCLE)};
if (not existing)
return store_as_locked (newEntry); // store new Entry marked as locked
else
return existing->lock (concreteBuffer, actualSize);
}
template<typename PAR, typename DEF>
Key
trackKey (PAR parent, DEF&& specialisation)
{
Key newKey{parent, forward<DEF>(specialisation)};
maybeStore (newKey);
return newKey;
}
void
maybeStore (Key const& key)
{
if (isKnown (key)) return;
table_.store (Entry{key, nullptr});
// stored a type marker entry with this key
ENSURE (HashVal(key) == HashVal(get(key)));
ENSURE (get(key).isTypeKey());
}
/** store a fully populated entry immediately starting with locked state
* @return a persisted copy that will be used to track the state henceforth.
* @remark the (optional) constructor function for a type embedded into the
* buffer is invoked when a _persistent_ entry transitions to _locked_ state;
* since a new buffer created with storage location is already marked as _locked,_
* for sake of consistency the embedded constructor must now be invoked; if this
* fails, the state has to be transitioned back to FREE before re-throwing.
*/
Entry&
store_as_locked (Entry const& metadata)
{
REQUIRE (metadata.isLocked());
Entry& newEntry = table_.store (metadata);
try
{
newEntry.invokeEmbeddedCtor();
ENSURE (LOCKED == newEntry.state());
ENSURE (newEntry.access());
}
catch(...)
{
newEntry.mark(FREE);
throw;
}
return newEntry;
}
/** import data from another metadata entry,
* yet reject violating sanity rules.
* @remark usually this is used to import new type-keys;
* an active buffer entry can only be handled if it is new,
* or implies a valid state transition.
* @note if the buffer is not NULL, it must be the same address,
* otherwise the keys would differ, since they are generated
* with \ref Key::forEntry()
* @return `true` if new data was stored, which implies
* that synchronisation should proceed up to the parent
*/
bool
doSynchronise (Entry const& otherEntry)
{
Entry* entry = table_.fetch (otherEntry);
if (not entry)
table_.store (otherEntry);
else
if (not otherEntry.isTypeKey()
and otherEntry.state() != entry->state())
{
ASSERT (not entry->isTypeKey(), "Metadata State-Engine corrupted");
if ( otherEntry.state() == FREE
or otherEntry.state() == LOCKED)
throw err::Logic ("Metadata synchronisation can not imply allocation/deallocation.");
else
entry->mark(otherEntry.state());
}
else
return false;
// something to synchronise => continue up the path
return true;
}
};
}} // namespace vault::mem
#endif /*VAULT_MEM_BUFFR_METADATA_H*/