/ServiceVM

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

Primary LanguageSmalltalkMIT LicenseMIT

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, 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):

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
  2. Service task
  3. Schedule task and Poll for result
  4. Seaside integration
  5. Installation
  6. ServiceVM Development Support for tODE

ServiceVM gem

For a service gem, we havetwo problems:

Gem control

Fortunately, Paul DeBruicker solved both of these problems Back in 2011. He created a couple of classes (WAGemStoneRunSmalltalkServer & WAGemStoneSmalltalkServer) and wrote two bash scripts (runSmalltalkServer and startSmalltalkServer) 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, 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:

WAGemStoneRunSmalltalkServer
   addServerOfClass: WAGemStoneServiceExampleVM
   withName: 'ServiceVM-ServiceVM'
   on: #().

and control the gem with these expressions:

"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:

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:

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:). 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:

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):

WAGemStoneServiceExampleTask valuable: [ 
  (HTTPSocket
    httpGet: 'http://www.time.org/zones/Europe/London.php')
    throughAll: 'Europe/London - ';
    upTo: Character space ].

or:

WAGemStoneServiceExampleTask 
  valuable: (WAGemStoneServiceExampleTimeInLondon 
           url: 'http://www.time.org/zones/Europe/London.php').

The processTask method in WAGemStoneServiceExampleTask is implemented as follows:

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:

| 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

Once we have a value we ask the user if they want to try again:

try again seaside page

Here's the render method:

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:

automaticPoll
  self addTaskToQueue.
  self poll: 1

The addTaskToQueue method:

addTaskToQueue
  task addToQueue

and the poll: method:

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

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):

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:

| svcRepo |
svcRepo := 'github://GsDevKit/ServiceVM:master/repository'. "Use this path if you haven't 
                                                             cloned the GitHub repository
                                                             don't forget to install the
                                                             scripts manually."
svcRepo := 'filtree:///opt/git/ServiceVM/repository'.      "Edit and use this path if you 
                                                             have cloned the GitHub 
                                                             repository."
GsDeployer bulkMigrate: [
{
  #('Zinc' 'github://glassdb/zinc:gemstone3.1/repository').
  {'ServiceVM'. svcRepo} } do: [:ar |
  | projectName repoPath |
  projectName := ar at: 1.
  repoPath := ar at: 2.
  Metacello new
    baseline: projectName;
    repository: repoPath;
    get.
  Metacello new
    baseline: projectName;
    repository: repoPath;
    load.
  Metacello new
    baseline: projectName;
    repository: repoPath;
    lock.
  ] ].

Install using tODE

Type the following commands at the tODE command prompt to load the Zinc and ServiceVM project:

cd /home/projects/zinc
project load @project                          # load Zinc from GitHub

cd /home/projects/serviceVM
project load @project                          # load the ServiceVM project from GitHub
project clone @project                         # clone the github repository

                                               # run installScripts script
eval `System performOnServer: '/opt/git/ServiceVM/bin/installScripts.sh'`

mount /opt/git/ServiceVM/tode /home serviceVM  # mount tODE dir at /home/serviceVM
cd /home/serviceVM
edit README.md                                 # edit README (this file) in tODE

There are several utiltiy scripts available in the /home/serviceVM directory:

Script Purpose
objlog short-cut script for opening object log
project project entry specifiecation
serviceExample script for manipulating service example task
serviceVM script for controlling the serviceVM
webServer script for controlling the zinc web server

Open Object Log Viewer

The objlog script executes the following tODE command:

ol view --age=`5 minutes` --reverse

which opens an Object Log window on the last 5 minutes worth of object log entries and lists the entries in reverse order with the newest entries at the top of the window. Here is a sample window:

ol view

A debugger can be opened on the continuation.

Project Entry

The project entry is a an object:

^ TDProjectSpecEntryDefinition new
    baseline: 'ServiceVM'
      repository: 'github://GsDevKit/ServiceVM:master/repository'
      loads: #('default');
    projectPath: self parent printString;
    status: #(#'active');
    yourself

used by the project list:

project list

The project list provides an overview of all projects loaded into your image.

Start/Stop Service VM (serviceVM)

The --start option starts the serviceVM in an external topaz session. The --stop option stops the serviceVM. Registration need only be done once in the image (the registration is persistent). Use ./serviceVM --help for additional options.

./serviceVM --register        # register the service vm (done once)
./serviceVM --start           # start the service vm gem
./serviceVM --stop            # stop the service vm gem

**serviceVM --register serviceVM --start serviceVM --stop

###Start/Stop Zinc Web Server (webServer) The --start option starts the web server in an external topaz session. The --stop option stops the web server. Registration need only be done once in the image (the registration is persistent). Use ./webServer --help for additional options.

./webServer --register=zinc   # register zinc as web server (done once)
./webServer --start           # start web server gem
./webServer --stop            # stop web server gem

****webServer --register=zinc webServer --start webServer ----stop*

Scheduling Service Tasks (serviceExample)

,/serviceExample --status               # state of service task engine
./serviceExample --task                 # create a new task
./serviceExample --task=3               # access task #3
./serviceExample --task=3 --addToQueue  # schedule task #3 to process next step
./serviceExample --task=3 --poll=10     # poll for completion of task #3 (wait 10 seconds)

**./serviceExample --status ./serviceExample --task ./serviceExample --addToQueue ./serviceExample --poll