ServiceVM

Description

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

Details

Source
GitHub
Dialect
pharo (25% confidence)
License
MIT
Stars
2
Forks
3
Created
June 6, 2014
Updated
March 21, 2025

Categories

Web Education / Howto

README excerpt

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
                                                            
← Back to results