Skip to content

Commit c3eace6

Browse files
authored
feat: Adding the ability to prioritize tasks (#519)
This change introduces the possibility of using a task prioritization mechanism. This mechanism is disabled by default, and can be enabled using the `enablePrioritization()` method. ## Reminders - [x] Added/ran automated tests - [x] Update README and/or examples - [x] Ran `mvn spotless:apply` --- Co-authored-by: Gustav Karlsson <[email protected]>
1 parent 292380a commit c3eace6

File tree

74 files changed

+930
-242
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+930
-242
lines changed

README.md

+49-7
Original file line numberDiff line numberDiff line change
@@ -125,25 +125,32 @@ An instance of a _one-time_ task has a single execution-time some time in the fu
125125
Define a _one-time_ task and start the scheduler:
126126

127127
```java
128-
OneTimeTask<MyTaskData> myAdhocTask = Tasks.oneTime("my-typed-adhoc-task", MyTaskData.class)
128+
TaskDescriptor<MyTaskData> MY_TASK =
129+
TaskDescriptor.of("my-onetime-task", MyTaskData.class);
130+
131+
OneTimeTask<MyTaskData> myTaskImplementation =
132+
Tasks.oneTime(MY_TASK)
129133
.execute((inst, ctx) -> {
130-
System.out.println("Executed! Custom data, Id: " + inst.getData().id);
134+
System.out.println("Executed! Custom data, Id: " + inst.getData().id);
131135
});
132136

133137
final Scheduler scheduler = Scheduler
134-
.create(dataSource, myAdhocTask)
135-
.registerShutdownHook()
136-
.build();
138+
.create(dataSource, myTaskImplementation)
139+
.registerShutdownHook()
140+
.build();
137141

138142
scheduler.start();
139-
140143
```
141144

142145
... and then at some point (at runtime), an execution is scheduled using the `SchedulerClient`:
143146

144147
```java
145148
// Schedule the task for execution a certain time in the future and optionally provide custom data for the execution
146-
scheduler.schedule(myAdhocTask.instance("1045", new MyTaskData(1001L)), Instant.now().plusSeconds(5));
149+
scheduler.schedule(
150+
MY_TASK
151+
.instanceWithId("1045")
152+
.data(new MyTaskData(1001L))
153+
.scheduledTo(Instant.now().plusSeconds(5)));
147154
```
148155

149156
### More examples
@@ -225,6 +232,32 @@ How long the scheduler will wait before interrupting executor-service threads. I
225232
consider if it is possible to instead regularly check `executionContext.getSchedulerState().isShuttingDown()`
226233
in the ExecutionHandler and abort long-running task. Default `30min`.
227234

235+
:gear: `.enablePriority()`<br/>
236+
It is possible to define a priority for executions which determines the order in which due executions
237+
are fetched from the database. An execution with a higher value for priority will run before an
238+
execution with a lower value (technically, the ordering will be `order by priority desc, execution_time asc`).
239+
Consider using priorities in the range 0-32000 as the field is defined as a `SMALLINT`. If you need a larger value,
240+
modify the schema. For now, this feature is **opt-in**, and column `priority` is only needed by users who choose to
241+
enable priority via this config setting.
242+
243+
Set the priority per instance using the `TaskInstance.Builder`:
244+
245+
```java
246+
scheduler.schedule(
247+
MY_TASK
248+
.instance("1")
249+
.priority(100)
250+
.scheduledTo(Instant.now()));
251+
```
252+
253+
**Note:**
254+
* When enabling this feature, make sure you have the new necessary indexes defined. If you
255+
regularly have a state with large amounts of executions both due and future, it might be beneficial
256+
to add an index on `(execution_time asc, priority desc)` (replacing the old `execution_time asc`).
257+
* This feature is not recommended for users of **MySQL** and **MariaDB** below version 8.x,
258+
as they do not support descending indexes.
259+
* Value `null` for priority may be interpreted differently depending on database (low or high).
260+
228261
#### Polling strategy
229262

230263
If you are running >1000 executions/s you might want to use the `lock-and-fetch` polling-strategy for lower overhead
@@ -408,6 +441,7 @@ db-scheduler.table-name=scheduled_tasks
408441
db-scheduler.immediate-execution-enabled=false
409442
db-scheduler.scheduler-name=
410443
db-scheduler.threads=10
444+
db-scheduler.priority-enabled=false
411445

412446
# Ignored if a custom DbSchedulerStarter bean is defined
413447
db-scheduler.delay-startup-until-context-ready=false
@@ -579,6 +613,14 @@ There are a number of users that are using db-scheduler for high throughput use-
579613

580614
See [releases](https://github.com/kagkarlsson/db-scheduler/releases) for release-notes.
581615

616+
**Upgrading to 15.x**
617+
* Priority is a new opt-in feature. To be able to use it, column `priority` and index `priority_execution_time_idx`
618+
must be added to the database schema. See table definitions for
619+
[postgresql](./b-scheduler/src/test/resources/postgresql_tables.sql),
620+
[oracle](./db-scheduler/src/test/resources/oracle_tables.sql) or
621+
[mysql](./db-scheduler/src/test/resources/mysql_tables.sql).
622+
At some point, this column will be made mandatory. This will be made clear in future release/upgrade-notes.
623+
582624
**Upgrading to 8.x**
583625
* Custom Schedules must implement a method `boolean isDeterministic()` to indicate whether they will always produce the same instants or not.
584626

db-scheduler-boot-starter/src/main/java/com/github/kagkarlsson/scheduler/boot/autoconfigure/DbSchedulerAutoConfiguration.java

+4
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ public Scheduler scheduler(DbSchedulerCustomizer customizer, StatsRegistry regis
164164
builder.enableImmediateExecution();
165165
}
166166

167+
if (config.isPriorityEnabled()) {
168+
builder.enablePriority();
169+
}
170+
167171
// Use custom executor service if provided
168172
customizer.executorService().ifPresent(builder::executorService);
169173

db-scheduler-boot-starter/src/main/java/com/github/kagkarlsson/scheduler/boot/config/DbSchedulerProperties.java

+11
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ public class DbSchedulerProperties {
114114
/** Whether or not to log the {@link Throwable} that caused a task to fail. */
115115
private boolean failureLoggerLogStackTrace = SchedulerBuilder.LOG_STACK_TRACE_ON_FAILURE;
116116

117+
/** Whether or executions are ordered by priority */
118+
private boolean priorityEnabled = false;
119+
117120
public boolean isEnabled() {
118121
return enabled;
119122
}
@@ -243,4 +246,12 @@ public boolean isAlwaysPersistTimestampInUtc() {
243246
public void setAlwaysPersistTimestampInUtc(boolean alwaysPersistTimestampInUTC) {
244247
this.alwaysPersistTimestampInUtc = alwaysPersistTimestampInUTC;
245248
}
249+
250+
public boolean isPriorityEnabled() {
251+
return priorityEnabled;
252+
}
253+
254+
public void setPriorityEnabled(boolean priorityEnabled) {
255+
this.priorityEnabled = priorityEnabled;
256+
}
246257
}

db-scheduler-boot-starter/src/test/java/com/github/kagkarlsson/scheduler/boot/autoconfigure/DbSchedulerAutoConfigurationTest.java

+14
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,20 @@ public void it_should_start_when_the_context_is_ready() {
182182
});
183183
}
184184

185+
@Test
186+
public void it_should_enable_priority() {
187+
ctxRunner
188+
.withPropertyValues("db-scheduler.priority-enabled=true")
189+
.run(
190+
(AssertableApplicationContext ctx) -> {
191+
assertThat(ctx).hasSingleBean(DataSource.class);
192+
assertThat(ctx).hasSingleBean(Scheduler.class);
193+
194+
DbSchedulerProperties props = ctx.getBean(DbSchedulerProperties.class);
195+
assertThat(props.isPriorityEnabled()).isTrue();
196+
});
197+
}
198+
185199
@Test
186200
public void it_should_support_custom_starting_strategies() {
187201
ctxRunner

db-scheduler-boot-starter/src/test/resources/schema.sql

+1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ create table if not exists scheduled_tasks (
1010
consecutive_failures INT,
1111
last_heartbeat TIMESTAMP WITH TIME ZONE,
1212
version BIGINT,
13+
priority INT,
1314
PRIMARY KEY (task_name, task_instance)
1415
);

db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerBuilder.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public class SchedulerBuilder {
7474
protected PollingStrategyConfig pollingStrategyConfig = DEFAULT_POLLING_STRATEGY;
7575
protected LogLevel logLevel = DEFAULT_FAILURE_LOG_LEVEL;
7676
protected boolean logStackTrace = LOG_STACK_TRACE_ON_FAILURE;
77+
protected boolean enablePriority = false;
7778
private boolean registerShutdownHook = false;
7879
private int numberOfMissedHeartbeatsBeforeDead = DEFAULT_MISSED_HEARTBEATS_LIMIT;
7980
private boolean alwaysPersistTimestampInUTC = false;
@@ -230,6 +231,11 @@ public SchedulerBuilder registerShutdownHook() {
230231
return this;
231232
}
232233

234+
public SchedulerBuilder enablePriority() {
235+
this.enablePriority = true;
236+
return this;
237+
}
238+
233239
public Scheduler build() {
234240
if (schedulerName == null) {
235241
schedulerName = new SchedulerName.Hostname();
@@ -249,6 +255,7 @@ public Scheduler build() {
249255
taskResolver,
250256
schedulerName,
251257
serializer,
258+
enablePriority,
252259
clock);
253260
final JdbcTaskRepository clientTaskRepository =
254261
new JdbcTaskRepository(
@@ -259,6 +266,7 @@ public Scheduler build() {
259266
taskResolver,
260267
schedulerName,
261268
serializer,
269+
enablePriority,
262270
clock);
263271

264272
ExecutorService candidateExecutorService = executorService;
@@ -287,11 +295,12 @@ public Scheduler build() {
287295
}
288296

289297
LOG.info(
290-
"Creating scheduler with configuration: threads={}, pollInterval={}s, heartbeat={}s enable-immediate-execution={}, table-name={}, name={}",
298+
"Creating scheduler with configuration: threads={}, pollInterval={}s, heartbeat={}s, enable-immediate-execution={}, enable-priority={}, table-name={}, name={}",
291299
executorThreads,
292300
waiter.getWaitDuration().getSeconds(),
293301
heartbeatInterval.getSeconds(),
294302
enableImmediateExecution,
303+
enablePriority,
295304
tableName,
296305
schedulerName.getName());
297306

db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/SchedulerClient.java

+8
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ class Builder {
213213
private Serializer serializer = Serializer.DEFAULT_JAVA_SERIALIZER;
214214
private String tableName = JdbcTaskRepository.DEFAULT_TABLE_NAME;
215215
private JdbcCustomization jdbcCustomization;
216+
private boolean priority = false;
216217

217218
private Builder(DataSource dataSource, List<Task<?>> knownTasks) {
218219
this.dataSource = dataSource;
@@ -237,6 +238,12 @@ public Builder tableName(String tableName) {
237238
return this;
238239
}
239240

241+
/** Will cause getScheduledExecutions(..) to return executions in priority order. */
242+
public Builder enablePriority() {
243+
this.priority = true;
244+
return this;
245+
}
246+
240247
public Builder jdbcCustomization(JdbcCustomization jdbcCustomization) {
241248
this.jdbcCustomization = jdbcCustomization;
242249
return this;
@@ -259,6 +266,7 @@ public SchedulerClient build() {
259266
taskResolver,
260267
new SchedulerClientName(),
261268
serializer,
269+
priority,
262270
clock);
263271

264272
return new StandardSchedulerClient(taskRepository, clock);

db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/jdbc/AutodetectJdbcCustomization.java

+7-6
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ public boolean supportsSingleStatementLockAndFetch() {
132132

133133
@Override
134134
public List<Execution> lockAndFetchSingleStatement(
135-
JdbcTaskRepositoryContext ctx, Instant now, int limit) {
136-
return jdbcCustomization.lockAndFetchSingleStatement(ctx, now, limit);
135+
JdbcTaskRepositoryContext ctx, Instant now, int limit, boolean orderByPriority) {
136+
return jdbcCustomization.lockAndFetchSingleStatement(ctx, now, limit, orderByPriority);
137137
}
138138

139139
@Override
@@ -143,14 +143,15 @@ public boolean supportsGenericLockAndFetch() {
143143

144144
@Override
145145
public String createGenericSelectForUpdateQuery(
146-
String tableName, int limit, String requiredAndCondition) {
146+
String tableName, int limit, String requiredAndCondition, boolean orderByPriority) {
147147
return jdbcCustomization.createGenericSelectForUpdateQuery(
148-
tableName, limit, requiredAndCondition);
148+
tableName, limit, requiredAndCondition, orderByPriority);
149149
}
150150

151151
@Override
152-
public String createSelectDueQuery(String tableName, int limit, String andCondition) {
153-
return jdbcCustomization.createSelectDueQuery(tableName, limit, andCondition);
152+
public String createSelectDueQuery(
153+
String tableName, int limit, String andCondition, boolean orderByPriority) {
154+
return jdbcCustomization.createSelectDueQuery(tableName, limit, andCondition, orderByPriority);
154155
}
155156

156157
@Override

db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/jdbc/DefaultJdbcCustomization.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.TimeZone;
2727

2828
public class DefaultJdbcCustomization implements JdbcCustomization {
29+
2930
public static final Calendar UTC = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"));
3031
private final boolean persistTimestampInUTC;
3132

@@ -88,7 +89,7 @@ public boolean supportsSingleStatementLockAndFetch() {
8889

8990
@Override
9091
public List<Execution> lockAndFetchSingleStatement(
91-
JdbcTaskRepositoryContext ctx, Instant now, int limit) {
92+
JdbcTaskRepositoryContext ctx, Instant now, int limit, boolean orderByPriority) {
9293
throw new UnsupportedOperationException(
9394
"lockAndFetch not supported for " + this.getClass().getName());
9495
}
@@ -100,19 +101,20 @@ public boolean supportsGenericLockAndFetch() {
100101

101102
@Override
102103
public String createGenericSelectForUpdateQuery(
103-
String tableName, int limit, String requiredAndCondition) {
104+
String tableName, int limit, String requiredAndCondition, boolean orderByPriority) {
104105
throw new UnsupportedOperationException(
105106
"method must be implemented when supporting generic lock-and-fetch");
106107
}
107108

108109
@Override
109-
public String createSelectDueQuery(String tableName, int limit, String andCondition) {
110+
public String createSelectDueQuery(
111+
String tableName, int limit, String andCondition, boolean orderByPriority) {
110112
final String explicitLimit = supportsExplicitQueryLimitPart() ? getQueryLimitPart(limit) : "";
111113
return "select * from "
112114
+ tableName
113115
+ " where picked = ? and execution_time <= ? "
114116
+ andCondition
115-
+ " order by execution_time asc "
117+
+ Queries.ansiSqlOrderPart(orderByPriority)
116118
+ explicitLimit;
117119
}
118120

db-scheduler/src/main/java/com/github/kagkarlsson/scheduler/jdbc/JdbcCustomization.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ public interface JdbcCustomization {
3939
boolean supportsSingleStatementLockAndFetch();
4040

4141
List<Execution> lockAndFetchSingleStatement(
42-
JdbcTaskRepositoryContext ctx, Instant now, int limit);
42+
JdbcTaskRepositoryContext ctx, Instant now, int limit, boolean orderByPriority);
4343

4444
boolean supportsGenericLockAndFetch();
4545

4646
String createGenericSelectForUpdateQuery(
47-
String tableName, int limit, String requiredAndCondition);
47+
String tableName, int limit, String requiredAndCondition, boolean orderByPriority);
4848

49-
String createSelectDueQuery(String tableName, int limit, String andCondition);
49+
String createSelectDueQuery(
50+
String tableName, int limit, String andCondition, boolean orderByPriority);
5051
}

0 commit comments

Comments
 (0)