Skip to content

Commit

Permalink
feat: Add possibility for deferred scheduling
Browse files Browse the repository at this point in the history
This feature is a first attempt to solve the problems described in #7:
When methods are auto-scheduled, the Executor and ThreadPools are
eagerly set up while the Injector is being created. If an error occurs
during Injector creation those Threads are left dangling in nirvana and
might prevent the JVM from properly shutting down.

Using @ManuallyStarted now marks a scheduled method as deferred. It will
first be scheduled by the time SchedulingService.startManualScheduling()
is called.
  • Loading branch information
skuzzle committed Jan 19, 2020
1 parent 306a703 commit 4ddebae
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 6 deletions.
21 changes: 21 additions & 0 deletions src/it/java/de/skuzzle/inject/async/ScheduledIT.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.skuzzle.inject.async;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
Expand All @@ -23,10 +24,12 @@
import de.skuzzle.inject.async.schedule.ExceptionHandler;
import de.skuzzle.inject.async.schedule.ExecutionContext;
import de.skuzzle.inject.async.schedule.ScheduledContext;
import de.skuzzle.inject.async.schedule.SchedulingService;
import de.skuzzle.inject.async.schedule.annotation.CronTrigger;
import de.skuzzle.inject.async.schedule.annotation.CronType;
import de.skuzzle.inject.async.schedule.annotation.DelayedTrigger;
import de.skuzzle.inject.async.schedule.annotation.ExecutionScope;
import de.skuzzle.inject.async.schedule.annotation.ManuallyStarted;
import de.skuzzle.inject.async.schedule.annotation.OnError;
import de.skuzzle.inject.async.schedule.annotation.Scheduled;
import de.skuzzle.inject.async.schedule.annotation.ScheduledScope;
Expand All @@ -42,8 +45,10 @@ public class ScheduledIT {
private static volatile CountDownLatch cronLatch = new CountDownLatch(2);
private static volatile CountDownLatch simpleLatch = new CountDownLatch(2);
private static volatile CountDownLatch delayedLatch = new CountDownLatch(2);
private static volatile CountDownLatch manualLatch = new CountDownLatch(2);
private static volatile int counterSimpl;
private static volatile int counterCron;
private static volatile int counterManual;

public static class TypeWithScheduledMethods {

Expand Down Expand Up @@ -113,6 +118,14 @@ public void testCancelCron(ScheduledContext ctx) {
++counterCron;
ctx.cancel(false);
}

@Scheduled
@SimpleTrigger(500)
@ManuallyStarted
public void testManual() {
++counterManual;
manualLatch.countDown();
}
}

public static class SomeClass {
Expand All @@ -136,6 +149,8 @@ public int getCount() {

@Inject
private TestExceptionHandler testExceptionHandler;
@Inject
private SchedulingService schedulingService;

@Before
public void setup() {
Expand Down Expand Up @@ -181,5 +196,11 @@ public void testExecuteMultipleTimes() throws Exception {
assertEquals("cancel might not have worked if counter > 1", 1, counterSimpl);
assertEquals("cancel might not have worked if counter > 1", 1, counterCron);
assertEquals(1, this.testExceptionHandler.getCount());

assertEquals(0, counterManual);
schedulingService.startManualScheduling();
manualLatch.await();
assertTrue(counterManual > 0);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.TypeLiteral;
import com.google.inject.spi.InjectionListener;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;

class SchedulerTypeListener implements TypeListener {

private static final Logger LOG = LoggerFactory.getLogger(SchedulerTypeListener.class);

// synchronized in case the injector is set up asynchronously
private List<Class<?>> scheduleStatics = Collections
.synchronizedList(new ArrayList<>());
private List<Class<?>> scheduleStatics = Collections.synchronizedList(new ArrayList<>());
private volatile boolean injectorReady;

private final SchedulingService schedulingService;
Expand Down Expand Up @@ -48,8 +52,10 @@ private void handleStaticScheduling(Class<?> type) {
// ready. In the first case, we collect the types for later handling in second
// case we can schedule them immediately
if (this.injectorReady) {
LOG.trace("Encountered type {} while Injector was ready", type);
MethodVisitor.forEachStaticMethod(type, this.schedulingService::scheduleStaticMethod);
} else {
LOG.trace("Encountered type {} while Injector was NOT ready", type);
this.scheduleStatics.add(type);
}
}
Expand All @@ -59,9 +65,9 @@ private <I> void handleMemberScheduling(TypeEncounter<I> encounter) {

@Override
public void afterInjection(I injectee) {
final Consumer<Method> action = method -> SchedulerTypeListener.this.schedulingService
.scheduleMemberMethod(method, injectee);
MethodVisitor.forEachMemberMethod(injectee.getClass(), action);
final Consumer<Method> scheduleMemberMethod = method -> schedulingService.scheduleMemberMethod(method,
injectee);
MethodVisitor.forEachMemberMethod(injectee.getClass(), scheduleMemberMethod);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import java.lang.reflect.Method;

import com.google.inject.Injector;

import de.skuzzle.inject.async.schedule.annotation.ManuallyStarted;
import de.skuzzle.inject.async.schedule.annotation.Scheduled;

/**
Expand Down Expand Up @@ -29,4 +32,13 @@ public interface SchedulingService {
* @param method The method to schedule. Must be a static method.
*/
void scheduleStaticMethod(Method method);

/**
* Schedules all encountered methods that are annotated with {@link ManuallyStarted}.
* This method should only be called once during the lifetime of your Guice
* {@link Injector}. Calling it multiple times will have no effect.
*
* @since 2.0.0
*/
void startManualScheduling();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ScheduledExecutorService;

import javax.inject.Provider;
Expand All @@ -21,11 +23,13 @@ class SchedulingServiceImpl implements SchedulingService {

private final Provider<Injector> injector;
private final Provider<TriggerStrategyRegistry> registry;
private final Collection<ManuallyStarted> manuallyStarted;

SchedulingServiceImpl(Provider<Injector> injector,
Provider<TriggerStrategyRegistry> registry) {
this.injector = injector;
this.registry = registry;
this.manuallyStarted = new ArrayList<>();
}

@Override
Expand All @@ -38,6 +42,12 @@ public void scheduleStaticMethod(Method method) {
scheduleMethod(method, null);
}

@Override
public void startManualScheduling() {
manuallyStarted.forEach(ManuallyStarted::scheduleNow);
manuallyStarted.clear();
}

private void scheduleMethod(Method method, Object self) {
if (!method.isAnnotationPresent(Scheduled.class)) {
return;
Expand All @@ -58,7 +68,37 @@ private void scheduleMethod(Method method, Object self) {
final ScheduledContextImpl context = new ScheduledContextImpl(method, self);
final InjectedMethodInvocation invocation = InjectedMethodInvocation.forMethod(method, self, injector.get());
final LockableRunnable runnable = Runnables.createLockedRunnableStack(invocation, context, handler);
strategy.schedule(context, scheduler, runnable);

if (mustStartManually(method)) {
LOG.debug("Method '{}' is marked to be scheduled manually", method);
this.manuallyStarted.add(new ManuallyStarted(context, scheduler, runnable, strategy));
} else {
strategy.schedule(context, scheduler, runnable);
}
}

private boolean mustStartManually(Method method) {
return method.isAnnotationPresent(de.skuzzle.inject.async.schedule.annotation.ManuallyStarted.class);
}

private static class ManuallyStarted {
private final ScheduledContext context;
private final ScheduledExecutorService scheduler;
private final LockableRunnable runnable;
private final TriggerStrategy strategy;

ManuallyStarted(ScheduledContext context, ScheduledExecutorService scheduler, LockableRunnable runnable,
TriggerStrategy strategy) {
this.context = context;
this.scheduler = scheduler;
this.runnable = runnable;
this.strategy = strategy;
}

public void scheduleNow() {
this.strategy.schedule(context, scheduler, runnable);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package de.skuzzle.inject.async.schedule.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import de.skuzzle.inject.async.schedule.SchedulingService;

/**
* Disables auto-scheduling of the annotated method. By default, if a method is annotated
* with {@link Scheduled}, it will be scheduled by the time its type/instance is
* encountered/created by the Guice framework. Using this annotation you can defer the
* actual scheduling until {@link SchedulingService#startManualScheduling()} is called.
*
* @author Simon Taddiken
* @since 2.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ManuallyStarted {

}

0 comments on commit 4ddebae

Please sign in to comment.