Ajax: Polling for Long Running Processes in Seaside with Scriptaculous

A Seaside session is inherently single threaded, so I can't use Ajax calls to do long running processes without tying up the main session and hanging the app from the users point of view. In order to get around this, I had to change my approach. Rather than launch a process and wait for the result, I fork the process, return immediately, and poll for the result to show up in a dictionary that is shared via the lexical environment the process was forked from.

I'm using Scriptaculous and its periodical callback mechanism to poll for updates. When a value is found the poll gets both the answer and a bit of JavaScript to stop the polling. Should an error occur, the poller is also stopped.

I've added a couple small extension methods to WATagBrush, but they seem to be working pretty well for me so far.

with: initialBlock waitMax: aDuration forWork: aBlock thenRender: aRenderBlock
    self attributeAt: #default put: initialBlock value.
    self with: initialBlock.
    self waitMax: aDuration forWork: aBlock thenRender: aRenderBlock

with: initialBlock waitMax: aDuration forWork: aBlock thenRender: aRenderBlock onError: anErrorString
    self attributeAt: #default put: initialBlock value.
    self attributeAt: #error put: anErrorString.
    self with: initialBlock.
    self waitMax: aDuration forWork: aBlock thenRender: aRenderBlock

And now the periodical that does the bulk of the work...

waitMax: aDuration forWork: aBlock thenRender: aRenderBlock
    | result |
    result := self forkWaitFor: aDuration longRunningProcess: aBlock.
    self session addLoadScript: (canvas periodical
        id: (self attributes at: #id);
        assignTo: (self attributes at: #id) , 'Poller';
        asynchronous: true;
        evalScripts: true;
        frequency: 2;
        onFailure: (canvas periodical
            alias: (self attributes at: #id) , 'Poller';
            call: 'stop') , (canvas element
                id: (self attributes at: #id);
                addClassName: #ajaxError;
                update: (self attributeAt: #error ifAbsent: [ 'Error' ]));
            [ :r | 
            (result at: #result) ifNil: [ r render: (self attributeAt: #default) ]
                [ r script: (r periodical
                        alias: (self attributes at: #id) , 'Poller';
                        call: 'stop').
                aRenderBlock value: r value: (result at: #result) ] ])

And a helper method to separate the actual forking from the rendering. When the background process finishes, either by getting a value or by timing out, it dumps its result into the dictionary which the polling process will then find and display.

forkWaitFor: aDuration longRunningProcess: aBlock 
    | result |
    result := Dictionary with: #result -> nil.
    [[ result 
        at: #result
        put: (aBlock 
                valueWithin: aDuration
                onTimeout: [ 'timeout' ]) ] 
        on: Error
            [ :error | 
                at: #result
                put: error messageText ] ] 
        forkAt: Processor systemBackgroundPriority
        named: 'polling'.
    ^ result

I use it like so...

html div 
    with: 'Searching...' 
    waitMax: 30 seconds 
    forWork: [ self getDataFromLongRunningProcess ] 
    thenRender: [:ajax :value | ajax update: value ]
    onError: 'Sorry!'

I might have to fix this up in the near future, to make #with: work with blocks like it should, but it's serving me well at the moment. I use this to fetch prices for hotels on Reserve Travel so I can show the hotel data immediately while in the background I'm running out to other systems to find prices which can sometimes take a while.

Comments (automatically disabled after 1 year)

[...] Another good post from Ramon Léon. [...] ]

Giles Bowkett 3928 days ago

Does this single-thread thing present a real problem? The most I've done in Seaside so far is following along with your blog screencast, but in one of the podcasts I've heard him on, Avi Bryant said Dabble uses Ajax mostly for pre-loading data, rather than UI stuff, so it must be feasible. Is changing the single-threaded nature of Seaside possible? Have you found this to be tricky only in unusual situations? I know most uses of Ajax don't generally involve long-running processes, although polling for the status of such processes is a pretty well-accepted method of handling them. (Sorry if the questions are overkill.)

Ramon Leon 3928 days ago

Actually, its single threaded nature is a feature, a necessary one, when you consider that requests come in on different threads, but the server contains long lived objects that must serve them all. Rather than making you think about such things, and do the locking yourself, Seaside simply forces all access to a session through a lock allowing one request at a time to access the session. So it's not something you want to change, just something you need to be aware of, and how know how to work around, should the need arise.

This isn't normally a problem in other frameworks because they don't keep things like pages and controls alive between multiple requests like Seaside does, but, it is something you have to be aware of if you try and do something like I did in this article, because otherwise you'll see all your processes that you expect to run asynchronously running serially and you won't know why.

When not doing Ajax, you'd never notice this anyway, but Ajax allows a page to kick off multiple threads on the server and do work asyncronously, which personally, I find highly useful, for much more than loading data.

I used this because I had some local data I could show right away, and then some other related data from remote systems that may or may not come back, and take several seconds to fetch. This allowed me to show the user what I had instantly and inform them, in the page widgets, that more data was being fetched, and when it comes in it'll display. I consider this little piece of code critical to the user experience in my applications. Consider how useful this might be in a mashup where you get data from many sources you might not control, or want to wait for.

Nick Alexander 3529 days ago

For future users of this snippet: if the id of the element displaying the output is not set, the updater will silently do nothing. Try:

html div id: 'someid'; with: 'Searching...' waitMax: 30 seconds forWork: [self getDataFromLongRunningProcess] thenRender: [:r :value | r text: value]

Thanks Ramon, for the best seaside blog of them all!

Ramon Leon 3528 days ago

No problem, it's a rather small pond.

about me|good books|popular posts|atom|rss