AsyncTicket is a pattern you can easily add to your model, backed by recent MDrivenServers.
Read about its background here: https://blog.mdriven.net/doing-stuff-later/
The solution to how to easily spin any kind of work and do it later – or async – is implemented by part model-pattern, part framework support, and part MDrivenServer support. That being said, it is important that you have versions from 2020-Feb-28 - or later - for this to work as described below.
The contract that instructs the system - that you want this ability - is fulfilled by adding a class that is defined like this:
- Named SysAsyncTicket
- Has attribute DeleteTime with DateTime-nullable type. This will be set by the server after your work has been executed to Now+KeepReceiptMinutes.
- Has attribute Done with DateTime-nullable type. This will be set by the server when work is done.
- Has attribute Error with string (nullable optional) type. If there is an initial issue with the ticket – like if the RootId is not valid, or the ViewModel is nonexistent - you will see this property filled with information.
- Has attribute ExecuteEarliest with DateTime-nullable type. If you want a delayed start you can set this – but leave it to null for the server to execute it as soon as possible.
- Has attribute KeepReceiptMinutes with Integer type – with an initial value set to your liking. Signals that the auto cleaning of a used ticket should happen X minutes after the job finishes.
- Has attribute RootId with string (nullable optional) type. This is resolved and set by the framework to the external id that the RootObject association points to. If it points to a yet unsaved object, the framework will discover this and set this property after a valid key is retrieved. If this extra pass was needed, the framework will automatically resave the SysAsyncTicket with the updated RootId property.
- Has attribute ViewModel with string (nullable optional) type. This must be a valid name of a ViewModel existing in your model – it does not need to be a serverside-ViewModel. Any rooted viewmodel will make do. You should use the constants found in the class, ie:
- Has a transient(Persistent=false) single association RootObject navigable in the direction of the most abstract class you ever want to use for async work (ie the SysSuperClass in a standard model). It is important that this link is set to Persistent=false since pointing out a hyper-abstract-class in a persistent association is really bad practice. It would require the framework to ask all possible subclasses if they have the key (this process is called exactification of a foreign key). In a system with hundreds of classes, this means hundreds of queries – i.e. do not set persistent links to abstract classes if the key is stored outside the abstract class – it will work but it will hurt performance.
Once you have this in your model and the server sees it, the server will create 2 new administrative serverside jobs. One job will be looking for SysAsyncTickets that have null in the Done attribute. This job has a high frequency – just like a key assigning job would have – but it is more ok for the SysAsyncTicket-job since there is only one of it. The other job deletes tickets that have a DeleteTime older than now – this job has a very low frequency.
The high-frequency SysAsyncTicket job will look up the root object from RootId – look up the referred ViewModel – and then execute the ViewModel just as a normal serverside job. Typically, this means it will execute the actions on the root-level in order from top to bottom then save the changed state that came out of those actions.
Updates 2020-03-03: No Need for a ServerSide ViewModel - Just Use a Method
Wouldn't it be neat if we could skip the ViewModel step for really small async cases like the number assignment above?
Yes! You can now send the Class.Method in the ViewModel property. The MDrivenServer will execute the method for the root object. However, it will first check the PreCondition of the method.
Given this, the sample above would skip the ViewModel+action and instead add a method on the Customer class:
Customer.AssignNumber: SomeSingleton.oclSingleton.LatestUsedCustomerNumber:=SomeSingleton.oclSingleton.LatestUsedCustomerNumber+1; self.CustomerNumber:=SomeSingleton.oclSingleton.LatestUsedCustomerNumber
Consider having the precondition of the AssignNumber method set to:
And the ticket:
let ticket = SysAsyncTicket.Create in ( ticket.RootObject:=self; ticket.ViewModel:=’Customer.AssignNumber’ -- This will call the AssignNumber method on self of type Customer, in the server context )
Note: Invoking a method differs from invoking a ServerSide-ViewModel as below.
let ticket = SysAsyncTicket.Create in ( ticket.RootObject:=self; ticket.ViewModel:=Customer.ViewModels.SomeViewModel -- This will invoke the SomeViewModel-ServerSideViewModel with self of type Customer as root )
Add the Priority Property to the SysAsyncTicket class:
If found, the OCL-PS query will include
->orderby(at|at.Priority) and this will push jobs you know are short and should be done asap to the top of any queue that may form. 1 has a higher priority than 10 and 1 will be taken from the queue before 10 if both are present at the time of decision.
Download a Ready Package to Merge Into Your Model
Executing a Method on a Subclass
If you produce a SysAsyncTicket to execute a method like this:
let ticket=SysAsyncTicket.Create in ( ticket.RootObject:=self; ticket.ViewModel:='Thing.AssignNumber' )
The actual type of self is not a "Thing" but rather a "SubClassOfThing" - then you run into trouble with Synchronization. To avoid trouble, name the reference on what to execute like this:
The problem is that the MDrivenServer would, in the first case, treat your object as a Thing (formally correct) and the changes recorded for Synchronization will assume it is an object of class "Thing" that has changed. When your client refreshes the server's data, they will be notified that a "Thing" with identity X has changed and your client will not be able to associate that to your object of class "SubClassOfThing".
The generic way to name the reference to execute is: