Skip to content
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,44 @@ A typical set of priorities might include: `Normal` and `Emergency`
This provides a means to configure the sort weight generator that maintains the primary ordering of Queue Entries on
a particular Queue. By default, the `existingValueSortWeightGenerator` will be utilized.

#### queue.autoCloseQueueEntriesAtTime

**Default Value:** None (empty), which disables automatic clearing

**Required?** False

**Description:**
The time of day, in `HH:mm` 24-hour format and the server's local time, at which active queue entries are
automatically ended each day. A scheduled task ends every queue entry that was still active as of this time; entries
started later in the day are left untouched.

This property is blank by default, so no clearing happens until an implementer sets it. An ordinary outpatient clinic
that wants its queues emptied at end of day would set `23:59`. Leave it blank if queue entries are used for anything
that should survive overnight — tracking where inpatients currently are within a multi-day visit, for instance, or a
queue of patients to follow up with over the coming week. Use `queue.autoCloseQueueEntriesForQueues` to enable
clearing for some queues but not others.

The task is registered with the scheduler as `Queue Module - Auto Close Queue Entries` and appears on the Manage
Scheduler page, where its interval can be changed or the task stopped until the next restart. Blanking this
property is what turns the clear off for good.

The task works from the most recent occurrence of the configured time rather than from the moment it happens to run,
so if it does not get a chance to run at that time (a restart or a maintenance window, say) the next run catches up
instead of leaving the queues uncleared until the following day. One consequence: the first run after this property
is set ends anything still open from before the most recent occurrence of the configured time, rather than waiting
for the next one.

#### queue.autoCloseQueueEntriesForQueues

**Default Value:** None (empty)

**Required?** False

**Description:**
A comma-separated list of queue uuids whose entries are automatically ended at the time configured by
`queue.autoCloseQueueEntriesAtTime`. Leave this property blank to clear entries in **all** queues. Unknown uuids are
logged and skipped rather than aborting the task.

### Sort Weight Generators

As described above in Global Property configuration, one can configure the specific algorithm to use to generate and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,67 @@
*/
package org.openmrs.module.queue;

import java.util.Date;

import lombok.extern.slf4j.Slf4j;
import org.openmrs.api.context.Context;
import org.openmrs.module.BaseModuleActivator;
import org.openmrs.module.DaemonToken;
import org.openmrs.module.DaemonTokenAware;
import org.openmrs.module.queue.tasks.QueueTimerTask;
import org.openmrs.module.queue.tasks.AutoCloseQueueEntryTask;
import org.openmrs.module.queue.tasks.AutoCloseVisitQueueEntryTask;
import org.openmrs.scheduler.SchedulerService;
import org.openmrs.scheduler.Task;
import org.openmrs.scheduler.TaskDefinition;

/**
* This class contains the logic that is run every time this module is either started or shutdown
*/
@Slf4j
public class QueueModuleActivator extends BaseModuleActivator implements DaemonTokenAware {
public class QueueModuleActivator extends BaseModuleActivator {

private static final String AUTO_CLOSE_VISIT_QUEUE_ENTRY_TASK = "Queue Module - Auto Close Visit Queue Entries";

private static final String AUTO_CLOSE_QUEUE_ENTRY_TASK = "Queue Module - Auto Close Queue Entries";

private static final long REPEAT_INTERVAL_SECONDS = 60L;

@Override
public void started() {
super.started();
log.info("Queue Module Started");
QueueTimerTask.setEnabled(true);
registerTask(AutoCloseVisitQueueEntryTask.class, AUTO_CLOSE_VISIT_QUEUE_ENTRY_TASK,
"Ends queue entries whose visit has been stopped");
registerTask(AutoCloseQueueEntryTask.class, AUTO_CLOSE_QUEUE_ENTRY_TASK,
"Ends active queue entries at the time of day configured in "
+ QueueModuleConstants.AUTO_CLOSE_QUEUE_ENTRIES_AT_TIME);
}

@Override
public void setDaemonToken(DaemonToken daemonToken) {
QueueTimerTask.setDaemonToken(daemonToken);
/**
* Creates the task definition the first time this module starts, and does nothing thereafter, so
* that an interval change or a stop made from the Manage Scheduler page is left alone. The
* scheduler starts the task at server startup and restores it across a module being started or
* stopped; starting it here covers only the case it cannot, of this module being installed into a
* running server.
*/
private void registerTask(Class<? extends Task> taskClass, String name, String description) {
try {
SchedulerService schedulerService = Context.getSchedulerService();
if (schedulerService.getTaskByName(name) != null) {
log.debug("Scheduled task {} is registered already", name);
return;
}
TaskDefinition taskDefinition = new TaskDefinition();
taskDefinition.setName(name);
taskDefinition.setDescription(description);
taskDefinition.setTaskClass(taskClass.getName());
taskDefinition.setStartTime(new Date());
taskDefinition.setRepeatInterval(REPEAT_INTERVAL_SECONDS);
taskDefinition.setStartOnStartup(true);
schedulerService.saveTaskDefinition(taskDefinition);
schedulerService.scheduleIfNotRunning(taskDefinition);
log.info("Registered scheduled task {}", name);
}
catch (Exception e) {
log.error("Unable to register task {}", name, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ public class QueueModuleConstants {
public final static String QUEUE_SORT_WEIGHT_GENERATOR = "queue.sortWeightGenerator";

public final static String EXISTING_VALUE_SORT_WEIGHT_GENERATOR = "existingValueSortWeightGenerator";

public static final String AUTO_CLOSE_QUEUE_ENTRIES_AT_TIME = "queue.autoCloseQueueEntriesAtTime";

public static final String AUTO_CLOSE_QUEUE_ENTRIES_FOR_QUEUES = "queue.autoCloseQueueEntriesForQueues";
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,18 @@ String generateVisitQueueNumber(@NotNull Location location, @NotNull Queue queue
@NotNull VisitAttributeType visitAttributeType);

/**
* Closes all active queue entries
* Ends the given queue entry at the given time. The entry is reloaded before it is written, so a
* queue entry that has been ended or otherwise modified since it was loaded is left alone rather
* than having the stale state written back over it. This is intended for the scheduled tasks, which
* work from a list of entries loaded before the first of them is saved.
*
* @param queueEntry the queue entry to end
* @param endedAt the time at which to end it
* @return true if the queue entry was ended, false if it had already ended or was modified by
* another transaction
*/
@Authorized(PrivilegeConstants.MANAGE_QUEUE_ENTRIES)
void closeActiveQueueEntries();
@Authorized({ PrivilegeConstants.MANAGE_QUEUE_ENTRIES })
boolean closeQueueEntry(@NotNull QueueEntry queueEntry, @NotNull Date endedAt);

/**
* @return the instance of SortWeightGenerator that is configured via global property, or null if
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,31 @@ public String generateVisitQueueNumber(Location location, Queue queue, Visit vis
return queueNumber;
}

/**
* @see QueueEntryService#closeQueueEntry(QueueEntry, Date)
*/
@Override
public void closeActiveQueueEntries() {
QueueEntrySearchCriteria criteria = new QueueEntrySearchCriteria();
criteria.setIsEnded(Boolean.FALSE);
List<QueueEntry> queueEntries = getQueueEntries(criteria);
queueEntries.forEach(this::endQueueEntry);
public boolean closeQueueEntry(@NotNull QueueEntry queueEntry, @NotNull Date endedAt) {
if (queueEntry.getId() == null) {
throw new IllegalArgumentException("Cannot close a queue entry that has not been saved");
}

// Reload from database to check current state and guard against concurrent modifications
QueueEntry currentState = dao.get(queueEntry.getId()).orElse(null);
if (currentState == null) {
log.debug("Queue entry {} no longer exists, not closing it", queueEntry.getId());
return false;
}
if (currentState.getVoided() || currentState.getEndedAt() != null) {
log.debug("Queue entry {} is already voided or ended, not closing it", queueEntry.getId());
return false;
}

// Capture the dateChanged for optimistic locking
Date expectedDateChanged = currentState.getDateChanged();

currentState.setEndedAt(endedAt);
return dao.updateIfUnmodified(currentState, expectedDateChanged);
}

@Override
Expand All @@ -279,11 +298,6 @@ protected QueueEntryService getProxiedQueueEntryService() {
return Context.getService(QueueEntryService.class);
}

private void endQueueEntry(@NotNull QueueEntry queueEntry) {
queueEntry.setEndedAt(new Date());
dao.createOrUpdate(queueEntry);
}

private static Date roundToSecond(Date date) {
if (date == null) {
return null;
Expand Down
Loading