Test Domain Store mutations without a duplicate policy engine
This commit is contained in:
parent
897578e7cb
commit
56b54163ae
2 changed files with 47 additions and 261 deletions
|
|
@ -434,19 +434,6 @@ impl HostCoordinatorDomainStore {
|
||||||
write.commit()?;
|
write.commit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn apply_reference(&self, mutations: &[DomainStoreMutation]) -> Result<(), DomainStoreError> {
|
|
||||||
if mutations.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let mut snapshot = self.snapshot()?;
|
|
||||||
for mutation in mutations {
|
|
||||||
apply_mutation(&mut snapshot, mutation)?;
|
|
||||||
}
|
|
||||||
validate_snapshot(&snapshot)?;
|
|
||||||
self.write_snapshot(&snapshot)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_generations(
|
fn read_generations(
|
||||||
|
|
@ -556,153 +543,6 @@ fn read_mailboxes(
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn apply_mutation(
|
|
||||||
snapshot: &mut DomainStore,
|
|
||||||
mutation: &DomainStoreMutation,
|
|
||||||
) -> Result<(), DomainStoreError> {
|
|
||||||
match mutation {
|
|
||||||
DomainStoreMutation::PutGeneration(next) => put_generation(snapshot, next.clone()),
|
|
||||||
DomainStoreMutation::PutParticipant(next) => put_participant(snapshot, next.clone(), false),
|
|
||||||
DomainStoreMutation::CleanParticipantAfterAbsence(next) => {
|
|
||||||
put_participant(snapshot, next.clone(), true)
|
|
||||||
}
|
|
||||||
DomainStoreMutation::PutSession(next) => put_session(snapshot, next.clone()),
|
|
||||||
DomainStoreMutation::PutMailbox(next) => put_mailbox(snapshot, next.clone(), false),
|
|
||||||
DomainStoreMutation::CleanMailboxAfterAbsence(next) => {
|
|
||||||
put_mailbox(snapshot, next.clone(), true)
|
|
||||||
}
|
|
||||||
DomainStoreMutation::PutSessionTerminal(next) => {
|
|
||||||
put_session_terminal(snapshot, next.clone(), false)
|
|
||||||
}
|
|
||||||
DomainStoreMutation::CleanSessionTerminalAfterAbsence(next) => {
|
|
||||||
put_session_terminal(snapshot, next.clone(), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn put_generation(snapshot: &mut DomainStore, next: Generation) -> Result<(), DomainStoreError> {
|
|
||||||
if next.domain_id != snapshot.domain.domain_id {
|
|
||||||
return Err(DomainStoreError::DomainMismatch);
|
|
||||||
}
|
|
||||||
if let Some(current) = snapshot
|
|
||||||
.generations
|
|
||||||
.iter_mut()
|
|
||||||
.find(|record| record.generation_id == next.generation_id)
|
|
||||||
{
|
|
||||||
validate_generation_transition(current, &next)?;
|
|
||||||
*current = next;
|
|
||||||
} else {
|
|
||||||
if next.durable_decision != DurableDecision::None {
|
|
||||||
return Err(DomainStoreError::InvalidRow("initial Generation"));
|
|
||||||
}
|
|
||||||
snapshot.generations.push(next);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn put_participant(
|
|
||||||
snapshot: &mut DomainStore,
|
|
||||||
next: Participant,
|
|
||||||
absence_proven: bool,
|
|
||||||
) -> Result<(), DomainStoreError> {
|
|
||||||
require_generation(snapshot, next.generation_id())?;
|
|
||||||
if let Some(current) = snapshot.participants.iter_mut().find(|record| {
|
|
||||||
record.generation_id() == next.generation_id() && record.role() == next.role()
|
|
||||||
}) {
|
|
||||||
if absence_proven {
|
|
||||||
current.validate_transition_to_clean_after_absence(&next)?;
|
|
||||||
} else {
|
|
||||||
current.validate_transition_to(&next)?;
|
|
||||||
}
|
|
||||||
*current = next;
|
|
||||||
} else {
|
|
||||||
if absence_proven || next.state() != ParticipantState::Intent || next.ownership().is_some()
|
|
||||||
{
|
|
||||||
return Err(DomainStoreError::InvalidRow("initial Participant"));
|
|
||||||
}
|
|
||||||
snapshot.participants.push(next);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn put_session(snapshot: &mut DomainStore, next: Session) -> Result<(), DomainStoreError> {
|
|
||||||
require_generation(snapshot, next.generation_id)?;
|
|
||||||
if let Some(current) = snapshot.sessions.iter_mut().find(|record| {
|
|
||||||
record.generation_id == next.generation_id && record.session_id == next.session_id
|
|
||||||
}) {
|
|
||||||
validate_session_transition(current, &next)?;
|
|
||||||
*current = next;
|
|
||||||
} else {
|
|
||||||
if !matches!(
|
|
||||||
next.session_phase,
|
|
||||||
SessionPhase::New | SessionPhase::Waiting | SessionPhase::Registering
|
|
||||||
) {
|
|
||||||
return Err(DomainStoreError::InvalidRow("initial Session"));
|
|
||||||
}
|
|
||||||
snapshot.sessions.push(next);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn put_mailbox(
|
|
||||||
snapshot: &mut DomainStore,
|
|
||||||
next: Mailbox,
|
|
||||||
absence_proven: bool,
|
|
||||||
) -> Result<(), DomainStoreError> {
|
|
||||||
require_generation(snapshot, next.generation_id)?;
|
|
||||||
if let Some(current) = snapshot
|
|
||||||
.mailboxes
|
|
||||||
.iter_mut()
|
|
||||||
.find(|record| record.generation_id == next.generation_id)
|
|
||||||
{
|
|
||||||
validate_mailbox_transition(current, &next, absence_proven)?;
|
|
||||||
*current = next;
|
|
||||||
} else {
|
|
||||||
if absence_proven || next.state != ParticipantState::Intent {
|
|
||||||
return Err(DomainStoreError::InvalidRow("initial Mailbox"));
|
|
||||||
}
|
|
||||||
snapshot.mailboxes.push(next);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn put_session_terminal(
|
|
||||||
snapshot: &mut DomainStore,
|
|
||||||
next: SessionTerminal,
|
|
||||||
absence_proven: bool,
|
|
||||||
) -> Result<(), DomainStoreError> {
|
|
||||||
require_session(snapshot, next.generation_id(), next.session_id())?;
|
|
||||||
if let Some(current) = snapshot.session_terminals.iter_mut().find(|record| {
|
|
||||||
record.generation_id() == next.generation_id() && record.session_id() == next.session_id()
|
|
||||||
}) {
|
|
||||||
if absence_proven {
|
|
||||||
current.validate_transition_to_clean_after_absence(&next)?;
|
|
||||||
} else {
|
|
||||||
current.validate_transition_to(&next)?;
|
|
||||||
}
|
|
||||||
*current = next;
|
|
||||||
} else {
|
|
||||||
if absence_proven
|
|
||||||
|| next.state() != SessionTerminalState::Intent
|
|
||||||
|| next.container_id().is_some()
|
|
||||||
|| !next.host_coordinator().is_unassigned()
|
|
||||||
|| next.pty_master().is_some()
|
|
||||||
|| !next.exec_client().is_unassigned()
|
|
||||||
|| next.guest_process().is_some()
|
|
||||||
{
|
|
||||||
return Err(DomainStoreError::InvalidRow("initial Session Terminal"));
|
|
||||||
}
|
|
||||||
snapshot.session_terminals.push(next);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_generation_transition(
|
fn validate_generation_transition(
|
||||||
current: &Generation,
|
current: &Generation,
|
||||||
next: &Generation,
|
next: &Generation,
|
||||||
|
|
|
||||||
|
|
@ -133,23 +133,6 @@ fn publication_partial() -> Participant {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn publication_clean() -> Participant {
|
|
||||||
Participant::snapshot(
|
|
||||||
GenerationId::new(7),
|
|
||||||
ParticipantRole::Publication,
|
|
||||||
ParticipantState::Clean,
|
|
||||||
Some(ParticipantOwnership::Publication(PublicationOwnership {
|
|
||||||
staged_socket_path: None,
|
|
||||||
staged_identity_path: None,
|
|
||||||
socket_path: None,
|
|
||||||
identity_path: None,
|
|
||||||
socket_identity: None,
|
|
||||||
identity_file_identity: None,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lock_file(path: &Path) -> File {
|
fn lock_file(path: &Path) -> File {
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
|
|
@ -165,99 +148,62 @@ fn open_store(directory: &TestDirectory) -> HostCoordinatorDomainStore {
|
||||||
HostCoordinatorDomainStore::open(lock, &directory.path("domain.redb"), &domain()).unwrap()
|
HostCoordinatorDomainStore::open(lock, &directory.path("domain.redb"), &domain()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_differential_step(
|
|
||||||
row_store: &HostCoordinatorDomainStore,
|
|
||||||
reference_store: &HostCoordinatorDomainStore,
|
|
||||||
mutations: &[DomainStoreMutation],
|
|
||||||
) {
|
|
||||||
let row_result = row_store.apply(mutations);
|
|
||||||
let reference_result = reference_store.apply_reference(mutations);
|
|
||||||
match (&row_result, &reference_result) {
|
|
||||||
(Ok(()), Ok(())) => {}
|
|
||||||
(Err(row_error), Err(reference_error)) => {
|
|
||||||
assert_eq!(format!("{row_error:?}"), format!("{reference_error:?}"));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
panic!("row-local and reference results differ: {row_result:?} != {reference_result:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
row_store.snapshot().unwrap(),
|
|
||||||
reference_store.snapshot().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn row_local_mutations_match_the_snapshot_reference_engine() {
|
fn row_local_mutations_produce_explicit_snapshots_and_atomic_rollback() {
|
||||||
let row_directory = TestDirectory::new("differential-row");
|
let directory = TestDirectory::new("row-local-snapshots");
|
||||||
let reference_directory = TestDirectory::new("differential-reference");
|
let store = open_store(&directory);
|
||||||
let row_store = open_store(&row_directory);
|
store
|
||||||
let reference_store = open_store(&reference_directory);
|
.apply(&[
|
||||||
|
|
||||||
let mut foreign_generation = generation(DurableDecision::None);
|
|
||||||
foreign_generation.domain_id = DomainId::new(bytes(99));
|
|
||||||
let steps = vec![
|
|
||||||
vec![DomainStoreMutation::PutParticipant(publication_intent())],
|
|
||||||
vec![DomainStoreMutation::PutGeneration(foreign_generation)],
|
|
||||||
vec![
|
|
||||||
DomainStoreMutation::PutGeneration(generation(DurableDecision::None)),
|
DomainStoreMutation::PutGeneration(generation(DurableDecision::None)),
|
||||||
DomainStoreMutation::PutParticipant(publication_intent()),
|
DomainStoreMutation::PutParticipant(publication_intent()),
|
||||||
DomainStoreMutation::PutSession(session(SessionPhase::New)),
|
DomainStoreMutation::PutSession(session(SessionPhase::New)),
|
||||||
DomainStoreMutation::PutMailbox(mailbox(ParticipantState::Intent)),
|
DomainStoreMutation::PutMailbox(mailbox(ParticipantState::Intent)),
|
||||||
DomainStoreMutation::PutSessionTerminal(bare_terminal(SessionTerminalState::Intent)),
|
DomainStoreMutation::PutSessionTerminal(bare_terminal(SessionTerminalState::Intent)),
|
||||||
],
|
])
|
||||||
vec![DomainStoreMutation::PutGeneration(generation(
|
.unwrap();
|
||||||
DurableDecision::Preparing,
|
assert_eq!(
|
||||||
))],
|
store.snapshot().unwrap(),
|
||||||
vec![DomainStoreMutation::PutGeneration(generation(
|
DomainStore {
|
||||||
DurableDecision::None,
|
metadata: DomainStoreMetadata::v1(),
|
||||||
))],
|
domain: domain(),
|
||||||
vec![DomainStoreMutation::PutSession(session(
|
generations: vec![generation(DurableDecision::None)],
|
||||||
SessionPhase::Waiting,
|
participants: vec![publication_intent()],
|
||||||
))],
|
sessions: vec![session(SessionPhase::New)],
|
||||||
vec![DomainStoreMutation::PutSession(session(
|
session_terminals: vec![bare_terminal(SessionTerminalState::Intent)],
|
||||||
SessionPhase::Attached,
|
mailboxes: vec![mailbox(ParticipantState::Intent)],
|
||||||
))],
|
|
||||||
vec![DomainStoreMutation::PutParticipant(publication_partial())],
|
|
||||||
vec![DomainStoreMutation::PutParticipant(publication_clean())],
|
|
||||||
vec![DomainStoreMutation::CleanParticipantAfterAbsence(
|
|
||||||
publication_clean(),
|
|
||||||
)],
|
|
||||||
vec![DomainStoreMutation::PutMailbox(mailbox(
|
|
||||||
ParticipantState::Clean,
|
|
||||||
))],
|
|
||||||
vec![DomainStoreMutation::CleanMailboxAfterAbsence(mailbox(
|
|
||||||
ParticipantState::Clean,
|
|
||||||
))],
|
|
||||||
vec![DomainStoreMutation::PutSessionTerminal(bare_terminal(
|
|
||||||
SessionTerminalState::Clean,
|
|
||||||
))],
|
|
||||||
vec![DomainStoreMutation::CleanSessionTerminalAfterAbsence(
|
|
||||||
bare_terminal(SessionTerminalState::Clean),
|
|
||||||
)],
|
|
||||||
];
|
|
||||||
|
|
||||||
for mutations in steps {
|
|
||||||
assert_differential_step(&row_store, &reference_store, &mutations);
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let before = row_store.snapshot().unwrap();
|
store
|
||||||
assert_differential_step(
|
.apply(&[
|
||||||
&row_store,
|
DomainStoreMutation::PutGeneration(generation(DurableDecision::Preparing)),
|
||||||
&reference_store,
|
DomainStoreMutation::PutSession(session(SessionPhase::Waiting)),
|
||||||
&[
|
DomainStoreMutation::PutParticipant(publication_partial()),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let before_refusal = DomainStore {
|
||||||
|
metadata: DomainStoreMetadata::v1(),
|
||||||
|
domain: domain(),
|
||||||
|
generations: vec![generation(DurableDecision::Preparing)],
|
||||||
|
participants: vec![publication_partial()],
|
||||||
|
sessions: vec![session(SessionPhase::Waiting)],
|
||||||
|
session_terminals: vec![bare_terminal(SessionTerminalState::Intent)],
|
||||||
|
mailboxes: vec![mailbox(ParticipantState::Intent)],
|
||||||
|
};
|
||||||
|
assert_eq!(store.snapshot().unwrap(), before_refusal);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.apply(&[
|
||||||
DomainStoreMutation::PutGeneration(generation(DurableDecision::Published)),
|
DomainStoreMutation::PutGeneration(generation(DurableDecision::Published)),
|
||||||
DomainStoreMutation::PutSession(session(SessionPhase::Attached)),
|
DomainStoreMutation::PutSession(session(SessionPhase::Attached)),
|
||||||
],
|
],)
|
||||||
|
.is_err()
|
||||||
);
|
);
|
||||||
assert_eq!(row_store.snapshot().unwrap(), before);
|
assert_eq!(store.snapshot().unwrap(), before_refusal);
|
||||||
|
|
||||||
drop(row_store);
|
drop(store);
|
||||||
drop(reference_store);
|
assert_eq!(open_store(&directory).snapshot().unwrap(), before_refusal);
|
||||||
assert_eq!(
|
|
||||||
open_store(&row_directory).snapshot().unwrap(),
|
|
||||||
open_store(&reference_directory).snapshot().unwrap()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue