Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emit events when replaying external txns #417

Merged
merged 11 commits into from
Aug 18, 2023
1 change: 0 additions & 1 deletion iModelCore/iModelPlatform/DgnCore/DgnElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1593,7 +1593,6 @@ DgnElementPtr DgnElement::_CloneForImport(DgnDbStatus* inStat, DgnModelR destMod
params.m_classId.IsValid() ? "invalid create params" : "attempt to clone with unknown class",
(int) (params.m_classId.IsValid() ? DgnDbStatus::BadRequest : DgnDbStatus::WrongClass)
);
return nullptr;
}

DgnElementPtr cloneElem = GetElementHandler().Create(params);
Expand Down
71 changes: 38 additions & 33 deletions iModelCore/iModelPlatform/DgnCore/TxnManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1021,36 +1021,41 @@ TxnManager::ModelChanges::ModelChanges(TxnManager& mgr) : m_mgr(mgr)
// If the file's opened in read-only mode though, there can be no changes to track.
if (mgr.GetDgnDb().IsReadonly())
{
m_determinedStatus = true;
m_status = Status::Readonly;
m_determinedMode = true;
m_mode = Mode::Readonly;
}
}

/*---------------------------------------------------------------------------------**//**
* @bsimethod
+---------------+---------------+---------------+---------------+---------------+------*/
TxnManager::ModelChanges::Status TxnManager::ModelChanges::DetermineStatus()
TxnManager::ModelChanges::Mode TxnManager::ModelChanges::DetermineMode()
{
if (!m_determinedStatus)
if (!m_determinedMode)
{
m_determinedStatus = true;
m_determinedMode = true;
if (m_mgr.GetDgnDb().GetGeometricModelUpdateStatement().IsValid())
m_status = Status::Success;
{
m_mode = Mode::Full;
}
else
Disable();
{
m_mode = Mode::Legacy;
ClearAll();
}
}

return m_status;
return m_mode;
}

/*---------------------------------------------------------------------------------**//**
* @bsimethod
+---------------+---------------+---------------+---------------+---------------+------*/
TxnManager::ModelChanges::Status TxnManager::ModelChanges::SetTrackingGeometry(bool track)
TxnManager::ModelChanges::Mode TxnManager::ModelChanges::SetTrackingGeometry(bool track)
{
DetermineStatus();
if (IsDisabled() || track == m_trackGeometry)
return m_status;
auto mode = DetermineMode();
if (Mode::Full != mode || track == m_trackGeometry)
return m_mode;

m_trackGeometry = track;

Expand All @@ -1065,7 +1070,7 @@ TxnManager::ModelChanges::Status TxnManager::ModelChanges::SetTrackingGeometry(b
}
});

return m_status;
return m_mode;
}

/*---------------------------------------------------------------------------------**//**
Expand All @@ -1084,10 +1089,8 @@ void TxnManager::ModelChanges::InsertGeometryChange(DgnModelId modelId, DgnEleme
* @bsimethod
+---------------+---------------+---------------+---------------+---------------+------*/
void TxnManager::ModelChanges::AddGeometricElementChange(DgnModelId modelId, DgnElementId elementId, TxnTable::ChangeType type, bool fromCommit) {
if (IsDisabled())
return;

m_geometricModels.Insert(modelId, fromCommit);

if (IsTrackingGeometry()) {
InsertGeometryChange(modelId, elementId, type);
return;
Expand Down Expand Up @@ -1137,9 +1140,7 @@ void TxnManager::ClearModelChanges() {
+---------------+---------------+---------------+---------------+---------------+------*/
void TxnManager::ModelChanges::Process()
{
DetermineStatus();
if (IsDisabled())
return;
auto mode = DetermineMode();

// When we get a Change that deletes a geometric element, we don't have access to its model Id at that time - look it up now.
for (auto const& deleted : m_deletedGeometricElements)
Expand All @@ -1155,6 +1156,12 @@ void TxnManager::ModelChanges::Process()
if (m_models.empty() && m_geometricModels.empty())
return;

if (Mode::Full != mode)
{
Clear(true);
return;
}

SetandRestoreIndirectChanges _v(m_mgr);

// if there were any geometric changes, update the "GeometryGuid" and "LastMod" properties in the Model table.
Expand Down Expand Up @@ -1626,30 +1633,28 @@ DgnDbStatus TxnManager::ReverseAll() {
return ReverseActions(TxnRange(startId, GetCurrentTxnId()));
}

/**
* For readonly connections, new Txns may be added from other writeable connections while this session is active.
* Since we always hold a SQLite transaction (the DefaultTxn) open, this session will not see any of
* those changes unless/until we explicitly close-and-restart the DefaultTxn.
*
* This method is called when the DefaultTxn is restarted and new Txns are discovered. It "replays" each new Txn in
* this session so that notifications for the changed elements/models can be sent for this connection as if the
* changes were just made. It calls `ApplyTxnChanges` but relies on the fact that the iModel is open for read and
* none of the changes are actually applied (they were applied in the connection where they were made.)
* This action is performed for "side effects" only. Of course since the connection is readonly, that's implied.
*/
void TxnManager::ReplayExternalTxns(TxnId from) {
if (!m_initTableHandlers || !m_dgndb.IsReadonly())
return; // this method can only be called on a readonly connection with the TxnManager active


for (TxnId curr = QueryNextTxnId(from); curr.IsValid(); curr = QueryNextTxnId(curr))
ApplyTxnChanges(curr, TxnAction::Reinstate);
TxnId curr = QueryNextTxnId(from);
bool haveTxns = curr.IsValid();
if (haveTxns) {
CallJsTxnManager("_onReplayExternalTxns");
while (curr.IsValid()) {
ApplyTxnChanges(curr, TxnAction::Reinstate);
curr = QueryNextTxnId(curr);
}
}

m_curr = GetLastTxnId(); // this is where the other session ends
if (m_curr.GetValue() == 0)
m_curr = TxnId(SessionId(1),0);
else
m_curr.Increment();

if (haveTxns)
CallJsTxnManager("_onReplayedExternalTxns");
}

/*---------------------------------------------------------------------------------**/ /**
Expand Down
54 changes: 35 additions & 19 deletions iModelCore/iModelPlatform/PublicAPI/DgnPlatform/TxnManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,17 @@ struct TxnManager : BeSQLite::ChangeTracker {
//=======================================================================================
struct ModelChanges
{
enum class Status : uint8_t
{
Success, //!< Operation successful; tracking is supported.
Readonly, //!< Tracking unsupported because iModel was opened in read-only mode.
VersionTooOld, //!< Tracking unsupported because iModel's version of BisCore pre-dates version 1.0.11
// The mode in which we're operating. For writable iModels, this is determined lazily upon first request. We cannot determine it
// immediately upon iModel open because schemas may subsequently be updated to a version that supports Full mode
enum class Mode : uint8_t {
// The iModel is read-only. It can apply changes from other sources, but cannot make direct changes.
// When changes are applied, in-memory state like the range index will be updated and events will be generated.
Readonly,
// The iModel is writable, but the version of its BisCore schema lacks the GeometricModel.GeometryGuid property.
// SetTrackingGeometry will fail and we will not attempt to update the GeometryGuid property when model geometry changes.
Legacy,
// The iModel is writable. When the geometry of a model changes, we will update the model's GeometryGuid property.
Full,
};
private:
ChangedIds<DgnModelId> m_models; // the set of models that have changes for the current transaction
Expand All @@ -354,17 +360,11 @@ struct TxnManager : BeSQLite::ChangeTracker {
bmap<DgnElementId, DgnModelId> m_modelsForDeletedElements; // maps Id of a deleted element to its model Id
ChangedIds<DgnElementId> m_deletedGeometricElements; // Ids of deleted geometric elements
TxnManager& m_mgr;
bool m_determinedStatus = false;
Status m_status;
Mode m_mode = Mode::Legacy;
bool m_determinedMode = false;
bool m_trackGeometry = false; // true if we are currently tracking changes to geometric elements

bool IsDisabled() const { return m_determinedStatus && Status::Success != m_status; }
void Disable()
{
m_determinedStatus = true;
m_status = Status::VersionTooOld;
ClearAll();
}
bool IsReadonly() const { return m_determinedMode && m_mode == Mode::Readonly; }

void Clear(bool preserveGeometryChanges)
{
Expand All @@ -381,10 +381,10 @@ struct TxnManager : BeSQLite::ChangeTracker {
public:
explicit ModelChanges(TxnManager& mgr);

void AddModel(DgnModelId modelId, bool fromCommit) { if (!IsDisabled()) m_models.Insert(modelId, fromCommit); }
void AddModel(DgnModelId modelId, bool fromCommit) { m_models.Insert(modelId, fromCommit); }
void AddGeometricElementChange(DgnModelId modelId, DgnElementId elementId, TxnTable::ChangeType type, bool fromCommit);
void AddDeletedElement(DgnElementId elemId, DgnModelId modelId) { if (!IsDisabled()) m_modelsForDeletedElements.Insert(elemId, modelId); }
void AddDeletedGeometricElement(DgnElementId elemId, bool fromCommit) { if (!IsDisabled()) m_deletedGeometricElements.Insert(elemId, fromCommit); }
void AddDeletedElement(DgnElementId elemId, DgnModelId modelId) { m_modelsForDeletedElements.Insert(elemId, modelId); }
void AddDeletedGeometricElement(DgnElementId elemId, bool fromCommit) { m_deletedGeometricElements.Insert(elemId, fromCommit); }

void Process();
void Notify();
Expand All @@ -401,11 +401,11 @@ struct TxnManager : BeSQLite::ChangeTracker {

// Set whether geometry changes should be tracked. They cannot be tracked if the db is read-only or
// BisCore version < 1.0.11 because the GeometryGuid property was introduced in that version of the schema.
DGNPLATFORM_EXPORT Status SetTrackingGeometry(bool track);
DGNPLATFORM_EXPORT Mode SetTrackingGeometry(bool track);

// Return whether model changes can be tracked for the DgnDb, or why they can't.
// Don't call this if you intend to upgrade the schemas contained in the DgnDb, until after you do so.
DGNPLATFORM_EXPORT Status DetermineStatus();
DGNPLATFORM_EXPORT Mode DetermineMode();
};

// Set and restore indirect changes mode
Expand Down Expand Up @@ -594,6 +594,22 @@ struct TxnManager : BeSQLite::ChangeTracker {
TxnId GetSessionStartId() const {return TxnId(m_curr.GetSession(), 0);}

DGNPLATFORM_EXPORT TxnId GetLastTxnId();

/** For readonly connections, new Txns may be added from other writeable connections while this session is active.
* Since we always hold a SQLite transaction (the DefaultTxn) open, this session will not see any of
* those changes unless/until we explicitly close-and-restart the DefaultTxn.
*
* This method is called when the DefaultTxn is restarted and new Txns are discovered. It "replays" each new Txn in
* this session so that notifications for the changed elements/models can be sent for this connection as if the
* changes were just made. It calls `ApplyTxnChanges` but relies on the fact that the iModel is open for read and
* none of the changes are actually applied (they were applied in the connection where they were made.)
* This action is performed for "side effects" only. Of course since the connection is readonly, that's implied.
*
* The events emitted to TypeScript by this operation (like "onElementsChanged" and "onModelGeometryChanged") are preceded
* by an "onReplayExternalTxns" event and followed by an "onReplayedExternalTxns" event.
*
* Note: this doesn't handle undo/redo. It is not expected that the process modifying the briefcase will use them.
*/
DGNPLATFORM_EXPORT void ReplayExternalTxns(TxnId start);

//! Given a TxnId, query for TxnId of the immediately previous committed Txn, if any.
Expand Down
10 changes: 5 additions & 5 deletions iModelJsNodeAddon/IModelJsNative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1172,14 +1172,14 @@ struct NativeDgnDb : BeObjectWrap<NativeDgnDb>, SQLiteOps
auto& modelChanges = GetDgnDb().Txns().m_modelChanges;
if (enable != modelChanges.IsTrackingGeometry())
{
auto status = modelChanges.SetTrackingGeometry(enable);
auto mode = modelChanges.SetTrackingGeometry(enable);
auto readonly = false;
switch (status)
switch (mode)
{
case TxnManager::ModelChanges::Status::Readonly:
case TxnManager::ModelChanges::Mode::Readonly:
readonly = true;
// fall-through intentional.
case TxnManager::ModelChanges::Status::VersionTooOld:
case TxnManager::ModelChanges::Mode::Legacy:
return CreateBentleyReturnErrorObject(readonly ? DgnDbStatus::ReadOnly : DgnDbStatus::VersionTooOld);
}
}
Expand All @@ -1191,7 +1191,7 @@ struct NativeDgnDb : BeObjectWrap<NativeDgnDb>, SQLiteOps

{
RequireDbIsOpen(info);
return Napi::Boolean::New(Env(), TxnManager::ModelChanges::Status::Success == GetDgnDb().Txns().m_modelChanges.DetermineStatus());
return Napi::Boolean::New(Env(), TxnManager::ModelChanges::Mode::Full == GetDgnDb().Txns().m_modelChanges.DetermineMode());
}

Napi::Value QueryModelExtents(NapiInfoCR info)
Expand Down