Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: f21642fbdfec68946d1fdb60d4971643f7779c5e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.internal.core;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
import org.eclipse.team.core.TeamException;

/**
 * This class provides the infrastructure for processing/dispatching of events using a
 * background job. This is useful in the following situations.
 * <ul>
 * <li>an operation is potentially long running but a responsive UI is desired
 * while the operation is being performed. To do this incoming events are processed
 * and resulting outgoing events are queued and then dispatched at an appropriate time,
 * thus batching UI updates.</li>
 * <li>a change is a POST_CHANGE delta requires further modifications to the workspace
 * which cannot be performed in the delta handler because the workspace is locked.</li>
 * <li>a data structure is not thread safe and requires serialized operations.<li>
 * </ul>
 * </p>
 * <p>
 * The event handler has the following characteristics:
 * <ol>
 * <li>Incoming events are placed in an incoming queue.</li>
 * <li>Each event is processed by calling the <code>processEvent</code> method
 * which is implemented by the subclass. The implementation may choose to process events
 * directly or queue events on an outgoing event queue</li>
 * <li>The <code>doDispatchEvents</code> method of the subclass is called at certain intervals
 * to give the subclass a chance to dispatch the events in it's outgoing queue. The interval between
 * the first 3 dispatches will be the <code>shortDispatchDelay</code> and subsequent intervals will be
 * the <code>longDispatchDelay</code>. This is done to avoid constantly hammering the UI for long running
 * operations.<li>
 * <li>Errors that occur during event processing or dispatch can be accumulated by calling the <code>handle</code>
 * method. Accumulated errors are used to form the status that is returned when the job completes.<li>
 * </ul>
 * </p>
 *
 * @since 3.0
 */
public abstract class BackgroundEventHandler {

	/**
	 * Event type constant used to identify a runnable event
	 */
	public static final int RUNNABLE_EVENT = 1000;

	// Events that need to be processed
	private List<Event> awaitingProcessing = new ArrayList<>();

	// The job that runs when events need to be processed
	private Job eventHandlerJob;

	// Indicate if the event handler has been shutdown
	private boolean shutdown;

	// Accumulate exceptions that occur
	private ExceptionCollector errors;

	// time the last dispatch occurred
	private long timeOfLastDispatch = 0L;

	// the number of dispatches that have occurred since the job started
	private int dispatchCount;

	// time between event dispatches
	private static final long DISPATCH_DELAY = 1500;

	// time between dispatches if the dispatch threshold has been exceeded
	private static final long LONG_DISPATCH_DELAY = 10000;

	// the number of dispatches that can occur before using the long delay
	private static final int DISPATCH_THRESHOLD = 3;

	// time to wait for messages to be queued
	private static final long WAIT_DELAY = 100;

	private String jobName;

	/**
	 * General event class. The type is specific to subclasses.
	 */
	public static class Event {
	    private int type;
		public Event(int type) {
			this.type = type;
		}
		public int getType() {
			return type;
		}
		@Override
		public String toString() {
			StringBuilder buffer = new StringBuilder();
			buffer.append("Background Event: "); //$NON-NLS-1$
			buffer.append(getTypeString());
			return buffer.toString();
		}
		public IResource getResource() {
		    return null;
		}
		protected String getTypeString() {
			return String.valueOf(type);
		}
	}

	/**
	 * Resource event class. The type is specific to subclasses.
	 */
	public static class ResourceEvent extends Event {
		private IResource resource;
		private int depth;
		public ResourceEvent(IResource resource, int type, int depth) {
		    super(type);
			this.resource = resource;
			this.depth = depth;
		}
		public int getDepth() {
			return depth;
		}
		@Override
		public IResource getResource() {
			return resource;
		}
		@Override
		public String toString() {
			StringBuilder buffer = new StringBuilder();
			buffer.append("resource: "); //$NON-NLS-1$
			buffer.append(resource.getFullPath());
			buffer.append(" type: "); //$NON-NLS-1$
			buffer.append(getTypeString());
			buffer.append(" depth: "); //$NON-NLS-1$
			buffer.append(getDepthString());
			return buffer.toString();
		}
		protected String getDepthString() {
			switch (depth) {
				case IResource.DEPTH_ZERO :
					return "DEPTH_ZERO"; //$NON-NLS-1$
				case IResource.DEPTH_ONE :
					return "DEPTH_ONE"; //$NON-NLS-1$
				case IResource.DEPTH_INFINITE :
					return "DEPTH_INFINITE"; //$NON-NLS-1$
				default :
					return "INVALID"; //$NON-NLS-1$
			}
		}
	}

	/**
	 * This is a special event used to run some work in the background.
	 * The preemptive flag is used to indicate that the runnable should take
	 * the highest priority and thus be placed on the front of the queue
	 * and be processed as soon as possible, preempting any event that is currently
	 * being processed. The current event will continue processing once the
	 * high priority event has been processed
	 */
	public static class RunnableEvent extends Event {
		private IWorkspaceRunnable runnable;
		private boolean preemtive;
		public RunnableEvent(IWorkspaceRunnable runnable, boolean preemtive) {
			super(RUNNABLE_EVENT);
			this.runnable = runnable;
			this.preemtive = preemtive;
		}
		public void run(IProgressMonitor monitor) throws CoreException {
			runnable.run(monitor);
		}
		public boolean isPreemtive() {
			return preemtive;
		}
	}

	protected BackgroundEventHandler(String jobName, String errorTitle) {
		this.jobName = jobName;
		errors =
			new ExceptionCollector(
				errorTitle,
				TeamPlugin.ID,
				IStatus.ERROR,
				null /* don't log */
		);
		createEventHandlingJob();
		schedule();
	}

	/**
	 * Create the job used for processing the events in the queue. The job stops working when
	 * the queue is empty.
	 */
	protected void createEventHandlingJob() {
		eventHandlerJob = new Job(getName()) {
			@Override
			public IStatus run(IProgressMonitor monitor) {
				return processEvents(monitor);
			}
			@Override
			public boolean shouldRun() {
				return ! isQueueEmpty();
			}
			@Override
			public boolean shouldSchedule() {
				return ! isQueueEmpty();
			}
			@Override
			public boolean belongsTo(Object family) {
				return BackgroundEventHandler.this.belongsTo(family);
			}
		};
		eventHandlerJob.addJobChangeListener(new JobChangeAdapter() {
			@Override
			public void done(IJobChangeEvent event) {
				jobDone(event);
			}
		});
		eventHandlerJob.setSystem(true);
		eventHandlerJob.setPriority(Job.SHORT);
	}

	/**
	 * Return whether this background handler belongs to the given job family.
	 * @param family the job family
	 * @return whether this background handler belongs to the given job family.
	 * @see Job#belongsTo(Object)
	 */
	protected boolean belongsTo(Object family) {
		return getJobFamiliy() == family;
	}

	/**
	 * Return the family that the background job for this
	 * event handler belongs to.
     * @return the family that the background job for this
	 * event handler belongs to
     */
    protected Object getJobFamiliy() {
        return null;
    }

    /**
	 * This method is invoked when the processing job completes. The
	 * default behavior of the handler is to restart the job if the queue
	 * is no longer empty and to clear the queue if the handler was shut down.
	 */
	protected void jobDone(IJobChangeEvent event) {
		if (isShutdown()) {
			// The handler has been shutdown. Clean up the queue.
			synchronized(this) {
				awaitingProcessing.clear();
			}
		} else if (! isQueueEmpty()) {
			// An event squeaked in as the job was finishing. Reschedule the job.
			schedule();
		}
	}

	/**
	 * Schedule the job to process the events now.
	 */
	protected void schedule() {
		eventHandlerJob.schedule();
	}

	/**
	 * Shutdown the event handler. Any events on the queue will be removed from the queue
	 * and will not be processed.
	 */
	public void shutdown() {
		shutdown = true;
		eventHandlerJob.cancel();
	}

	/**
	 * Returns whether the handle has been shutdown.
	 * @return Returns whether the handle has been shutdown.
	 */
	public boolean isShutdown() {
		return shutdown;
	}

	/**
	 * Queue the event and start the job if it's not already doing work. If the job is
	 * already running then notify in case it was waiting.
	 * @param event the event to be queued
	 */
	protected synchronized void queueEvent(Event event, boolean front) {
		if (Policy.DEBUG_BACKGROUND_EVENTS) {
			System.out.println("Event queued on " + getName() + ":" + event.toString()); //$NON-NLS-1$ //$NON-NLS-2$
		}
		if (front) {
			awaitingProcessing.add(0, event);
		} else {
			awaitingProcessing.add(event);
		}
		if (!isShutdown() && eventHandlerJob != null) {
			if(eventHandlerJob.getState() == Job.NONE) {
				schedule();
			} else {
				notify();
			}
		}
	}

	/**
	 * Return the name that is to be associated with the background job.
	 * @return the job name
	 */
	protected String getName() {
		return jobName;
	}

	/*
	 * Return the next event that has been queued, removing it from the queue.
	 * @return the next event in the queue
	 */
	protected synchronized Event nextElement() {
		if (isShutdown() || isQueueEmpty()) {
			return null;
		}
		return awaitingProcessing.remove(0);
	}

	protected synchronized Event peek() {
		if (isShutdown() || isQueueEmpty()) {
			return null;
		}
		return awaitingProcessing.get(0);
	}

	/**
	 * Return whether there are unprocessed events on the event queue.
	 * @return whether there are unprocessed events on the queue
	 */
	protected synchronized boolean isQueueEmpty() {
		return awaitingProcessing.isEmpty();
	}

	/**
	 * Process events from the events queue and dispatch results. This method does not
	 * directly check for or handle cancelation of the provided monitor. However,
	 * it does invoke <code>processEvent(Event)</code> which may check for and handle
	 * cancelation by shutting down the receiver.
	 * <p>
	 * The <code>isReadyForDispatch()</code> method is used in conjunction
	 * with the <code>dispatchEvents(IProgressMonitor)</code> to allow
	 * the output of the event handler to be batched in order to avoid
	 * fine grained UI updating.
	 * @param monitor a progress monitor
	 */
	protected IStatus processEvents(IProgressMonitor monitor) {
		errors.clear();
		try {
			// It's hard to know how much work is going to happen
			// since the queue can grow. Use the current queue size as a hint to
			// an infinite progress monitor
			monitor.beginTask(null, IProgressMonitor.UNKNOWN);
			IProgressMonitor subMonitor = Policy.infiniteSubMonitorFor(monitor, 90);
			subMonitor.beginTask(null, 1024);

			Event event;
			timeOfLastDispatch = System.currentTimeMillis();
			dispatchCount = 1;
			while ((event = nextElement()) != null && ! isShutdown()) {
				try {
					processEvent(event, subMonitor);
					if (Policy.DEBUG_BACKGROUND_EVENTS) {
						System.out.println("Event processed on " + getName() + ":" + event.toString()); //$NON-NLS-1$ //$NON-NLS-2$
					}
					if(isReadyForDispatch(true /*wait if queue is empty*/)) {
						dispatchEvents(Policy.subMonitorFor(subMonitor, 1));
					}
				} catch (CoreException e) {
					// handle exception but keep going
					handleException(e);
				}
			}
		} finally {
			monitor.done();
		}
		return errors.getStatus();
	}

	/**
	 * Dispatch any accumulated events by invoking <code>doDispatchEvents</code>
	 * and then rest the dispatch counters.
	 * @param monitor a progress monitor
	 * @throws TeamException
	 */
	protected final void dispatchEvents(IProgressMonitor monitor) throws TeamException {
		if (doDispatchEvents(monitor)) {
			// something was dispatched so adjust dispatch count.
			dispatchCount++;
		}
		timeOfLastDispatch = System.currentTimeMillis();
	}

	/**
	 * Notify clients of processed events. Return <code>true</code> if there
	 * was something to dispatch and false otherwise. This is used to help
	 * control the frequency of dispatches (e.g. if there is a lot of dispatching
	 * going on, the frequency of dispatches may be reduced.
	 * @param monitor a progress monitor
	 */
	protected abstract boolean doDispatchEvents(IProgressMonitor monitor) throws TeamException;

	/**
	 * Returns <code>true</code> if processed events should be dispatched and
	 * <code>false</code> otherwise. Events are dispatched at regular intervals
	 * to avoid fine grain events causing the UI to be too jumpy. Also, if the
	 * events queue is empty we will wait a small amount of time to allow
	 * pending events to be queued. The queueEvent notifies when events are
	 * queued.
	 * @return <code>true</code> if processed events should be dispatched and
	 * <code>false</code> otherwise
	 */
	protected boolean isReadyForDispatch(boolean wait) {
		// Check if the time since the last dispatch is greater than the delay.
		if (isDispatchDelayExceeded())
			return true;

		synchronized(this) {
			// If we have incoming events, process them before dispatching
			if(! isQueueEmpty() || ! wait) {
				return false;
			}
			// There are no incoming events but we want to wait a little before
			// dispatching in case more events come in.
			try {
				wait(getDispatchWaitDelay());
			} catch (InterruptedException e) {
				// just continue
			}
		}
		return isQueueEmpty() || isDispatchDelayExceeded();
	}

	private boolean isDispatchDelayExceeded() {
		long duration = System.currentTimeMillis() - timeOfLastDispatch;
		return ((dispatchCount < DISPATCH_THRESHOLD && duration >= getShortDispatchDelay()) ||
				duration >= getLongDispatchDelay());
	}

	/**
	 * Return the amount of time to wait for more events before dispatching.
	 * @return the amount of time to wait for more events before dispatching.
	 */
	protected long getDispatchWaitDelay() {
		return WAIT_DELAY;
	}

    /**
	 * Return the value that is used to determine how often
	 * the events are dispatched (i.e. how often the UI is
	 * updated) for the first 3 cycles. The default value is 1.5 seconds.
	 * After the first 3 cycles, a longer delay is used
     * @return the dispatch delay used for the first 3 cycles.
     */
    protected long getShortDispatchDelay() {
        return DISPATCH_DELAY;
    }

	/**
	 * Return the value that is used to determine how often
	 * the events are dispatched (i.e. how often the UI is
	 * updated) after the first 3 cycles. The default value is 10 seconds.
     * @return the dispatch delay used after the first 3 cycles.
     */
    protected long getLongDispatchDelay() {
        return LONG_DISPATCH_DELAY;
    }

    /**
	 * Handle the exception by recording it in the errors list.
	 * @param e
	 */
	protected void handleException(CoreException e) {
		errors.handleException(e);
	}

	/**
	 * Process the event in the context of a running background job. Subclasses may
	 * (but are not required to) check the provided monitor for cancelation and shut down the
	 * receiver by invoking the <code>shutdown()</code> method.
	 * <p>
	 * In many cases, a background event handler will translate incoming events into outgoing
	 * events. If this is the case, the handler should accumulate these events in the
	 * <code>proceessEvent</code> method and propagate them from the <code>dispatchEvent</code>
	 * method which is invoked periodically in order to batch outgoing events and avoid
	 * the UI becoming too jumpy.
	 *
	 * @param event the <code>Event</code> to be processed
	 * @param monitor a progress monitor
	 */
	protected abstract void processEvent(Event event, IProgressMonitor monitor) throws CoreException;

	/**
	 * Return the job from which the <code>processedEvent</code> method is invoked.
	 * @return Returns the background event handling job.
	 */
	public Job getEventHandlerJob() {
		return eventHandlerJob;
	}
}

Back to the top