The Service VM is intended to provide example code for creating and using a separate "Service VM" for offloading work that in a Squeak/Pharo Seaside application, you would have forked of a thread to do the work
ServiceVM
=========
This project provides example code for creating and using a
separate GemStone vm for processing long running operations from a Seaside HTTP request.
As described in [Porting Application-Specific Seaside Threads to GemStone][6], it is not
advisable to fork a thread to handle a long running operation in a GemStone vm.
Several years ago, [Nick Ager asked the question (in essense)][5]:
> So what do you expect us to do instead?
to which I replied:
> The basic idea is that you create a separate gem that services tasks
> that are put into an RCQueue (multiple producers and a single consumer).
> The gem polls for tasks in the queue, performs the task, then finishes
> the task, storing the results in the task....On the Seaside side you
> would use HTML redirect (WADelayedAnswerDecoration) while waiting for
> the task to be completed.
That is quite a mouthful, so let's break it down:
1. [ServiceVM gem](#servicevm-gem)
2. [Service task](#service-task)
3. [Schedule task and Poll for result](#schedule-task-and-poll-for-result)
4. [Seaside integration](#seaside-integration)
5. [Installation](#installation)
6. [ServiceVM Development Support for tODE](#servicevm-development-support-for-tode)
## ServiceVM gem
For a service gem, we havetwo problems:
* [How do we start and stop a service vm?](#gem-control)
* [How do we define the service vm service loop?](#service-loop)
### Gem control
Fortunately, Paul DeBruicker solved both of these problems
Back in 2011. He created a
[couple of classes (WAGemStoneRunSmalltalkServer & WAGemStoneSmalltalkServer)][33]
and wrote two bash scripts
([runSmalltalkServer][34] and [startSmalltalkServer][35]) that
make it possible to start and run a ServiceVM gem for the purpose of executing long
running operations. The idea is similar the one used to
[control Seaside web server gems][36], but generalized
to allow for starting gems that run an arbitrary service loop.
You can register a server class (in this case **WAGemStoneServiceExampleVM**) with
the class **WAGemStoneRunSmalltalkServer**:
```Smalltalk
WAGemStoneRunSmalltalkServer
addServerOfClass: WAGemStoneServiceExampleVM
withName: 'ServiceVM-ServiceVM'
on: #().
```
and control the gem with these expressions:
```Smalltalk
"serviceVM --start"
| server serviceName |
serviceName := 'ServiceVM-ServiceVM'.
server := WAGemStoneRunSmalltalkServer serverNamed: serviceName.
WAGemStoneRunSmalltalkServer startGems: server.
"serviceVM --stop"
| server serviceName |
serviceName := 'ServiceVM-ServiceVM'.
server := WAGemStoneRunSmalltalkServer serverNamed: serviceName.
WAGemStoneRunSmalltalkServer stopGems: server.
```
### Service Loop
The service vm's service loop is responsible for keeping
an eye on the queue of service tasks, pluck tasks from the
queue when they become available then fork a thread in which the task will perform it's work.
The main loop wakes up every 200ms and services the task queue:
```Smalltalk
serviceLoop
| count |
count := 0.
[ true ]
whileTrue: [
self performTasks: count. "service the task queue"
(Delay forMilliseconds: 200) wait. "Sleep for a 200ms"
count := count + 1 ]
```
In the **WAGemStoneMaintenanceTask** infrastructure, the *performTask:* message above ends up
evaluating the block defined below:
```Smalltalk
serviceVMServiceTaskQueue
^ self
name: 'Service VM Loop'
frequency: 1
valuable: [ :vmTask |
"1. CHECK FOR TASKS IN QUEUE (non-transactional)"
(self serviceVMTasksAvailable: vmTask)
ifTrue: [
| tasks repeat |
repeat := true.
"2. PULL TASKS FROM QUEUE UNTIL QUEUE IS EMPTY OR 100 TASKS IN PROGRESS"
[ repeat and: [ self serviceVMTasksInProcess < 100 ] ]
whileTrue: [
repeat := false.
GRPlatform current
doTransaction: [
"3. REMOVE TASKS FROM QUEUE..."
tasks := self serviceVMTasks: vmTask ].
tasks do: [ :task |
"4. ...FORK BLOCK AND PROCESS TASK"
[ task processTask ] fork ].
repeat := tasks notEmpty ] ] ]
reset: [ :vmTask | vmTask state: 0 ]
```
From a GemStone perspective, it is important to note that only the **serviceVMTasks:** method
is performed from within the transaction mutex ([GRGemStonePlatform>>doTransaction:][37]).
There are many concurrent threads running within the service vm, so all threads running
must take care to hold the transaction mutex for as short a time as possible. Also when
running "outside of transaction" one must be aware that any persistent state may change
at transaction boundaries initiated by threads other than your own so one must use discipline
within your application to either:
* avoid changing the state of persistent objects used in service vm
* or, copy any state from *unsafe* persistent objects into temporary variables
or *private* persistent objects.
Speaking of **serviceVMTasks:**, here's the implementation:
```Smalltalk
serviceVMTasks: vmTask
| tasks persistentCounterValue |
tasks := #().
persistentCounterValue := WAGemStoneServiceExampleTask sharedCounterValue.
WAGemStoneServiceExampleTask queue size > 0
ifTrue: [
vmTask state: persistentCounterValue.
tasks := WAGemStoneServiceExampleTask queue removeCount: 10.
WAGemStoneServiceExampleTask inProcess addAll: tasks ].
^ tasks
```
## Service task
The service task is an instance of **WAGemStoneServiceExampleTask** and takes a *valuable*
(e.g., a block or any object that responds to *value*):
```Smalltalk
WAGemStoneServiceExampleTask valuable: [
(HTTPSocket
httpGet: 'http://www.time.org/zones/Europe/London.php')
throughAll: 'Europe/London - ';
upTo: Character space ].
```
or:
```Smalltalk
WAGemStoneServiceExampleTask
valuable: (WAGemStoneServiceExampleTimeInLondon
url: 'http://www.time.org/zones/Europe/London.php').
```
The *processTask* method in **WAGemStoneServiceExampleTask** is implemented as follows:
```Smalltalk
processTask
| value |
self performSafely: [ value := taskValuable value ].
GRPlatform current
doTransaction: [
taskValue := value.
hasValue := true.
self class inProcess remove: self ]
```
## Schedule task and Poll for result
To add tasks to the service vm queue, you simply send the #addToQueue message to the task
and then check the state of the task until it has been serviced:
```Smalltalk
| task |
task :=WAGemStoneServiceExampleTask
valuable: (WAGemStoneServiceExampleTimeInLondon
url: 'http://www.time.org/zones/Europe/London.php').
task addToQueue.
System commit. "commit needed to that service vm can see the task"
[
System abort. "abort needed to see new state of task"
task hasValue ] whileFalse: [(Delay forSeconds: 1) wait ].
```
## Seaside integration
For Seaside the component we start with a task that has no value (yet)
and prompt the user to automatically poll for a result or to manually
pool for the result:
![initial seaside page][20]
Once we have a value we ask the user if they want to
try again:
![try again seaside page][38]
Here's the render method:
```Smalltalk
renderContentOn: html
| autoLabel manualLabel createNewTask |
createNewTask := false.
task hasError
ifTrue: [
html heading: 'Error'.
html text: task exception description ]
ifFalse: [
task hasValue
ifTrue: [
html heading: 'The time in London is: ' , task value , '.'.
autoLabel := 'Try again and wait for result?'.
manualLabel := 'Try again and manually poll for result (refresh page)?'.
createNewTask := true ]
ifFalse: [
html heading: 'The time in London is not available, yet. '.
autoLabel := 'Get time in London and wait for result?'.
manualLabel := 'Get time in London and manually poll for result (refresh page)?' ].
html anchor
callback: [
createNewTask
ifTrue: [ task := self newTask ].
self automaticPoll ];
with: autoLabel.
html
break;
text: ' or ';
break.
html anchor
callback: [
createNewTask
ifTrue: [ task := self newTask ].
self addTaskToQueue ];
with: manualLabel ]
```
The automaticPoll method:
```Smalltalk
automaticPoll
self addTaskToQueue.
self poll: 1
```
The addTaskToQueue method:
```Smalltalk
addTaskToQueue
task addToQueue
```
and the poll: method:
```Smalltalk
poll: cycle
self
call:
(WAComponent new
addMessage: 'waiting for time in London...(' , cycle printString , ')';
addDecoration: (WADelayedAnswerDecoration new delay: 2);
yourself)
onAnswer: [
task hasValue
ifFalse: [ self poll: cycle + 1 ] ]
```
## Installation
* [Install using Metacello](#install-using-metacello)
* [Install using tODE](#install-using-tode)
### Install using Metacello
Clone the https://github.com/GsDevKit/ServiceVM repository to your local disk and
install the scripts needed by the service vm in the $GEMSTONE product tree (make
sure you have $GEMSTONE defined before running the installScripts.sh step):
```shell
cd /opt/git # root dir for git repository
git clone https://github.com/GsDevKit/ServiceVM.git # clone service vm
cd ServiceVM
bin/installScripts.sh # $GEMSTONE must be defined
```
Use the following script to load Zinc and the ServiceVM project into a fresh
extent0.seaside.dbf:
```Smalltalk
| svcRepo |
svcRepo := 'github://GsDevKit/ServiceVM:master/repository'. "Use this path if you haven't
cloned the GitHub repository
don't forget to install the