Skip to content

Commit a6aa211

Browse files
committed
Fix issues with scheduled exports running too much or not at all.
In one issue, the scheduled exports were being run with every service run, when executions had been missed. In the other, the scheduled exports wouldn't be run due to bug #583 having incremented too much the execution count, yielding a next scheduled time too far in the future. Fixes: #591 #594
1 parent 6048bd8 commit a6aa211

4 files changed

Lines changed: 89 additions & 27 deletions

File tree

app/src/main/java/org/gnucash/android/model/ScheduledAction.java

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -173,35 +173,64 @@ public long getTimeOfLastSchedule(){
173173
}
174174

175175
/**
176-
* Computes the next time that this scheduled action is supposed to be executed
176+
* Computes the next time that this scheduled action is supposed to be
177+
* executed based on the execution count.
178+
*
177179
* <p>This method does not consider the end time, or number of times it should be run.
178-
* It only considers when the next execution would theoretically be due</p>
180+
* It only considers when the next execution would theoretically be due.</p>
181+
*
179182
* @return Next run time in milliseconds
180183
*/
181-
public long computeNextScheduledExecutionTime(){
182-
int multiplier = mRecurrence.getPeriodType().getMultiplier();
183-
//this is the last planned time for the action to occur, not the last run time
184-
long lastActionTime = getTimeOfLastSchedule(); //mStartDate + ((mExecutionCount-1)*getPeriod());
185-
if (lastActionTime < 0){
184+
public long computeNextCountBasedScheduledExecutionTime(){
185+
return computeNextScheduledExecutionTimeStartingAt(getTimeOfLastSchedule());
186+
}
187+
188+
/**
189+
* Computes the next time that this scheduled action is supposed to be
190+
* executed based on the time of the last run.
191+
*
192+
* <p>This method does not consider the end time, or number of times it should be run.
193+
* It only considers when the next execution would theoretically be due.</p>
194+
*
195+
* @return Next run time in milliseconds
196+
*/
197+
public long computeNextTimeBasedScheduledExecutionTime() {
198+
return computeNextScheduledExecutionTimeStartingAt(getLastRunTime());
199+
}
200+
201+
/**
202+
* Computes the next time that this scheduled action is supposed to be
203+
* executed starting at startTime.
204+
*
205+
* <p>This method does not consider the end time, or number of times it should be run.
206+
* It only considers when the next execution would theoretically be due.</p>
207+
*
208+
* @param startTime time in milliseconds to use as start to compute the next schedule.
209+
*
210+
* @return Next run time in milliseconds
211+
*/
212+
private long computeNextScheduledExecutionTimeStartingAt(long startTime) {
213+
if (startTime <= 0){ // has never been run
186214
return mStartDate;
187215
}
188216

189-
LocalDateTime localDate = LocalDateTime.fromDateFields(new Date(lastActionTime));
217+
int multiplier = mRecurrence.getPeriodType().getMultiplier();
218+
LocalDateTime nextScheduledExecution = LocalDateTime.fromDateFields(new Date(startTime));
190219
switch (mRecurrence.getPeriodType()) {
191220
case DAY:
192-
localDate = localDate.plusDays(multiplier);
221+
nextScheduledExecution = nextScheduledExecution.plusDays(multiplier);
193222
break;
194223
case WEEK:
195-
localDate = localDate.plusWeeks(multiplier);
224+
nextScheduledExecution = nextScheduledExecution.plusWeeks(multiplier);
196225
break;
197226
case MONTH:
198-
localDate = localDate.plusMonths(multiplier);
227+
nextScheduledExecution = nextScheduledExecution.plusMonths(multiplier);
199228
break;
200229
case YEAR:
201-
localDate = localDate.plusYears(multiplier);
230+
nextScheduledExecution = nextScheduledExecution.plusYears(multiplier);
202231
break;
203232
}
204-
return localDate.toDate().getTime();
233+
return nextScheduledExecution.toDate().getTime();
205234
}
206235

207236
/**

app/src/main/java/org/gnucash/android/service/ScheduledActionService.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,10 @@ private static void executeScheduledEvent(ScheduledAction scheduledAction, SQLit
142142
//the last run time is computed instead of just using "now" so that if the more than
143143
// one period has been skipped, all intermediate transactions can be created
144144

145+
scheduledAction.setLastRun(System.currentTimeMillis());
145146
//update the last run time and execution count
146147
ContentValues contentValues = new ContentValues();
147-
contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_LAST_RUN, System.currentTimeMillis());
148+
contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_LAST_RUN, scheduledAction.getLastRunTime());
148149
contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_EXECUTION_COUNT, executionCount);
149150
db.update(DatabaseSchema.ScheduledActionEntry.TABLE_NAME, contentValues,
150151
DatabaseSchema.ScheduledActionEntry.COLUMN_UID + "=?", new String[]{scheduledAction.getUID()});
@@ -162,13 +163,7 @@ private static void executeScheduledEvent(ScheduledAction scheduledAction, SQLit
162163
*/
163164
private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase db) {
164165
int executionCount = 0;
165-
long now = System.currentTimeMillis();
166-
long endTime = scheduledAction.getEndTime();
167-
168-
if (endTime > 0 && endTime < now)
169-
return executionCount;
170-
171-
if (scheduledAction.computeNextScheduledExecutionTime() > now)
166+
if (!shouldExecuteScheduledBackup(scheduledAction))
172167
return 0;
173168

174169
ExportParams params = ExportParams.parseCsv(scheduledAction.getTag());
@@ -183,6 +178,19 @@ private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase
183178
return executionCount;
184179
}
185180

181+
private static boolean shouldExecuteScheduledBackup(ScheduledAction scheduledAction) {
182+
long now = System.currentTimeMillis();
183+
long endTime = scheduledAction.getEndTime();
184+
185+
if (endTime > 0 && endTime < now)
186+
return false;
187+
188+
if (scheduledAction.computeNextTimeBasedScheduledExecutionTime() > now)
189+
return false;
190+
191+
return true;
192+
}
193+
186194
/**
187195
* Executes scheduled transactions which are to be added to the database.
188196
* <p>If a schedule was missed, all the intervening transactions will be generated, even if
@@ -214,7 +222,7 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa
214222

215223
//we may be executing scheduled action significantly after scheduled time (depending on when Android fires the alarm)
216224
//so compute the actual transaction time from pre-known values
217-
long transactionTime = scheduledAction.computeNextScheduledExecutionTime();
225+
long transactionTime = scheduledAction.computeNextCountBasedScheduledExecutionTime();
218226
while (transactionTime <= endTime) {
219227
Transaction recurringTrxn = new Transaction(trxnTemplate, true);
220228
recurringTrxn.setTime(transactionTime);
@@ -224,7 +232,7 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa
224232

225233
if (totalPlannedExecutions > 0 && executionCount >= totalPlannedExecutions)
226234
break; //if we hit the total planned executions set, then abort
227-
transactionTime = scheduledAction.computeNextScheduledExecutionTime();
235+
transactionTime = scheduledAction.computeNextCountBasedScheduledExecutionTime();
228236
}
229237

230238
transactionsDbAdapter.bulkAddRecords(transactions, DatabaseAdapter.UpdateMethod.insert);

app/src/test/java/org/gnucash/android/test/unit/model/ScheduledActionTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,11 @@ public void testComputingNextScheduledExecution(){
105105
recurrence.setPeriodStart(new Timestamp(startDate.getMillis()));
106106
scheduledAction.setRecurrence(recurrence);
107107

108-
assertThat(scheduledAction.computeNextScheduledExecutionTime()).isEqualTo(startDate.getMillis());
108+
assertThat(scheduledAction.computeNextCountBasedScheduledExecutionTime()).isEqualTo(startDate.getMillis());
109109

110110
scheduledAction.setExecutionCount(3);
111111
DateTime expectedTime = new DateTime(2016, 2, 15, 12, 0);
112-
assertThat(scheduledAction.computeNextScheduledExecutionTime()).isEqualTo(expectedTime.getMillis());
112+
assertThat(scheduledAction.computeNextCountBasedScheduledExecutionTime()).isEqualTo(expectedTime.getMillis());
113113
}
114114

115115
@Test

app/src/test/java/org/gnucash/android/test/unit/service/ScheduledActionServiceTest.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,12 +280,28 @@ public void recurringTransactions_shouldHaveScheduledActionUID(){
280280
assertThat(transactionsDbAdapter.getRecordsCount()).isZero();
281281
}
282282

283+
/**
284+
* Scheduled backups should run only once.
285+
*
286+
* <p>Backups may have been missed since the last run, but still only
287+
* one should be done.</p>
288+
*
289+
* <p>For example, if we have set up a daily backup, the last one
290+
* was done on Monday and it's Thursday, two backups have been
291+
* missed. Doing the two missed backups plus today's wouldn't be
292+
* useful, so just one should be done.</p>
293+
*
294+
* <p><i>Note</i>: the execution count will include the missed runs
295+
* as computeNextCountBasedScheduledExecutionTime depends on it.</p>
296+
*/
283297
@Test
284298
public void scheduledBackups_shouldRunOnlyOnce(){
285299
ScheduledAction scheduledBackup = new ScheduledAction(ScheduledAction.ActionType.BACKUP);
286-
scheduledBackup.setStartTime(new DateTime(2016, 2, 17, 17, 0).getMillis());
300+
scheduledBackup.setStartTime(LocalDateTime.now()
301+
.minusMonths(4).minusDays(2).toDate().getTime());
287302
scheduledBackup.setRecurrence(PeriodType.MONTH, 1);
288303
scheduledBackup.setExecutionCount(2);
304+
scheduledBackup.setLastRun(LocalDateTime.now().minusMonths(2).toDate().getTime());
289305

290306
ExportParams backupParams = new ExportParams(ExportFormat.XML);
291307
backupParams.setExportTarget(ExportParams.ExportTarget.SD_CARD);
@@ -297,12 +313,20 @@ public void scheduledBackups_shouldRunOnlyOnce(){
297313

298314
List<ScheduledAction> actions = new ArrayList<>();
299315
actions.add(scheduledBackup);
300-
ScheduledActionService.processScheduledActions(actions, mDb);
301316

317+
// Check there's not a backup for each missed run
318+
ScheduledActionService.processScheduledActions(actions, mDb);
302319
assertThat(scheduledBackup.getExecutionCount()).isEqualTo(3);
303320
File[] backupFiles = backupFolder.listFiles();
304321
assertThat(backupFiles).hasSize(1);
305322
assertThat(backupFiles[0]).exists().hasExtension("gnca");
323+
324+
// Check also across service runs
325+
ScheduledActionService.processScheduledActions(actions, mDb);
326+
assertThat(scheduledBackup.getExecutionCount()).isEqualTo(3);
327+
backupFiles = backupFolder.listFiles();
328+
assertThat(backupFiles).hasSize(1);
329+
assertThat(backupFiles[0]).exists().hasExtension("gnca");
306330
}
307331

308332
/**
@@ -315,6 +339,7 @@ public void scheduledBackups_shouldRunOnlyOnce(){
315339
public void scheduledBackups_shouldNotRunBeforeNextScheduledExecution(){
316340
ScheduledAction scheduledBackup = new ScheduledAction(ScheduledAction.ActionType.BACKUP);
317341
scheduledBackup.setStartTime(LocalDateTime.now().minusDays(2).toDate().getTime());
342+
scheduledBackup.setLastRun(scheduledBackup.getStartTime());
318343
scheduledBackup.setExecutionCount(1);
319344
scheduledBackup.setRecurrence(PeriodType.WEEK, 1);
320345

0 commit comments

Comments
 (0)