NotesSessions, XPages and Threads
When you build an XPages application, long running operations will time out. After all there is a timing limit how long a browser (or XPiNC) client (and the user) will wait for a response. You could resort to launching an agent via a console command, but besides the security consideration it is quite a hack and headache. Mixing XPages and agents doesn't have a happy ending (and should be confined to the upgrading phase of your classic application).
Enter Java multi-threading. I call it " Teenage-mode": your code does multiple things at the same time and doesn't seem to get distracted.
When you read the classics you will find threading hard and rather threatening (pun intended), but thanks to progress in Java6 and above it isn't too hard.
There is the Executor framework that handles all your
(Image courtesy of Lee Chee Chew)
To get a new
The final missing piece is to define
As usual YMMV.
Enter Java multi-threading. I call it " Teenage-mode": your code does multiple things at the same time and doesn't seem to get distracted.
When you read the classics you will find threading hard and rather threatening (pun intended), but thanks to progress in Java6 and above it isn't too hard.
There is the Executor framework that handles all your
Runnables
. There are 2 challenges to overcome: one specific to Notes and one general to all concurrency programming. The former: The Notes Session
class is not threadsave and can't be shared between threads, the later: you have to get your queueing right:
(Image courtesy of Lee Chee Chew)
To get a new
Session
object, we have the session cloner, thanks to the extension library. All your threads implement the Runnable
interface that is later called by the executor framework. There are 2 approaches for background task: one where you expect the thread to return some value at some time and the other where a thread runs its course and terminates silently when done. For a current project I opted for the later. The threads contain a callback class where they report back what they have to say. To make it all work we need:
- A management class that executes the threads for us. Preferably long living (like session or application beans)
- A cloned Notes Session
- One or more classes implementing the runnable interface
package com.notessensei.demo ;
import java.util.Collection ;
import java.util.Map ;
import lotus.domino.Database ;
import lotus.domino.NotesException ;
import lotus.domino.Session ;
import com.ibm.domino.xsp.module.nsf.NSFComponentModule ;
import com.ibm.domino.xsp.module.nsf.NotesContext ;
import com.ibm.domino.xsp.module.nsf.SessionCloner ;
public abstract class AbstractNotesBackgroundTask implements Runnable {
protected final Session notesSession ;
protected final Collection < String > callBackMessages ;
private SessionCloner sessionCloner ;
private NSFComponentModule module ;
public AbstractNotesBackgroundTask ( final Session optionalSession, final Collection < String > messageHandler ) {
this. callBackMessages = messageHandler ;
// optionalSession MUST be NULL when this should run in a thread, contain a session when
// the class is running in the same thread as it was constructed
this. notesSession = optionalSession ;
this. setDominoContextCloner ( ) ;
}
public void run ( ) {
this. callBackMessages. add ( "Background task run starting: " + this. getClass ( ). toString ( ) ) ;
try {
Session session ;
if ( this. notesSession == null ) {
NotesContext context = new NotesContext ( this. module ) ;
NotesContext. initThread (context ) ;
session = this. sessionCloner. getSession ( ) ;
} else {
// We run in an established session
session = this. notesSession ;
}
/* Do the work here */
this. runNotes ( ) ;
} catch ( Throwable e ) {
e. printStacktrace ( ) ;
} finally {
if ( this. notesSession == null ) {
NotesContext. termThread ( ) ;
try {
this. sessionCloner. recycle ( ) ;
} catch (NotesException e1 ) {
e1. printStacktrace ( ) ;
}
}
}
this. callBackMessages. add ( "Background task run completed: " + this. getClass ( ). toString ( ) ) ;
}
private void setDominoContextCloner ( ) {
// Domino stuff to be able to get a cloned session
if ( this. notesSession == null ) {
try {
this. module = NotesContext. getCurrent ( ). getModule ( ) ;
this. sessionCloner = SessionCloner. getSessionCloner ( ) ;
} catch ( Exception e ) {
e. printStacktrace ( ) ;
}
}
}
protected abstract void runNotes ( ) ;
}
You only need to subclass it and implement import java.util.Collection ;
import java.util.Map ;
import lotus.domino.Database ;
import lotus.domino.NotesException ;
import lotus.domino.Session ;
import com.ibm.domino.xsp.module.nsf.NSFComponentModule ;
import com.ibm.domino.xsp.module.nsf.NotesContext ;
import com.ibm.domino.xsp.module.nsf.SessionCloner ;
public abstract class AbstractNotesBackgroundTask implements Runnable {
protected final Session notesSession ;
protected final Collection < String > callBackMessages ;
private SessionCloner sessionCloner ;
private NSFComponentModule module ;
public AbstractNotesBackgroundTask ( final Session optionalSession, final Collection < String > messageHandler ) {
this. callBackMessages = messageHandler ;
// optionalSession MUST be NULL when this should run in a thread, contain a session when
// the class is running in the same thread as it was constructed
this. notesSession = optionalSession ;
this. setDominoContextCloner ( ) ;
}
public void run ( ) {
this. callBackMessages. add ( "Background task run starting: " + this. getClass ( ). toString ( ) ) ;
try {
Session session ;
if ( this. notesSession == null ) {
NotesContext context = new NotesContext ( this. module ) ;
NotesContext. initThread (context ) ;
session = this. sessionCloner. getSession ( ) ;
} else {
// We run in an established session
session = this. notesSession ;
}
/* Do the work here */
this. runNotes ( ) ;
} catch ( Throwable e ) {
e. printStacktrace ( ) ;
} finally {
if ( this. notesSession == null ) {
NotesContext. termThread ( ) ;
try {
this. sessionCloner. recycle ( ) ;
} catch (NotesException e1 ) {
e1. printStacktrace ( ) ;
}
}
}
this. callBackMessages. add ( "Background task run completed: " + this. getClass ( ). toString ( ) ) ;
}
private void setDominoContextCloner ( ) {
// Domino stuff to be able to get a cloned session
if ( this. notesSession == null ) {
try {
this. module = NotesContext. getCurrent ( ). getModule ( ) ;
this. sessionCloner = SessionCloner. getSessionCloner ( ) ;
} catch ( Exception e ) {
e. printStacktrace ( ) ;
}
}
}
protected abstract void runNotes ( ) ;
}
runNotes()
to get started. However you probably want to hand over more parameters, so you need to modify the constructor accordingly. Please note: you can't bring Notes objects across thread boundaries. Next the class that controls all the threads. It should live in the session or application.The class is actually quite simple:
package com.notessensei.demo ;
import java.io.Serializable ;
import java.util.concurrent.ExecutorService ;
import java.util.concurrent.Executors ;
public class NotesBackgroundManager implements Serializable {
private static final long serialVersionUID = 1L ;
// # of threads running concurrently
private static final int THREADPOOLSIZE = 10 ;
private final ExecutorService service = Executors. newFixedThreadPool (THREADPOOLSIZE ) ;
public CSIApplication ( ) {
// For use in managed beans
}
public void submitService ( final Runnable taskDef ) {
if (taskDef == null ) {
System. err. println ( "submitService: NULL callable submitted to submitService" ) ;
return ;
}
// Execute runs without return
this. service. execute (taskDef ) ;
}
@ Override
protected void finalize ( ) throws Throwable {
if ( ( this. service != null ) && ! this. service. isTerminated ( ) ) {
this. service. shutdown ( ) ;
}
super. finalize ( ) ;
}
}
If you rather want the thread returning something at the end of the run (which you must poll from the ExecutorService, you would call ExecutorService.submit and get back a Future holding the future result, but that's another story for another time.
import java.io.Serializable ;
import java.util.concurrent.ExecutorService ;
import java.util.concurrent.Executors ;
public class NotesBackgroundManager implements Serializable {
private static final long serialVersionUID = 1L ;
// # of threads running concurrently
private static final int THREADPOOLSIZE = 10 ;
private final ExecutorService service = Executors. newFixedThreadPool (THREADPOOLSIZE ) ;
public CSIApplication ( ) {
// For use in managed beans
}
public void submitService ( final Runnable taskDef ) {
if (taskDef == null ) {
System. err. println ( "submitService: NULL callable submitted to submitService" ) ;
return ;
}
// Execute runs without return
this. service. execute (taskDef ) ;
}
@ Override
protected void finalize ( ) throws Throwable {
if ( ( this. service != null ) && ! this. service. isTerminated ( ) ) {
this. service. shutdown ( ) ;
}
super. finalize ( ) ;
}
}
The final missing piece is to define
NotesBackgroundManager
as managed bean e.g. background
and you can launch a background task in SSJS: background.submitService(new com.notessensei.demo.SampleNotesBackgroundTask());
As usual YMMV.
Posted by Stephan H Wissel on 23 July 2013 | Comments (7) | categories: XPages