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:
- ServiceVM gem
- Service task
- Schedule task and Poll for result
- Seaside integration
- Installation
- ServiceVM Development Support for tODE
For a service gem, we havetwo problems:
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.
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
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 ]
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 ].
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:
Once we have a value we ask the user if they want to try again:
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 ] ]
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.
] ].
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:
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:
A debugger can be opened on the continuation.
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
:
The project list
provides an overview of all projects loaded into your image.
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
*
,/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