Bug 197167 - [persistence][api] Need callback API to know when RSE model is fully restored
Summary: [persistence][api] Need callback API to know when RSE model is fully restored
Status: RESOLVED FIXED
Alias: None
Product: Target Management
Classification: Tools
Component: RSE (show other bugs)
Version: 2.0   Edit
Hardware: All All
: P2 enhancement (vote)
Target Milestone: 3.0 M6   Edit
Assignee: David Dykstal CLA
QA Contact: Martin Oberhuber CLA
URL:
Whiteboard:
Keywords: api
: 220320 (view as bug list)
Depends on: 177332 197025
Blocks: 233048 220320 222376
  Show dependency tree
 
Reported: 2007-07-19 12:28 EDT by Martin Oberhuber CLA
Modified: 2008-05-20 15:45 EDT (History)
1 user (show)

See Also:
mober.at+eclipse: review+


Attachments
implemenation of initialization and notification for InitRSEJob (27.06 KB, patch)
2008-03-03 21:58 EST, David Dykstal CLA
no flags Details | Diff
mylyn/context/zip (11.54 KB, application/octet-stream)
2008-03-03 21:58 EST, David Dykstal CLA
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Martin Oberhuber CLA 2007-07-19 12:28:42 EDT
As an extension of the API introduced through bug #177332, I'd like to see an API for registering a callback to know when the RSE model is fully restored.

We need such a callback in IRSEPersistenceManager, but also need to rewrite the InitRSEJob such that
  1.) Clients can register early-startup-calls to be performed when the RSE 
      model is initialized for the first time; such client initialization 
      handlers should be chained, to ensure that the final "everything 
      complete" flag is only set once really all clients are complete.
      An extension point is likely needed for this.
  2.) Clients can know when the RSEUIPlugin$InitRSEJob is done.
  3.) It may be necessary to move the whole model initialization (which is
      currently done by RSEUIPlugin$InitRSEJob) from UI into Core, to ensure
      that headless applications also get a fully initialized RSE model as 
      needed. Actually, the extension point for clients to contribute should
      probably have a flag to indicate whether intialization is
        - to be done in UI mode only or also in headless mode
        - to be done on first workspace creation only or whenever RSE starts
  
Once the callback API is in place, the code for deferred initialization of the SystemView (bug #197025) should be changed to make use of the callbacks.
Comment 1 Martin Oberhuber CLA 2007-08-07 08:53:51 EDT
For (2) - wait until InitRSEJob is complete - clients can already use the following code with TM / RSE 2.0:

Job[] jobs = Job.getJobManager().find(null);
for(int i=0; i<jobs.length; i++) {
    if ("Initialize RSE".equals(jobs[i].getName())) { //$NON-NLS-1$
        jobs[i].join();
        break;
    }
}

We are now using this method in our unit tests (bug #198956), as well as SystemView#createPartControl (bug #197025). Clients are encouraged to do the same (bug #194802).
Comment 2 Martin Oberhuber CLA 2008-02-26 07:29:44 EST
*** Bug 220320 has been marked as a duplicate of this bug. ***
Comment 3 Martin Oberhuber CLA 2008-02-26 07:31:45 EST
Note that as per bug 220320, just doing Job.join() may not be sufficient in some cases where a plugin tries to wait for RSE Initialization extermely early. In this case, a JobChangeListener needs to be registered such that only after the Job changes from WAITING to RUNNING we join.

Scheduling for 3.0M6 since this is new API.
Comment 4 David Dykstal CLA 2008-02-27 15:28:25 EST
1.) Clients can register early-startup-calls to be performed when the RSE 
      model is initialized for the first time; such client initialization 
      handlers should be chained, to ensure that the final "everything 
      complete" flag is only set once really all clients are complete.
      An extension point is likely needed for this.
      
I'm not sure I understand what this might be used for. It sounds like you want to register two kinds of listeners - ones that are declared in an extension point and are called at the end of initialization but prior to saying "init complete" and another set that gets called after "init complete" is set. This latter set would probably be registered by the prior set. Am I understanding this correctly? I suspect there should be rules about what kinds of work can be done in the prior set - e.g. no further jobs started.
Comment 5 Martin Oberhuber CLA 2008-02-28 15:27:02 EST
No, I'm talking about two different kinds:

(a) Consider the "Local" node. It's auto-created by default on an empty 
    workspace when RSE is started the first time. Other contributors might
    also want to auto-create some connections but only the first time, and
    especially "lazily" i.e. only when the RSE is initialized by some user 
    action.
    This is what you call the "first set": initialization contributors.
    I'd think that a contributor can do whatever it wants, provided that
    it tells RSE somehow when it's finished.

(b) Initialization-complete-listeners. Kinds of clients that want to start
    their work only when they know for sure that the model has been initialized
    properly. For instance, one might want to enable an "Add Host" button 
    only when it's known that all systemTypes have been initialized by their
    dynamic providers and the model has been read in completely.
Comment 6 David Dykstal CLA 2008-02-29 11:17:11 EST
Regarding (a)

It seems that each "initializer" should provide its own indication of when it is done. This will allow it to structure itself any way it wishes. If the initializer is running synchronously with the init job then it will mark itself as complete before we begin the next initializer. If the initializer is running asynchronous work, it will not t be marked complete. Therefore at the end of the init job we will have to run polling loop to determine initializer completeness. I assume there should be a timeout mechanism for this, any suggestions for its design?

So the mechanism looks something like this:

// run the initializers
for each initializer {
	initializer.run();
	if (!initializer.isComplete()) {
		incompleteSet.add(initializer);
	}
}
// poll the initializers
while (!incompleteSet.isEmpty())
	for each initializer in the incompleteSet {
		if (initializer.isComplete()) remove it from the set // there are ways to do this safely
	}
	sleep for a bit
	if (timeout time as arrived) break
}
// call back to to the listeners for initialization complete.
...

	
Comment 7 Martin Oberhuber CLA 2008-02-29 12:09:43 EST
I don't think it is a good idea to let initializers run in parallel. They potentially modify the RSE model, and could make it inconsistent when they try doing so at the same time.

I'd prefer forcing initializers to run sequentially only. Next initializer in the chain will only be started when previous initializer has signaled completion. When the last initializer in the chain has signaled completion, all of RSE is considered complete.

I also don't think that introducing a timeout is a good idea. What would a proper value be? - In our commercial product, we have seen initialization of Makefile Fragments taking up to the range of 10 minutes. The build system was not usable while these were generated, but it happened only the first time after installation, and it happened in a background job. 

There is no problem with initializer jobs taking long as long as they eventually complete. It's the fault of the initializer and not the fault of the framework if it's slow. So a product shipping plugins with slow initializers is responsible for the integration testing and ensuring that initializers eventually complete.
Comment 8 David Dykstal CLA 2008-02-29 13:49:17 EST
Good point. It simplifies the code a bit too. So the code looks something like this. We should of course handle exceptions and use a progress monitor so they can report progress.

restore the model from its persistent form
// run the initializers, these may obtain and supplement the model using API
for each initializer {
	monitor =  a subprogressmonitor of the init job's monitor
	try {
		initializer.run(monitor);
		while (!initializer.isComplete()) {
			wait for a bit
		}
	} catch (Throwable t) {
		log t
	}
	monitor.done(); // just in case the initializer didn't do this, multiple done calls are allowed
}
// call back to to the listeners indicating initialization complete.
Comment 9 David Dykstal CLA 2008-03-03 21:56:53 EST
I've implemented the initializer mechanism and placed it in a patch. I've also moved the InitRSEJob into core and used the initializers to create the Local connection as part of the UI initialization. Core will now initialize without it.

There is a new method InitRSEJob.waitForCompletion() that will perform essentially the same incantation we've been telling folks to use for initialization. If users wish to be notified in a callback form they can use a JobChangeListener and monitor for the RUNNING->NONE transition.

The API and the extension point are now fully documented. Previous API should work as described although the package of InitRSEJob has changed.

A patch will be attached for a quick review before committing.
Comment 10 David Dykstal CLA 2008-03-03 21:58:53 EST
Created attachment 91472 [details]
implemenation of initialization and notification for InitRSEJob

implements a new waitForCompletion and allows initializers to be called as part of initializing RSE
Comment 11 David Dykstal CLA 2008-03-03 21:58:55 EST
Created attachment 91473 [details]
mylyn/context/zip
Comment 12 David Dykstal CLA 2008-03-04 10:08:53 EST
API Summary

org.eclipse.rse.core.InitRSEJob
has been moved as an internal class in RSEUIPlugin to a top level class in org.eclipse.rse.core. This is a new class and is now publicly available API. New methods are
static getInstance() - returns the job itself
static waitForCompletion() - causes the executing thread to wait for completion of the singleton job
isComplete() - returns a boolean indicating completion of this job (but not its completion status)
The "run()" method, while public is not client API but for use by the platform's job manager.

org.eclipse.rse.core.Messages
not really public, just strings for InitRSEJob.

new extension point
org.eclipse.rse.core.modelInitializers

So while this is new API, there are no breaking API changes.
Comment 13 David Dykstal CLA 2008-03-04 10:12:03 EST
Martin -- can you give this a quick look. I think it satisfies the intent of the request and gives Ankit what he desires as well. -- Dave
Comment 14 Martin Oberhuber CLA 2008-03-04 10:16:17 EST
I don't think it's a good idea to make the InitRSEJob public as a whole. That's exposing an implementation detail that doesn't need to be exposed.

I'd prefer having public API methods only for
  RSECorePlugin.isInitComplete();
  RSECorePlugin.waitForInitCompletion() throws InterruptedException;
and, optionally,
  RSECorePlugin.addInitCompleteListener();

Also, regarding the Messages, there is no such thing as "not really public". If public then we need to maintain it in the future. Otherwise, better move to internal or create a new class in internal.
Comment 15 Martin Oberhuber CLA 2008-03-04 10:18:37 EST
We might also want to add API that allows clients to wait for certain phases of initialization to be complete. For instance, a client could want to wait till the basic model and SystemRegistry are initialized, but not be interested in additional initialization steps from extenders.
Comment 16 David Dykstal CLA 2008-03-04 10:24:10 EST
 (In reply to comment #14)
> I don't think it's a good idea to make the InitRSEJob public as a whole. That's
> exposing an implementation detail that doesn't need to be exposed.
> 
> I'd prefer having public API methods only for
> RSECorePlugin.isInitComplete();
> RSECorePlugin.waitForInitCompletion() throws InterruptedException;
> and, optionally,
> RSECorePlugin.addInitCompleteListener();
> 
> Also, regarding the Messages, there is no such thing as "not really public". If
> public then we need to maintain it in the future. Otherwise, better move to
> internal or create a new class in internal.

Fair enough. I see the point. Thanks.
Comment 17 David Dykstal CLA 2008-03-04 10:26:10 EST
 (In reply to comment #15)
> We might also want to add API that allows clients to wait for certain phases of
> initialization to be complete. For instance, a client could want to wait till
> the basic model and SystemRegistry are initialized, but not be interested in
> additional initialization steps from extenders.

This is something I'd rather hold for later. Without a specific use case I'm not sure we would guess the right points.
Comment 18 Martin Oberhuber CLA 2008-03-04 10:41:23 EST
(In reply to comment #17)
> This is something I'd rather hold for later. Without a specific use case I'm
> not sure we would guess the right points.

My point is that even if we don't support multiple phases from the beginning, we could design the API to account for those in the future:

public boolean isInitComplete(int init_phase);
public static final int INIT_PHASE_MODEL = 1;
public static final int INIT_PHASE_ALL = 2;

More int constants can be added to the API in a binary compatible manner later.

Comment 19 David Dykstal CLA 2008-03-04 22:15:41 EST
API Summary

org.eclipse.rse.core.RSECorePlugin
static waitForCompletion() - causes the executing thread to wait for completion of the initialization job
static isComplete(int phase) - returns a boolean indicating completion of a particular phase of the initialization job
static addInitListener(IRSEInitListener listener) - adds a listener to the set of listeners for init phase change events
static removeInitListener(IRSEInitListener listener) - removes a listener from the set of listeners for init phase changes
constants added for phases:
static int INIT_ALL = 999
static int INIT_MODEL = 1

org.eclipse.rse.core.IRSEInitListener - defines the interface for listening to init phase changes

This is new API, there are no breaking API changes.
Comment 20 David Dykstal CLA 2008-03-04 22:27:57 EST
*** Bug 220320 has been marked as a duplicate of this bug. ***
Comment 21 Martin Oberhuber CLA 2008-03-05 06:06:14 EST
The change looks great except for one thing:

Because the RSEUIPlugin declares an InitRSE Extension, the RSEUIPlugin will be activated as soon as RSEcorePlugin is activated. I'm not sure if this is really what we want.

Either we want to have the "Local" host added to the model even if UI is not loaded; then the initializer should be in RSE.core. Or, we want to wait with adding the "Local" host until UI is loaded - then it should not be declared as a modelInitializer extension, but a Job for adding the Local host should simply be scheduled when rse.ui initializes.

I think that as a rule of thumb, model initializers should typically be non-UI because that's their main benefit -- they can initialize the model even before UI is activated. In fact they are very similar to the AbstractPreferenceInitializer added with Eclipse 3.0, see
http://help.eclipse.org/help33/topic/org.eclipse.platform.doc.isv/reference/extension-points/org_eclipse_core_runtime_preferences.html

We should perhaps review that code once again and ensure that ours is working similar.

I'm not re-opening the bug though since the desired API has been added and behavior has not become worse compared to the previous situation. Feel free to file a new bug as clone if you don't think you can address it immediately.
Comment 22 Martin Oberhuber CLA 2008-03-05 06:31:34 EST
Checking implementation again, I think that

1.) RSECorePlugin#waitForCompletion() should also have an "int phase"
    parameter
2.) InitRSEJob#isComplete needs to be volatile or protected by synchronized
3.) InitRSEJob#isModelComplete needs to be volatile or protected by synchronized
4.) InitRSEJob#waitForCompletion should not Thread.sleep() but install a
    Job Change Listener in a nested class and wait() on status change of 
    that class -- I hate Polling, and 10 seconds is way too long IMHO
5.) I don't think an InterruptedException on the waitForCompletion should
    be logged as an error. It's normal behavior when users starts up Eclipse
    and chooses to exit Eclipse before everything gets a chance to get
    initialized.
6.) In InitRSEJob#run() you're setting isComplete == true even if the
    Job was canceled by the Monitor... I don't think this is a good idea!
    I think that when the Job is canceled, all waiting Threads should
    throw an InterruptedException.
7.) The initializers are missing a feature: Just like we do for the "Local" 
    host, initializers should have a "run-once-per-workspace-only" capability
    in their extension. When that is set TRUE, the InitRSEJob should use a
    mark file in the workspace metadata to see if the initializer has ever
    been run before. If yes, it should not even instantiate the initializer
    --> saves instantiating plugins if not necessary.
    Somehow I also think you should instantiate/run each initializer in 
    turn rather than first instantiating them all then running them all.
8.) notifyListeners: Why use ArrayList myListeners? A simple array would
    be good enough here:

    IRSEInitListener[] myListeners;
    synchronized (listeners) {
        myListeners = (IRSEInitListener[])listeners.toArray(
                       new IRSEInitListener[listeners.size()]);
    }
    for (int i=0; i<myListeners.length; i++) {
        try {
            myListeners[i].phaseComplete(phase);
        } catch (RuntimeException e) {
            logger.logError(...);
        }
    }

9.) In getInstance() it's safer to do it like in ArchiveHandlerManager --
    your lazy init with null check can actually lead to TWO instances
    which cannot happen in the other approach, and thanks to the 
    Classloader the other approach is lazy as well.
    You don't need the initialize-on-demand holder class idiom here
    since you have no other static methods in InitRSEJob:
    http://www.theserverside.com/discussions/thread.tss?thread_id=35349

Reopening now because issues (2), (3), (6) are critical logic problems and
issue (4) may pose unnecessary performance limitation.
Comment 23 Martin Oberhuber CLA 2008-03-05 06:32:55 EST
And another one:

10) Why use a HashSet for the listeners? There is no need for random access
    to the listeners. HashSet needs a lot more memory than a simple ArrayList
    which should be good enough here.
Comment 24 David Dykstal CLA 2008-03-05 10:10:42 EST
I'm not sure I understand #6 of your list. The signature for run() does not allow throwing an InterruptedException and the contract calls for a cancel status to be returned in this case.
Comment 25 Martin Oberhuber CLA 2008-03-05 10:20:21 EST
I thought something along these lines:

boolean isCanceled = false;
public void run() {
   /*...*/
   if(status.isCanceled()) {
       isCanceled = true;
       anyWaiters.notifyAll();
   }
}

public boolean isComplete(int phase) {
   /*...*/
   if(isCanceled) return false;
}

public void waitForCompleteion() {
   while(!isComplete(phase) && !isCanceled) {
      /*...*/
   }
}
Comment 26 David Dykstal CLA 2008-03-06 07:20:25 EST
API Summary

org.eclipse.rse.core.RSECorePlugin
static waitForInitCompletion() - causes the executing thread to wait for completion of the initialization job
static waitForInitCompletion(int phase) - causes the executing thread to wait for the completion of a particular phase of initialization
static isInitComplete(int phase) - returns a boolean indicating completion of a particular phase of the initialization job
static addInitListener(IRSEInitListener listener) - adds a listener to the set of listeners for init phase change events
static removeInitListener(IRSEInitListener listener) - removes a listener from the set of listeners for init phase changes
constants added for phases:
static int INIT_ALL = 0
static int INIT_MODEL = 1
static int INIT_INITIALIZER = 2

org.eclipse.rse.core.IRSEInitListener - defines the interface for listening to init phase changes (changed slightly from the previous commit)
Comment 27 David Dykstal CLA 2008-03-06 07:23:46 EST
I've updated and reorganized the code according to Martin's suggestions. I've also removed the "isComplete" predicate on an initializer as it was serving no purpose other than controlling a poll with the potential for an initialization lockup for ill-behaved initializers. Initializers now simply run and return. If they choose do fork asynch work they may do so, but it is not up to the initialization framework to control synchronization with that work.
Comment 28 David Dykstal CLA 2008-03-06 07:26:18 EST
Ankit -- 

in the simple case you can just 
RSECorePlugin.waitForInitCompletion();

See the RSEUIInitJob for an example of its use in an asych context kicked off from the plugin startup code.
Comment 29 Martin Oberhuber CLA 2008-03-06 12:55:36 EST
Looks very good now. The one problem I'm seeing now is that you call 
    notifyListeners()
from inside a synchronized block:
    public synchronized void RSEInitJob$Phase#done()

This is an "open call" and very dangerous because the lock is being kept held on RSEInitJob$Phase so nobody else can enter it's corresponding monitor until the contributed, unknown method returns from notification.

Deadlock could occur if the client being notified, as part of notification, switches to another thread (e.g. Display.syncExec()) and after that Thread switch queries completeness of a phase via RSECorePlugin.isInitializationComplete().

Since this is critical, I'm reopening once again to get it fixed.
Comment 30 David Dykstal CLA 2008-03-06 13:58:16 EST
You are absolutely correct. I should have caught that. Thanks.
Now fixed.