12#include <unordered_map>
16#include "../attributes.hpp"
18#include "../core/executor.hpp"
19#include "../core/model.hpp"
20#include "../core/registry.hpp"
21#include "action_log.hpp"
23namespace morph::journal {
49 SchemaMismatchError(std::string_view modelType, std::string_view actionType, std::string_view recorded,
50 std::string_view current)
51 : std::runtime_error{
"journal::replay: payload schema mismatch for " + std::string{modelType} +
"/" +
52 std::string{actionType} +
": entry was written with schema " + std::string{recorded} +
53 ", this build decodes it as " + std::string{current} +
54 " (register a migration for this pair, or replay with a build that matches)"} {}
64enum class UnstampedPayloadPolicy : std::uint8_t {
110 using Migration = std::function<std::string(std::string_view)>;
118 void add(std::string_view actionType, std::string_view fromSchema,
Migration migration) {
119 _migrations[Key{std::string{actionType}, std::string{fromSchema}}] = std::move(migration);
132 std::string_view fromSchema)
const MORPH_LIFETIMEBOUND {
133 auto iter = _migrations.find(::morph::model::detail::PairKeyView{actionType, fromSchema});
134 return iter == _migrations.end() ? nullptr : &iter->second;
138 void clear() { _migrations.clear(); }
142 [[nodiscard]] std::size_t
size() const noexcept {
return _migrations.size(); }
145 using Key = std::pair<std::string, std::string>;
151 std::unordered_map<Key, Migration, ::morph::model::detail::PairKeyHash, ::morph::model::detail::PairKeyEqual>
158[[nodiscard]]
inline PayloadMigrationRegistry& defaultPayloadMigrations() {
159 static PayloadMigrationRegistry registry;
174inline bool& tlsIsReplaying() {
175 thread_local bool tls =
false;
217[[nodiscard]]
inline bool isReplaying() noexcept {
return detail::tlsIsReplaying(); }
270inline std::unique_ptr<::morph::model::detail::IModelHolder> replay(
271 std::string_view modelTypeId,
const std::vector<LogEntry>& entries,
272 ::morph::model::detail::ModelRegistryFactory& registry = ::morph::model::detail::defaultRegistry(),
273 ::morph::model::detail::ActionDispatcher& dispatcher = ::morph::model::detail::defaultDispatcher(),
274 const PayloadMigrationRegistry& migrations = defaultPayloadMigrations(),
275 UnstampedPayloadPolicy unstamped = UnstampedPayloadPolicy::Replay) {
276 auto holder = registry.create(modelTypeId);
281 holder->attachActionLog(
nullptr, {});
283 for (
const auto& entry : entries) {
291 if (entry.outcome == Outcome::Failed) {
298 const std::string current = dispatcher.schemaFor(entry.modelType, entry.actionType);
299 std::string_view payload{entry.payload};
300 std::string migrated;
301 if (entry.schema.empty()) {
302 if (unstamped == UnstampedPayloadPolicy::Refuse) {
305 }
else if (!current.empty() && entry.schema != current) {
306 const auto* migration = migrations.find(entry.actionType, entry.schema);
307 if (migration ==
nullptr) {
310 migrated = (*migration)(entry.payload);
313 dispatcher.dispatch(entry.modelType, entry.actionType, *holder, payload);
347 : _owned{
owner, std::make_shared<State>()} {}
356 _owned.apply(
"SessionLog::append", [entry = std::move(entry)](State& state)
mutable {
357 entry.
seq = ++state.nextSeq;
358 state.all.push_back(std::move(entry));
369 [[nodiscard]] std::vector<LogEntry>
entries(std::string_view entityKey = {})
const override {
370 return detail::entriesFor(_owned.read(
"SessionLog::entries").all, entityKey);
406 std::unique_ptr<::morph::model::detail::IModelHolder>
undoLast(
407 std::string_view modelTypeId,
408 ::morph::model::detail::ModelRegistryFactory& registry = ::morph::model::detail::defaultRegistry(),
409 ::morph::model::detail::ActionDispatcher& dispatcher = ::morph::model::detail::defaultDispatcher()) {
410 State& state = _owned.read(
"SessionLog::undoLast");
411 if (!state.all.empty()) {
416 state.all.pop_back();
418 return replay(modelTypeId, state.all, registry, dispatcher);
444 ::morph::model::detail::defaultDispatcher()) {
445 State& state = _owned.read(
"SessionLog::checkpoint");
450 std::vector<LogEntry> pending;
451 uint64_t highestSeq = state.committedUpToSeq;
452 for (
const auto& entry : state.all) {
453 if (entry.seq > state.committedUpToSeq) {
454 pending.push_back(entry);
455 highestSeq = std::max(highestSeq, entry.seq);
458 if (pending.empty()) {
461 state.committedUpToSeq = highestSeq;
462 for (
auto& entry : coalesced(pending, dispatcher)) {
463 durableSink.
append(std::move(entry));
474 std::string entityKey)
const override {
475 return _owned.ask<std::vector<LogEntry>>(
476 "SessionLog::entries", replyExec,
477 [entityKey = std::move(entityKey)](State& state) {
return detail::entriesFor(state.all, entityKey); });
485 static std::vector<LogEntry> coalesced(
const std::vector<LogEntry>& pending,
486 ::morph::model::detail::ActionDispatcher& dispatcher) {
487 std::vector<LogEntry> out;
488 std::unordered_map<std::string, std::size_t> lastIndexForKey;
489 for (
const auto& entry : pending) {
490 if (!dispatcher.coalesce(entry.modelType, entry.actionType)) {
491 out.push_back(entry);
494 std::string key = entry.modelType +
'\0' + entry.entityKey +
'\0' + entry.actionType;
495 auto iter = lastIndexForKey.find(key);
496 if (iter == lastIndexForKey.end()) {
497 lastIndexForKey.emplace(std::move(key), out.size());
498 out.push_back(entry);
501 out[iter->second] = entry;
509 std::vector<LogEntry> all;
510 uint64_t committedUpToSeq{0};
514 ::morph::exec::detail::OwnedState<State> _owned;
Move-only handle representing the eventual result of an asynchronous operation.
Registry of payload migrations, keyed by (actionType, fromSchema).
const Migration * find(std::string_view actionType, std::string_view fromSchema) const
Looks up the migration for (actionType, fromSchema).
std::function< std::string(std::string_view)> Migration
Rewrites one recorded payload JSON into the current shape.
void add(std::string_view actionType, std::string_view fromSchema, Migration migration)
Registers migration for entries of actionType stamped with fromSchema. Replaces any migration already...
std::size_t size() const noexcept
Returns how many migrations are registered.
void clear()
Removes every registered migration.
::morph::async::Completion< std::vector< LogEntry > > askEntries(::morph::exec::IExecutor &replyExec, std::string entityKey) const override
Reads the history on the owner, delivered on replyExec.
std::vector< LogEntry > entries(std::string_view entityKey={}) const override
Returns the full history (or one entity's slice of it) in append order. On the owner.
::morph::exec::IExecutor * owner() const noexcept override
The executor given at construction.
std::unique_ptr<::morph::model::detail::IModelHolder > undoLast(std::string_view modelTypeId, ::morph::model::detail::ModelRegistryFactory ®istry=::morph::model::detail::defaultRegistry(), ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher())
Drops the most recently appended entry and replays everything that remains against a fresh model inst...
SessionLog(::morph::exec::IExecutor &owner)
Constructs an empty session log belonging to owner.
void checkpoint(IActionLog &durableSink, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher())
Coalesces entries appended since the last checkpoint and forwards the reduced set,...
void append(LogEntry entry) override
Appends entry, assigning a monotonically increasing seq, on the owner.
void flush() override
No-op — checkpoint() is this class's real commit point. Callable anywhere.
RAII helper that sets the thread-local replay flag for its scope, restoring the previous value on des...
~ScopedReplayFlag()
Restores the saved value.
ScopedReplayFlag()
Sets the thread-local replay flag to true, saving whatever value was there before.
OwnedState: a component's state, and the one executor it is touched on.
Abstract executor interface.
Interface for durable storage of executed-action entries.
virtual void flush()=0
Pushes any buffered entries to the durable backend. No-op for sinks with nothing to buffer (e....
virtual std::vector< LogEntry > entries(std::string_view entityKey={}) const =0
Returns recorded entries in append order.
virtual void append(LogEntry entry)=0
Appends entry. Implementations assign entry.seq.
One recorded execution of an action against a model instance.
uint64_t seq
Monotonic order assigned by the sink on append(). Callers pass 0.
Thrown by replay() when a recorded entry's payload fingerprint (LogEntry::schema) disagrees with the ...
SchemaMismatchError(std::string_view modelType, std::string_view actionType, std::string_view recorded, std::string_view current)
Builds the message naming the action and both fingerprints.