Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ public async Task ClearAllAsync(CancellationToken cancellationToken)

foreach (var node in nodes)
{
session.Delete(node);
// GH-3986: delete by document id rather than by entity. LoadAllNodesAsync opens and
// disposes its own session, so these WolverineNode instances are tracked by a session
// that is already gone, and RavenDB's Delete<T>(T entity) overload throws
// "... is not associated with the session, cannot delete unknown entity instance".
// Deleting by id matches what DeleteAsync(Guid, int) already does for a single node,
// and what the AgentAssignment deletes below have always done.
session.Delete(node.NodeId.ToString());
foreach (var agent in node.ActiveAgents)
{
session.Delete(AgentAssignment.ToId(agent));
Expand Down
14 changes: 12 additions & 2 deletions src/Persistence/Wolverine.Sqlite/SqliteNodePersistence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@ public SqliteNodePersistence(DatabaseSettings settings, SqliteMessageStore datab
public async Task ClearAllAsync(CancellationToken cancellationToken)
{
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
await conn.CreateCommand($"delete from {_nodeTable}")

// GH-3986: the assignment rows have to go explicitly. PostgreSQL and Sql Server get this from
// the node_id foreign key's ON DELETE CASCADE, but the SQLite assignment table has no foreign
// key -- SQLite would not enforce one anyway without PRAGMA foreign_keys=ON per connection.
// Left behind, the rows are invisible to LoadAllNodesAsync (it only attaches an assignment to a
// node id it actually loaded) right up until a node re-registers under the same id, which is
// exactly the GH-3604 ejection/re-registration path -- and then it comes back owning agents it
// was never reassigned.
await conn.CreateCommand($"delete from {_assignmentTable};delete from {_nodeTable};")
.ExecuteNonQueryAsync(cancellationToken);
}

Expand Down Expand Up @@ -86,8 +94,10 @@ public async Task DeleteAsync(Guid nodeId, int assignedNodeNumber)
}

await using var conn = await _dataSource.OpenConnectionAsync(CancellationToken.None).ConfigureAwait(false);

// GH-3986: same missing cascade as ClearAllAsync -- delete this node's assignments by hand.
await conn.CreateCommand(
$"delete from {_nodeTable} where id = @id;update {IncomingTable} set {OwnerId} = 0 where {OwnerId} = @number;update {OutgoingTable} set {OwnerId} = 0 where {OwnerId} = @number;")
$"delete from {_assignmentTable} where {NodeId} = @id;delete from {_nodeTable} where id = @id;update {IncomingTable} set {OwnerId} = 0 where {OwnerId} = @number;update {OutgoingTable} set {OwnerId} = 0 where {OwnerId} = @number;")
.With("id", nodeId.ToString())
.With("number", assignedNodeNumber)
.ExecuteNonQueryAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,37 @@ public async Task reregister_after_ejection_preserves_identity_and_restores_assi
resurrected.ActiveAgents.OrderBy(x => x.ToString()).ShouldBe([agent2, agent1]);
}

}
[Fact]
public async Task clear_all_wipes_node_records_and_their_assignments()
{
// GH-3986. ClearAllAsync() is what a Solo-mode start calls to sweep the node records a previous
// Balanced-mode run left behind, so it has to work when the rows it deletes were written by an
// earlier *process*, not by this one. RavenDB's implementation deleted by tracked entity, which
// only works if the same session loaded it, and threw InvalidOperationException on every stale
// node. Two nodes, so a provider that only clears the first one is caught as well.
var first = createNode();
var second = createNode();

first.AssignedNodeNumber = await _database.Nodes.PersistAsync(first, CancellationToken.None);
second.AssignedNodeNumber = await _database.Nodes.PersistAsync(second, CancellationToken.None);

var agent1 = new Uri("red://leader");
var agent2 = new Uri("red://five");
var agent3 = new Uri("blue://leader");

await _database.Nodes.AssignAgentsAsync(first.NodeId, new[] { agent1, agent2 }, CancellationToken.None);
await _database.Nodes.AssignAgentsAsync(second.NodeId, new[] { agent3 }, CancellationToken.None);

await _database.Nodes.ClearAllAsync(CancellationToken.None);

(await _database.Nodes.LoadAllNodesAsync(CancellationToken.None)).ShouldBeEmpty();

// The assignments have to go too, not just the node rows. Re-registering the same node id is the
// provider-agnostic way to see them: if the assignment records survived, they reattach here.
await _database.Nodes.ReregisterNodeAsync(first, CancellationToken.None);

var reregistered = (await _database.Nodes.LoadAllNodesAsync(CancellationToken.None)).Single();
reregistered.NodeId.ShouldBe(first.NodeId);
reregistered.ActiveAgents.ShouldBeEmpty();
}
}
Loading