I am by no means experienced in formal methods. You can thank Hillel Wayne’s published work for anything I managed to learn, and me for anything I didn’t.
Sorry, this is a long one. I have more difficulties than questions, but I hope people here can offer some pointers for the first besides answers to the second.
I’m trying to model what from an enterprise, industry perspective looks like a simple example that still has some relevance to real life. I hope I’m neither too wordy not missing too many crucial details below:
-
There is some kind of aggregate in the DDD sense - a business concept that has a unique identifier, might contain other things with unique identifiers and is a “transaction boundary”.
- That’s not a good explanation of what a DDD aggregate is, but I think it’s what’ll be the most important for writing the model.
- Any update we make that only changes something within the aggregate is atomic.
-
I want to change the data representation in the DB.
-
The update will be gradual, not all-at-once. We don’t want to stop the normal use of the application.
-
An application instance (there’s more than one) can write data in the
Oldformat, theNewformat or anIntermediateformat. This determines which versions of the aggregate it can read.Writes Can read OldOld,IntermediateNewNew,IntermediateIntermediateOld,New,Intermediate- In this model I’m not interested in modeling properties like the equivalence of the before/after representations. Assume that’s done separately.
-
Application instances change what versions they write one-at-atime.
-
Normal client requests still update the database through the application instances through the whole lifetime of the process.
- These write out whatever representation the application instance is putting out.
-
The overall idea is to:
- Update all application instances from writing
OldtoIntermediate - Make sure all the aggregate instances get updated by the running application instances.
- Update all application instances from writing
IntermediatetoNew. - Make sure all the aggregate instances get update by the running application instances (the assumption is that overwriting
IntermediatewithNewremoves the redundancies present in the old one.
- Update all application instances from writing
-
These are baby steps. I don’t even try to think about cases like “through pathological scheduling the DB commits a very old write way after the entire migration process seems to have completed successfully”.
- It might be an interesting future extension, or not. I’ve not given this sort of thing thought yet, as I’m struggling to get the current model doing sensible things.
- Lower hanging fruit I’ve also left intentionally unaddressed: additions of new aggregate instances and deletions of existing ones.
- For an RDBMS (unless everything’s stuck in a JSON cloumn in PostgreSQL) I’d have to have schema changes in the process as well. At this-or-close-to-this level of detail it does not seem like there’d be anything interesting the model could say about them.
module migration
check { spec implies eventually always Stutter = PossibleStep }
// "clean" model signatures
sig AppInstance {
, var writes : one Schema
}
sig Aggregate {
, var schema : one Schema
}
one sig Old extends Schema {}
one sig Intermediate extends Schema {}
one sig New extends Schema {}
abstract sig Schema {}
pred canRead[i: one AppInstance, a: one Aggregate] {
a.schema in writerCanReadSchema[i.writes]
}
fun writerCanReadSchema[ws: one Schema]: set Schema {
{ s: Schema |
{
ws = Old implies not s = New
ws = New implies not s = Old
}
}
}
// safety
pred qos {
all i: AppInstance | all a: Aggregate | canRead[i, a]
}
// liveness
pred completes {
storedForeverAs[New] and writtenForeverAs[New]
}
pred neverWrittenAgain[s: Schema] { eventually always no (AppInstance.writes & s) }
pred writtenForeverAs[s: Schema] { eventually always AppInstance.writes = s }
pred neverStoredAsAgain[s: Schema] { eventually always no (Aggregate.schema & s) }
pred storedForeverAs[s: Schema] { eventually always Aggregate.schema = s }
pred progressesToNo[s: Schema] {
eventually always no (Aggregate.schema & s)
eventually always no (AppInstance.writes & s)
}
pred progressesTo[s: Schema] {
eventually always Aggregate.schema = s
eventually always Aggregate.schema = s
}
// fairness support signatures
var sig PossibleStep in Step {}
one sig Workload extends Step {}
one sig Migration extends Step {}
one sig Stutter extends Step {}
abstract sig Step {}
// behaviour
pred spec {
init and step
}
pred init {
#Aggregate > 1 and #Aggregate < 4
#AppInstance > 2 and #AppInstance < 4
all a: Aggregate | a.schema = Old
all i: AppInstance | i.writes = Old
PossibleStep = Migration
}
pred step {
always {
Migration = PossibleStep or Stutter = PossibleStep
some AppInstance.writes - New implies Migration in PossibleStep
Migration = PossibleStep implies migrationStep
Stutter = PossibleStep implies stutter
}
}
pred workloadStep {
// TODO: implement
stutter
}
pred migrationStep {
AppInstance.writes = New implies Migration not in PossibleStep'
some i: AppInstance | i.writes = Old implies updateInstance[i, Intermediate]
some i: AppInstance | {
i.writes = Intermediate
no (AppInstance.writes & Old)
} implies updateInstance[i, New]
}
pred updateInstance[i: AppInstance, nw: Intermediate + New] {
all a: Aggregate | a.schema' = a.schema
one i: AppInstance {
all o: AppInstance - i | o.writes' = o.writes
i.writes' = nw
}
}
pred stutter {
all i: AppInstance | i.writes' = i.writes
all a: Aggregate | a.schema' = a.schema
}
- I’ve read some people on here reporting pain points when there’s a lot of variables being updated at the same time.
- I’m having similar difficulties even though there’s only 2 time-varying relations in the model.
- I am sure some of my pain is self-inflicted. I’ve probably tried multiple complex things that someone more experienced could express more simply.
- Some of it might be that I’m trying bits and pieces from specs I can see on here without being able to find understandable (to me) documentation for them, ex
let unchanged[s, r] = s.(r)' = s.r. - By now I think that the most likely way for “clear nonsense” to happen is when I’ve not specified how all variables change from each and every reachable/possible state.
- Some indication in the visualizer that this is happening (dashed arrow on the behaviour trace? IDK) might go a long way for someone like me.
- It’s not obvious why the spec is triggering Alloy’s arbitrary-change-when-unspecified behavior. I feel like knowing how to structure a action predicate (might be the wrong term?) to avoid that from happening would go a long way towards moving from “mostly fixing up the basic encoding of the model” to “mostly working on the problem”.
- Sometimes I’m surprised when some variables do undergo arbitrary change and yet the safety property I’ve defined (
qos) holds. This seems too good to be true given all the uncertainty, confusion and pain I’m experiencing otherwise.
- I’ve tried adding some fairness (? don’t grasp the terminology here well) to ensure things proceed up to the point where nothing can change anymore. This is clearly not a trivial thing to figure out how to implement from scratch.
- Would this kind of model be better served by TLA+?
- I tried splitting this off into multiple modules so I had the spec in one and
run/checkcommands in others.- I wanted to do this to work with different commands for the same spec more easily.
- Unfortunately, the table view on an instance seems to hide all the signatures (?) from the imported spec module.
- I hope I’m just missing something in the UI:
- either to show signatures from other modules in the table view with these
- or to pick which command to run from multiple present in a module.
- How do I highlight the syntax as Alloy in Disqus?
alloynoralloy6after the first triple-backtick do not seem to work (if I usejavaat least some keywords get highlighted in the previes).
- I’m sure I’m forgetting something else I wanted to ask/mention.