Skip to content

Commit d790b80

Browse files
authored
Merge pull request #595 from rivaldi8/bugfix-591-scheduled-actions-missed-executions2
Fix issues with scheduled exports running too much or not at all Fixes #591, fixes #594
2 parents 6048bd8 + b2e9bf7 commit d790b80

File tree

4 files changed

+95
-34
lines changed

4 files changed

+95
-34
lines changed

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: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,17 @@ 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());
146+
//set the execution count in the object because it will be checked for the next iteration in the calling loop
147+
scheduledAction.setExecutionCount(executionCount); //this call is important, do not remove!!
145148
//update the last run time and execution count
146149
ContentValues contentValues = new ContentValues();
147-
contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_LAST_RUN, System.currentTimeMillis());
148-
contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_EXECUTION_COUNT, executionCount);
150+
contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_LAST_RUN,
151+
scheduledAction.getLastRunTime());
152+
contentValues.put(DatabaseSchema.ScheduledActionEntry.COLUMN_EXECUTION_COUNT,
153+
scheduledAction.getExecutionCount());
149154
db.update(DatabaseSchema.ScheduledActionEntry.TABLE_NAME, contentValues,
150155
DatabaseSchema.ScheduledActionEntry.COLUMN_UID + "=?", new String[]{scheduledAction.getUID()});
151-
152-
//set the execution count in the object because it will be checked for the next iteration in the calling loop
153-
scheduledAction.setExecutionCount(executionCount); //this call is important, do not remove!!
154156
}
155157

156158
/**
@@ -161,26 +163,31 @@ private static void executeScheduledEvent(ScheduledAction scheduledAction, SQLit
161163
* @return Number of times backup is executed. This should either be 1 or 0
162164
*/
163165
private static int executeBackup(ScheduledAction scheduledAction, SQLiteDatabase db) {
164-
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());
175170
try {
176171
//wait for async task to finish before we proceed (we are holding a wake lock)
177172
new ExportAsyncTask(GnuCashApplication.getAppContext(), db).execute(params).get();
178-
scheduledAction.setExecutionCount(++executionCount);
179173
} catch (InterruptedException | ExecutionException e) {
180174
Crashlytics.logException(e);
181175
Log.e(LOG_TAG, e.getMessage());
182176
}
183-
return executionCount;
177+
return 1;
178+
}
179+
180+
private static boolean shouldExecuteScheduledBackup(ScheduledAction scheduledAction) {
181+
long now = System.currentTimeMillis();
182+
long endTime = scheduledAction.getEndTime();
183+
184+
if (endTime > 0 && endTime < now)
185+
return false;
186+
187+
if (scheduledAction.computeNextTimeBasedScheduledExecutionTime() > now)
188+
return false;
189+
190+
return true;
184191
}
185192

186193
/**
@@ -214,7 +221,7 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa
214221

215222
//we may be executing scheduled action significantly after scheduled time (depending on when Android fires the alarm)
216223
//so compute the actual transaction time from pre-known values
217-
long transactionTime = scheduledAction.computeNextScheduledExecutionTime();
224+
long transactionTime = scheduledAction.computeNextCountBasedScheduledExecutionTime();
218225
while (transactionTime <= endTime) {
219226
Transaction recurringTrxn = new Transaction(trxnTemplate, true);
220227
recurringTrxn.setTime(transactionTime);
@@ -224,7 +231,7 @@ private static int executeTransactions(ScheduledAction scheduledAction, SQLiteDa
224231

225232
if (totalPlannedExecutions > 0 && executionCount >= totalPlannedExecutions)
226233
break; //if we hit the total planned executions set, then abort
227-
transactionTime = scheduledAction.computeNextScheduledExecutionTime();
234+
transactionTime = scheduledAction.computeNextCountBasedScheduledExecutionTime();
228235
}
229236

230237
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)