When you have an MDrivenServer you can do a lot of useful things in serverside jobs.
Up until now there has been one common and one fantastic way to trigger serverside jobs:
- The common way: Timer/Clock to run a Query periodically to check information criteria’s and start the job for the returned results.
- The fantastic way: Cache invalidation – where the job is analyzed on what members it uses and those member-instances are remembered in a cache manifest. And whenever anything is updated in the system that touches the information used – the server cross reference the manifests and invalidate the exact caches, whereupon the jobs can be started to update them again. Extremely lean close to the theoretical optimum.
#1 is a simple straightforward robust way to handle things. It will discover objects that meet a criteria by checking regularly.
#2 is the holy grale and solves one of the hardest problems in information technology – but it is not fit for all jobs – since keeping cache manifests costs resources.
In production systems we rely heavily on serverside jobs for tasks like these:
- To assign unique numbers to stuff (the “exclusive access” problem, back in the days this used to be solved with a serializable transaction on database-level )
- To communicate with other systems (integration, send data to and receive data from others)
- Updating persisted calculated values (the cache problem).
- Check some automated step in a state machine or the like and move it forward (the self playing piano problem, or orchestration)
The #1-“exclusive access”-need is possible to solve with timer/clock+information but not ideal since a number should be assigned quickly – and it is not ideal to have a lot of these clock jobs that almost never do anything but must be checked as often as possible.
The #2-integration-need is best solved with a timer/clock+information-based trigger ; “It has passed 10 minutes, do we have the need, then execute”. There is often no other signal to react on other than passage of time.
The #3-cache-need is best solved with Cache Invalidation – so that the system automatically sees when dependent data is changed and reacts accordingly – very lean. But you could use a timer/clock to recalculate the cache ever so often – even though it may not be needed (resource waste, but sadly very common practice).
The #4-orchestration can also be done with timer/clock+information check but again- as in #1 – it is not ideal since they will most of the time – when clock-criteria is fulfilled – not have the information criteria fulfilled and we have done a check in vain.
Every need is addressable with timer/clock+information check – but it is not the most lean way to do tasks of type 1 and 4.
This is why we now introduce a new third way to trigger work on the server – or as we also may call it – asynchronous work.
The solution on how to easily spin of any kind of work and to 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 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 not existing 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 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 on the class, ie: ticket.ViewModel:=YourClass.ViewModels.SomeViewModelName
- 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, as 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 – ie 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 is looking for SysAsyncTickets that has null in the Done attribute. This job has 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 has 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 – and then save and changed state that came out of those actions.
Seemingly simple as this is, it do brings a powerful solution to many of the needs we come across building scalable multi user enterprise-grade information systems.
If I have a Customer object and I want to give it a unique identity number. I cannot just assign a number from a known latest-used-number-singleton because I do not know what other users are doing right now – they may want unique numbers for their new customers as well. So I need to have “exclusive access” to the latest number while I increase it, grab it and assign it to my customer.
I can do this by defining a ViewModel and name it TheCustomerNumberAssigner, rooted in Customer that has one action defined like this:
I now want this ViewModel action to execute in a serialized context so that I have exclusive access – and the only place that can be guaranteed is on the server.
To trigger this asynchronous execution I can now go like this from a method inside the Customer class:
let ticket=SysAsyncTicket.Create in (
Once the customer and ticket are saved the server will find the Ticket, resolve the customer, find the viewmodel, execute the action (could be many actions – but only one in this case), save.
Once a refresh is done your end-user will see the assigned customernumber – and possibly communicate it to people waiting for it.
This is mostly the exact same behavior that the Timer/Clock-way to solve this problem – but this way will scale better because you want to assign unique numbers to invoices and a hundred other different classes you may have. With this new approach we only have 1 timer/clock triggering, consuming the SysAsyncTickets as they are discovered.
Would it not be neat if we could skip the ViewModel step for really small async cases like the number assignment above?
Yes! You can now send in Class.Method in the ViewModel property – the MDrivenServer will execute the method for the root object – it will however 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:
Consider having the precondition of the AssignNumber method set to:
And the ticket:
let ticket=SysAsyncTicket.Create in (