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
Old
format, theNew
format or anIntermediate
format. This determines which versions of the aggregate it can read.Writes Can read Old
Old
,Intermediate
New
New
,Intermediate
Intermediate
Old
,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
Old
toIntermediate
- Make sure all the aggregate instances get updated by the running application instances.
- Update all application instances from writing
Intermediate
toNew
. - Make sure all the aggregate instances get update by the running application instances (the assumption is that overwriting
Intermediate
withNew
removes 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
/check
commands 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?
alloy
noralloy6
after the first triple-backtick do not seem to work (if I usejava
at least some keywords get highlighted in the previes).
- I’m sure I’m forgetting something else I wanted to ask/mention.