From ed0e1bbc7a160de57de10d4abb60d35aeea90834 Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Fri, 30 Apr 2021 11:12:06 +0200 Subject: [PATCH] Finishing off, tests --- biz.aQute.api/bnd.bnd | 3 +- .../aQute/scheduler/api/CronExpression.java | 18 ++++ .../java/biz/aQute/scheduler/api/CronJob.java | 18 ++-- .../biz/aQute/scheduler/api/Scheduler.java | 55 +++++++--- biz.aQute.scheduler.basic.provider/bnd.bnd | 8 +- .../basic/provider/CentralScheduler.java | 102 +++++++++++++++++- .../basic/provider/CronAdjuster.java | 43 ++------ .../basic/provider/SchedulerImpl.java | 92 +--------------- .../basic/provider/SchedulerBundleTest.java | 82 ++++++++++++++ .../basic/provider/SchedulerImplTest.java | 29 ++++- .../test.bndrun | 11 ++ 11 files changed, 307 insertions(+), 154 deletions(-) create mode 100644 biz.aQute.api/src/main/java/biz/aQute/scheduler/api/CronExpression.java create mode 100644 biz.aQute.scheduler.basic.provider/src/test/java/biz/aQute/scheduler/basic/provider/SchedulerBundleTest.java create mode 100644 biz.aQute.scheduler.basic.provider/test.bndrun diff --git a/biz.aQute.api/bnd.bnd b/biz.aQute.api/bnd.bnd index 89f6e12..759baa9 100644 --- a/biz.aQute.api/bnd.bnd +++ b/biz.aQute.api/bnd.bnd @@ -8,6 +8,7 @@ org.osgi.namespace.implementation,\ org.osgi.annotation.bundle,\ org.osgi.dto,\ - org.apache.felix.http.servlet-api + org.apache.felix.http.servlet-api,\ + org.osgi.service.component.annotations -sub: *.bnd \ No newline at end of file diff --git a/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/CronExpression.java b/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/CronExpression.java new file mode 100644 index 0000000..e6ce78b --- /dev/null +++ b/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/CronExpression.java @@ -0,0 +1,18 @@ +package biz.aQute.scheduler.api; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +/** + * An annotation to simplify using a CronJob + */ +@ComponentPropertyType +public @interface CronExpression { + + /** + * The 'cron.expression' service property + * @return + */ + String cron(); + +} + diff --git a/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/CronJob.java b/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/CronJob.java index 6524560..8f5ca27 100644 --- a/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/CronJob.java +++ b/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/CronJob.java @@ -11,7 +11,7 @@ * chronos. *

* The Unix Cron defines a syntax that is used by the Cron service. A user - * should register a Cron service with the {@link CronJob#CRON} property. The + * should register a Cron service with the {@value CronJob#CRON} property. The * value is according to the {link http://en.wikipedia.org/wiki/Cron}. *

* @@ -59,11 +59,12 @@ * Additionally, you can use some fixed formats: * *

- * @yearly (or @annually)	Run once a year at midnight on the morning of January 1	0 0 1 1 *
- * @monthly	Run once a month at midnight on the morning of the first day of the month	0 0 1 * *
- * @weekly	Run once a week at midnight on Sunday morning	0 0 * * 0
- * @daily	Run once a day at midnight	0 0 * * *
- * @hourly	Run once an hour at the beginning of the hour	0 * * * *
+ *
+ * @yearly (or @annually)	Run once a year at midnight on the morning of January 		0 0 0 1 JAN *
+ * @monthly	Run once a month at midnight on the morning of the first day of the month	0 0 0 1 *   *
+ * @weekly	Run once a week at midnight on Sunday morning	                            0 0 0 * *   7
+ * @daily	Run once a day at midnight													0 0 0 * *   *
+ * @hourly	Run once an hour at the beginning of the hour								0 * * * *   *
  * @reboot	Run at startup	@reboot (at service registration time)
  * 
*

@@ -71,15 +72,14 @@ * Major difference is the day number. In Quartz this is 0-6 for SAT-SUN while * here it is 1-7 for MON-SUN. * - * @param The parameter for the cron job */ public interface CronJob { /** * The service property that specifies the cron schedule. The type is * String+. */ - String CRON = "cron"; - String NAME = "name"; + String CRON = "cron"; + String NAME = "name"; /** * Run a cron job. diff --git a/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/Scheduler.java b/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/Scheduler.java index a23d601..444b3e1 100644 --- a/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/Scheduler.java +++ b/biz.aQute.api/src/main/java/biz/aQute/scheduler/api/Scheduler.java @@ -1,5 +1,6 @@ package biz.aQute.scheduler.api; +import java.time.temporal.TemporalAdjuster; import java.util.concurrent.Callable; import java.util.concurrent.Executor; @@ -16,13 +17,14 @@ interface RunnableWithException { void run() throws Exception; } - /** * Schedule a runnable to run periodically at a fixed rate. The schedule can * be canceled by the returned task. * - * @param name The name of the task - * @param runnable the task to run + * @param name + * The name of the task + * @param runnable + * the task to run */ Task periodic(Runnable runnable, long ms, String name); @@ -30,16 +32,20 @@ interface RunnableWithException { * Schedule a runnable to run after a certain time. The schedule can be * canceled by the returned task if it was not yet canceled. * - * @param name The name of the task - * @param runnable the task to run + * @param name + * The name of the task + * @param runnable + * the task to run */ Task after(Runnable runnable, long ms, String name); /** * Executes a task in the background, intended for short term tasks * - * @param name The name of the task - * @param runnable the task to run + * @param name + * The name of the task + * @param runnable + * the task to run */ Task execute(Runnable runnable, String name); @@ -50,11 +56,13 @@ interface RunnableWithException { Promise submit(Callable callable, String name); /** - * Execute long running task and optionally restart when - * exceptions are thrown. + * Execute long running task and optionally restart when exceptions are + * thrown. * - * @param r The body - * @param manage restart when it fails + * @param r + * The body + * @param manage + * restart when it fails * */ Task deamon(RunnableWithException r, boolean manage, String name); @@ -66,12 +74,31 @@ interface RunnableWithException { * be used to stop scheduling. This variation does not take an environment * object. * - * @param r The Runnable to run - * @param name The name - * @param cronExpression A Cron Expression + * @param r + * The Runnable to run + * @param name + * The name + * @param cronExpression + * A Cron Expression * @return A closeable to terminate the schedule * @throws Exception */ Task schedule(RunnableWithException r, String cronExpression, String name) throws Exception; + /** + * Return a {@link TemporalAdjuster} based on a Cron expression. You can use + * a Temporal Adjust to calculate the time beteween a date time and the next + * trigger point. + * + *

+	 *  TemporalAdjuster cron = this.getCronAdjuster("@hourly");
+	 * 	ZonedDateTime now = ZonedDateTime.now();
+	 * 	ZonedDateTime next = now.with(cron);
+	 * 
+ * + * @param cronExpression a Cron expression as specified in {@link CronJob} + * @return a Temporal Adjuster based on a cron expression + */ + TemporalAdjuster getCronAdjuster(String cronExpression); + } diff --git a/biz.aQute.scheduler.basic.provider/bnd.bnd b/biz.aQute.scheduler.basic.provider/bnd.bnd index 6ade09d..eb6c755 100644 --- a/biz.aQute.scheduler.basic.provider/bnd.bnd +++ b/biz.aQute.scheduler.basic.provider/bnd.bnd @@ -9,11 +9,15 @@ biz.aQute.api.scheduler,\ org.osgi.util.promise,\ aQute.libg - + -testpath: \ biz.aQute.wrapper.junit,\ biz.aQute.wrapper.hamcrest,\ org.assertj.core,\ - org.awaitility + org.awaitility,\ + biz.aQute.launchpad,\ + osgi.core, \ + slf4j.api, \ + slf4j.simple diff --git a/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/CentralScheduler.java b/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/CentralScheduler.java index 01013cb..7c564a2 100644 --- a/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/CentralScheduler.java +++ b/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/CentralScheduler.java @@ -1,27 +1,45 @@ package biz.aQute.scheduler.basic.provider; +import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ServiceScope; import org.osgi.util.promise.Promise; import org.osgi.util.promise.PromiseFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@Component(service = CentralScheduler.class, scope = ServiceScope.SINGLETON) -class CentralScheduler { +import aQute.lib.converter.Converter; +import biz.aQute.scheduler.api.CronJob; +import biz.aQute.scheduler.api.Task; +import biz.aQute.scheduler.basic.provider.SchedulerImpl.TaskImpl; + +@Component(service = CentralScheduler.class, scope = ServiceScope.SINGLETON, immediate = true) +public class CentralScheduler { + final List crons = new ArrayList<>(); final static Logger logger = LoggerFactory.getLogger(SchedulerImpl.class); - final ScheduledExecutorService scheduler ; + final ScheduledExecutorService scheduler; final PromiseFactory factory; - + Clock clock = Clock.systemDefaultZone(); long shutdownTimeout = 5000; + final SchedulerImpl frameworkTasks = new SchedulerImpl(this); @Activate public CentralScheduler() { @@ -31,6 +49,7 @@ public CentralScheduler() { @Deactivate void deactivate() { + frameworkTasks.deactivate(); scheduler.shutdown(); try { if (scheduler.awaitTermination(500, TimeUnit.MILLISECONDS)) @@ -68,4 +87,79 @@ public Promise submit(Callable callable, String name) { }); } + class Cron { + + CronJob target; + Task schedule; + + Cron(CronJob target, String cronExpression, String name) throws Exception { + this.target = target; + this.schedule = frameworkTasks.schedule(target::run, cronExpression, name); + } + + void close() throws IOException { + schedule.cancel(); + } + } + + void schedule(TaskImpl task, CronAdjuster cron, long delay) { + synchronized (task) { + if (task.canceled) { + return; + } + ScheduledFuture schedule = scheduler.schedule(() -> { + task.run(); + schedule(task, cron, nextDelay(cron)); + }, delay, TimeUnit.MILLISECONDS); + task.cancel = () -> schedule.cancel(true); + } + } + + long nextDelay(CronAdjuster cron) { + ZonedDateTime now = ZonedDateTime.now(clock); + ZonedDateTime next = now.with(cron); + long delay = next.toInstant() + .toEpochMilli() - System.currentTimeMillis(); + if (delay < 1) + delay = 1; + return delay; + } + + + + + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + void addSchedule(CronJob s, Map map) throws Exception { + String name = Converter.cnv(String.class, map.get(CronJob.NAME)); + String[] schedules = Converter.cnv(String[].class, map.get(CronJob.CRON)); + if (schedules == null || schedules.length == 0) + return; + + if (name == null) { + name = "unknown " + Instant.now(); + } + + synchronized (crons) { + for (String schedule : schedules) { + try { + Cron cron = new Cron(s, schedule, name); + crons.add(cron); + } catch (Exception e) { + logger.error("Invalid cron expression " + schedule + " from " + map, e); + } + } + } + } + + void removeSchedule(CronJob s) { + synchronized (crons) { + for (Iterator cron = crons.iterator(); cron.hasNext();) { + Cron c = cron.next(); + if (c.target == s) { + cron.remove(); + c.schedule.cancel(); + } + } + } + } } diff --git a/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/CronAdjuster.java b/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/CronAdjuster.java index c4dda9a..7d31c95 100644 --- a/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/CronAdjuster.java +++ b/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/CronAdjuster.java @@ -6,9 +6,6 @@ import java.time.temporal.Temporal; import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalField; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -93,7 +90,6 @@ Temporal isOk(Temporal t) { final Field dayOfWeek; final Field year; final Field fields[]; - final Map map; final boolean reboot; /* @@ -101,10 +97,7 @@ Temporal isOk(Temporal t) { */ public CronAdjuster(String specification) { - String entries[] = specification.split("(\n|\r)+"); - map = doEnv(entries); - - String expression = entries[entries.length - 1].trim(); + String expression = specification.trim(); reboot = expression.equals("@reboot"); @@ -136,30 +129,6 @@ public CronAdjuster(String specification) { }; } - private Map doEnv(String[] entries) { - Map map = new HashMap(); - if (entries.length > 1) { - for (int i = 0; i < entries.length - 1; i++) { - - if (entries[i].startsWith("#") || entries[i].isEmpty()) - continue; - - int n = entries[i].indexOf('='); - if (n >= 0) { - String key = entries[i].substring(0, n) - .trim(); - String value = entries[i].substring(n + 1) - .trim(); - map.put(key, value); - } else { - map.put(entries[i].trim(), Boolean.TRUE.toString()); - } - } - return map; - } else - return Collections.emptyMap(); - } - /** *
 	 * 	@yearly (or @annually)	Run once a year at midnight on the morning of January 1	0 0 1 1 *
@@ -189,6 +158,12 @@ private String preDeclared(String expression) {
 			case "@hourly" :
 				return "4 0 * * * ?";
 
+			case "@minutely" :
+				return "* 0 * * * *";
+
+			case "@secondly" :
+				return "* * * * * *";
+
 			case "@reboot" :
 				return "0 0 0 1 1 ? 1900";
 
@@ -521,10 +496,6 @@ private Checker and(Checker a, Checker b) {
 		return (temporal) -> a.matches(temporal) && b.matches(temporal);
 	}
 
-	public Map getEnv() {
-		return map;
-	}
-
 	public boolean isReboot() {
 		return reboot;
 	}
diff --git a/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/SchedulerImpl.java b/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/SchedulerImpl.java
index c64c10e..fc8362e 100644
--- a/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/SchedulerImpl.java
+++ b/biz.aQute.scheduler.basic.provider/src/main/java/biz/aQute/scheduler/basic/provider/SchedulerImpl.java
@@ -1,15 +1,9 @@
 package biz.aQute.scheduler.basic.provider;
 
-import java.io.IOException;
-import java.time.Clock;
 import java.time.Instant;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
+import java.time.temporal.TemporalAdjuster;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Executor;
@@ -20,16 +14,12 @@
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
-import org.osgi.service.component.annotations.ReferenceCardinality;
-import org.osgi.service.component.annotations.ReferencePolicy;
 import org.osgi.service.component.annotations.ServiceScope;
 import org.osgi.util.promise.Deferred;
 import org.osgi.util.promise.Promise;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import aQute.lib.converter.Converter;
-import biz.aQute.scheduler.api.CronJob;
 import biz.aQute.scheduler.api.Scheduler;
 import biz.aQute.scheduler.api.Task;
 
@@ -39,12 +29,10 @@
  */
 @Component(scope = ServiceScope.PROTOTYPE)
 public class SchedulerImpl implements Executor, Scheduler {
-	final List		crons	= new ArrayList<>();
 	final Logger			logger	= LoggerFactory.getLogger(SchedulerImpl.class);
 	final Set			tasks	= Collections.synchronizedSet(new HashSet<>());
 	final CentralScheduler	scheduler;
 	final Object			lock	= new Object();
-	Clock					clock	= Clock.systemDefaultZone();
 
 	class TaskImpl implements Runnable, Task {
 		final RunnableWithException	runnable;
@@ -197,84 +185,14 @@ public Task schedule(RunnableWithException job, String cronExpression, String na
 		TaskImpl task = new TaskImpl(job, name, false, false);
 		CronAdjuster cron = new CronAdjuster(cronExpression);
 
-		schedule(task, cron, cron.isReboot() ? 1 : nextDelay(cron));
+		scheduler.schedule(task, cron, cron.isReboot() ? 1 : scheduler.nextDelay(cron));
 		tasks.add(task);
 		return task;
 	}
 
-	private void schedule(TaskImpl task, CronAdjuster cron, long delay) {
-		synchronized (task) {
-			if (task.canceled) {
-				return;
-			}
-			ScheduledFuture schedule = scheduler.scheduler.schedule(() -> {
-				System.out.println("tick");
-				task.run();
-				schedule(task, cron, nextDelay(cron));
-			}, delay, TimeUnit.MILLISECONDS);
-			task.cancel = () -> schedule.cancel(true);
-		}
-	}
-
-	private long nextDelay(CronAdjuster cron) {
-		ZonedDateTime now = ZonedDateTime.now(clock);
-		ZonedDateTime next = now.with(cron);
-		long delay = next.toInstant()
-				.toEpochMilli() - System.currentTimeMillis();
-		if (delay < 1)
-			delay = 1;
-		System.out.println("delay " + delay);
-		return delay;
-	}
-
-	class Cron {
-
-		CronJob	target;
-		Task	schedule;
-
-		Cron(CronJob target, String cronExpression, String name) throws Exception {
-			this.target = target;
-			this.schedule = schedule(target::run, cronExpression, name);
-		}
-
-		void close() throws IOException {
-			schedule.cancel();
-		}
-	}
-
-	@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
-	void addSchedule(CronJob s, Map map) throws Exception {
-		String name = Converter.cnv(String.class, map.get(CronJob.NAME));
-		String[] schedules = Converter.cnv(String[].class, map.get(CronJob.CRON));
-		if (schedules == null || schedules.length == 0)
-			return;
-
-		if (name == null) {
-			name = "unknown " + Instant.now();
-		}
-
-		synchronized (crons) {
-			for (String schedule : schedules) {
-				try {
-					Cron cron = new Cron(s, schedule, name);
-					crons.add(cron);
-				} catch (Exception e) {
-					logger.error("Invalid  cron expression " + schedule + " from " + map, e);
-				}
-			}
-		}
-	}
-
-	void removeSchedule(CronJob s) {
-		synchronized (crons) {
-			for (Iterator cron = crons.iterator(); cron.hasNext();) {
-				Cron c = cron.next();
-				if (c.target == s) {
-					cron.remove();
-					c.schedule.cancel();
-				}
-			}
-		}
+	@Override
+	public TemporalAdjuster getCronAdjuster(String cronExpression) {
+		return new CronAdjuster(cronExpression);
 	}
 
 }
diff --git a/biz.aQute.scheduler.basic.provider/src/test/java/biz/aQute/scheduler/basic/provider/SchedulerBundleTest.java b/biz.aQute.scheduler.basic.provider/src/test/java/biz/aQute/scheduler/basic/provider/SchedulerBundleTest.java
new file mode 100644
index 0000000..66d22c1
--- /dev/null
+++ b/biz.aQute.scheduler.basic.provider/src/test/java/biz/aQute/scheduler/basic/provider/SchedulerBundleTest.java
@@ -0,0 +1,82 @@
+package biz.aQute.scheduler.basic.provider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Optional;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.service.component.annotations.Component;
+
+import aQute.launchpad.Launchpad;
+import aQute.launchpad.LaunchpadBuilder;
+import biz.aQute.scheduler.api.CronJob;
+
+public class SchedulerBundleTest {
+	static LaunchpadBuilder	builder	= new LaunchpadBuilder().bndrun("test.bndrun");
+
+	static Semaphore		present	= new Semaphore(0);
+
+	@Component(property = "cron=@reboot")
+	public static class TestRebootService implements CronJob {
+
+		@Override
+		public void run() throws Exception {
+			present.release();
+		}
+
+	}
+
+	@Component(property = "cron=0/1 * * * * *")
+	public static class TestCleanup implements CronJob {
+
+		@Override
+		public void run() throws Exception {
+			present.release();
+		}
+
+	}
+
+	@Test
+	public void testAnnotation() throws Exception {
+		try (Launchpad lp = builder.create()) {
+			present.drainPermits();
+			lp.bundle().addResource(TestRebootService.class).start();
+			Optional service = lp.getService(CronJob.class);
+			assertThat(service).isPresent();
+
+			boolean found = present.tryAcquire(1, 5000, TimeUnit.MILLISECONDS);
+			assertTrue(found);
+		}
+	}
+
+	@Test
+	public void testCleanedup() throws Exception {
+		try (Launchpad lp = builder.create().inject(this)) {
+			present.drainPermits();
+			lp.report();
+
+			Bundle bundle = lp.bundle().addResource(TestCleanup.class).start();
+			Optional service = lp.getService(CronJob.class);
+			assertThat(service).isPresent();
+
+			boolean active = present.tryAcquire(2, 5000, TimeUnit.MILLISECONDS);
+			assertTrue(active);
+
+			bundle.stop();
+
+			service = lp.getService(CronJob.class);
+			assertThat(service).isNotPresent();
+
+			Thread.sleep(1000);
+			present.drainPermits();
+			active = present.tryAcquire(1, 2000, TimeUnit.MILLISECONDS);
+			assertFalse(active);
+
+		}
+	}
+}
diff --git a/biz.aQute.scheduler.basic.provider/src/test/java/biz/aQute/scheduler/basic/provider/SchedulerImplTest.java b/biz.aQute.scheduler.basic.provider/src/test/java/biz/aQute/scheduler/basic/provider/SchedulerImplTest.java
index 25f745e..f2a7b98 100644
--- a/biz.aQute.scheduler.basic.provider/src/test/java/biz/aQute/scheduler/basic/provider/SchedulerImplTest.java
+++ b/biz.aQute.scheduler.basic.provider/src/test/java/biz/aQute/scheduler/basic/provider/SchedulerImplTest.java
@@ -142,7 +142,6 @@ public void testCron() throws Exception {
 
 		Semaphore s = new Semaphore(0);
 		Task schedule = impl.schedule(() -> {
-			System.out.println("release");
 			s.release();
 		}, "0/2 * * * * *", "test1");
 		s.acquire(3);
@@ -151,5 +150,33 @@ public void testCron() throws Exception {
 		assertTrue(diff >= 5 && diff <= 6);
 	}
 
+	@Test
+	public void testCronReboot() throws Exception {
+		long now = System.currentTimeMillis();
+
+		Semaphore s = new Semaphore(0);
+		Task schedule = impl.schedule(() -> {
+			s.release();
+		}, "@reboot", "test1");
+		s.acquire(1);
+		schedule.cancel();
+		long diff = (System.currentTimeMillis() - now + 500) / 1000;
+		assertTrue(diff <= 1);
+	}
+
+	@Test
+	public void testCronSecondly() throws Exception {
+		long now = System.currentTimeMillis();
+
+		Semaphore s = new Semaphore(0);
+		Task schedule = impl.schedule(() -> {
+			s.release();
+		}, "@secondly", "test1");
+		s.acquire(2);
+		schedule.cancel();
+		long diff = (System.currentTimeMillis() - now + 500) / 1000;
+		assertTrue(diff >= 1 && diff <= 3);
+	}
+
 
 }
diff --git a/biz.aQute.scheduler.basic.provider/test.bndrun b/biz.aQute.scheduler.basic.provider/test.bndrun
new file mode 100644
index 0000000..cbcb73d
--- /dev/null
+++ b/biz.aQute.scheduler.basic.provider/test.bndrun
@@ -0,0 +1,11 @@
+-runpath: slf4j.api
+-runrequires: osgi.identity;filter:='(osgi.identity=biz.aQute.scheduler.basic.provider)'
+-runfw: org.apache.felix.framework;version='[6.0.2,6.0.2]'
+-runee: JavaSE-1.8
+-runbundles: \
+	aQute.libg;version='[5.1.0,5.1.1)',\
+	biz.aQute.api.scheduler;version=snapshot,\
+	biz.aQute.scheduler.basic.provider;version=snapshot,\
+	org.apache.felix.scr;version='[2.1.16,2.1.17)',\
+	org.osgi.util.function;version='[1.1.0,1.1.1)',\
+	org.osgi.util.promise;version='[1.1.1,1.1.2)'
\ No newline at end of file