use of io.lumeer.api.model.DelayedAction in project engine by Lumeer.
the class DelayedActionCodec method decode.
@Override
public DelayedAction decode(final BsonReader reader, final DecoderContext decoderContext) {
final Document bson = documentCodec.decode(reader, decoderContext);
final DelayedAction action = new DelayedAction();
final String id = bson.getObjectId(ID).toHexString();
action.setId(id);
if (bson.getDate(DelayedAction.CHECK_AFTER) != null) {
ZonedDateTime checkAfter = ZonedDateTime.ofInstant(bson.getDate(DelayedAction.CHECK_AFTER).toInstant(), ZoneOffset.UTC);
action.setCheckAfter(checkAfter);
}
if (bson.getDate(DelayedAction.STARTED_PROCESSING) != null) {
ZonedDateTime startedProcessing = ZonedDateTime.ofInstant(bson.getDate(DelayedAction.STARTED_PROCESSING).toInstant(), ZoneOffset.UTC);
action.setStartedProcessing(startedProcessing);
}
if (bson.getDate(DelayedAction.COMPLETED) != null) {
ZonedDateTime completed = ZonedDateTime.ofInstant(bson.getDate(DelayedAction.COMPLETED).toInstant(), ZoneOffset.UTC);
action.setCompleted(completed);
}
action.setProcessor(bson.getString(DelayedAction.PROCESSOR));
action.setProgress(bson.getInteger(DelayedAction.PROGRESS));
action.setResourcePath(bson.getString(DelayedAction.RESOURCE_PATH));
action.setInitiator(bson.getString(DelayedAction.INITIATOR));
action.setReceiver(bson.getString(DelayedAction.RECEIVER));
action.setCorrelationId(bson.getString(DelayedAction.CORRELATION_ID));
if (bson.getString(DelayedAction.NOTIFICATION_TYPE) != null) {
action.setNotificationType(NotificationType.valueOf(bson.getString(DelayedAction.NOTIFICATION_TYPE)));
}
if (bson.getString(DelayedAction.NOTIFICATION_CHANNEL) != null) {
action.setNotificationChannel(NotificationChannel.valueOf(bson.getString(DelayedAction.NOTIFICATION_CHANNEL)));
}
Document data = bson.get(DelayedAction.DATA, Document.class);
action.setData(new DataDocument(data == null ? new Document() : data));
return action;
}
use of io.lumeer.api.model.DelayedAction in project engine by Lumeer.
the class DelayedActionProcessor method aggregateActions.
private List<DelayedAction> aggregateActions(final List<DelayedAction> actions) {
var actionsByIds = actions.stream().collect(Collectors.toMap(DelayedAction::getId, Function.identity()));
final List<DelayedAction> newActions = new ArrayList<>();
for (final NotificationChannel channel : NotificationChannel.values()) {
var actionsByUserAndTask = getActionsByTask(actions, channel);
actionsByUserAndTask.forEach((k, v) -> {
// for all chunks where there are more notifications than 1 for the same user
if (v.size() > 1) {
// check that we have the receiver and task id
if (v.stream().allMatch(a -> a.getReceiver() != null && a.getData().getString(DelayedAction.DATA_DOCUMENT_ID) != null)) {
// sort the actions from oldest to newest to merge them together in the right order
v.sort(Comparator.comparing(DelayedAction::getCheckAfter));
// merge the actions
DelayedAction action = v.get(0);
boolean wasAssignee = action.getNotificationType() == NotificationType.TASK_ASSIGNED;
final Set<NotificationType> originalTypes = new HashSet<>();
final List<String> originalIds = new ArrayList<>();
originalTypes.add(action.getNotificationType());
originalIds.add(action.getId());
actionsByIds.remove(action.getId());
for (int i = 1; i < v.size(); i++) {
final DelayedAction other = v.get(i);
action = action.merge(other);
wasAssignee = wasAssignee || (other.getNotificationType() == NotificationType.TASK_ASSIGNED);
originalTypes.add(other.getNotificationType());
originalIds.add(other.getId());
actionsByIds.remove(other.getId());
}
// set the correct type of the new aggregated action
if (wasAssignee) {
action.setNotificationType(NotificationType.TASK_ASSIGNED);
} else {
action.setNotificationType(NotificationType.TASK_CHANGED);
}
action.getData().append(DelayedAction.DATA_ORIGINAL_ACTION_TYPES, new ArrayList(originalTypes)).append(DelayedAction.DATA_ORIGINAL_ACTION_IDS, originalIds);
newActions.add(action);
}
}
});
}
newActions.addAll(actionsByIds.values());
return newActions;
}
use of io.lumeer.api.model.DelayedAction in project engine by Lumeer.
the class DelayedActionIT method testAssignment.
@Test
public void testAssignment() {
List<UserNotification> notifications = userNotificationDao.getRecentNotifications(user2.getId());
assertThat(notifications.size()).isEqualTo(0);
Document doc = createDocument("My cool task", List.of("evžen@vystrčil.cz", user2.getEmail()), new Date(ZonedDateTime.now().minus(DelayedActionDao.PROCESSING_DELAY_MINUTES, ChronoUnit.MINUTES).toInstant().toEpochMilli()), "To Do", List.of(), "so just another task");
List<DelayedAction> actions = delayedActionDao.getActions();
// now we should have:
// evžen@vystrčil.cz - PAST_DUE_DATE x 3 channels
// rspath@lumeerio.com (USER2 - observer) - PAST_DUE_DATE x 3 channels
// evžen@vystrčil.cz - TASK_ASSIGNED x 3 channels
// rspath@lumeerio.com (USER2 - observer) - TASK_ASSIGNED x 3 channels
// evžen@vystrčil.cz - STATE_UPDATE x 3 channels
// rspath@lumeerio.com (USER2 - observer) - STATE_UPDATE x 3 channels
var types = countOccurrences(actions, DelayedAction::getNotificationType);
assertThat(types.get(NotificationType.STATE_UPDATE)).isEqualTo(6);
assertThat(types.get(NotificationType.TASK_ASSIGNED)).isEqualTo(6);
assertThat(types.get(NotificationType.PAST_DUE_DATE)).isEqualTo(6);
assertThat(countOccurrences(actions, DelayedAction::getStartedProcessing).get(null)).isEqualTo(18);
assertThat(countOccurrences(actions, DelayedAction::getCompleted).get(null)).isEqualTo(18);
assertThat(countOccurrences(actions, (action) -> action.getCheckAfter().isBefore(ZonedDateTime.now())).get(true)).isEqualTo(18);
var channels = countOccurrences(actions, DelayedAction::getNotificationChannel);
assertThat(channels.get(NotificationChannel.Email)).isEqualTo(6);
assertThat(channels.get(NotificationChannel.Internal)).isEqualTo(6);
assertThat(channels.get(NotificationChannel.Slack)).isEqualTo(6);
assertThat(countOccurrences(actions, DelayedAction::getInitiator).get(user.getEmail())).isEqualTo(18);
assertThat(countOccurrences(actions, DelayedAction::getReceiver).get(user2.getEmail())).isEqualTo(9);
delayedActionProcessor.process();
notifications = userNotificationDao.getRecentNotifications(user2.getId());
// now TASK_ASSIGNED and STATE_UPDATE got aggregated to TASK_ASSIGNED
// each user receives 2 internal notifications - aggregated TASK_ASSIGNED and PAST_DUE_DATE
assertThat(notifications.size()).isEqualTo(2);
types = countOccurrences(notifications, UserNotification::getType);
assertThat(types.get(NotificationType.PAST_DUE_DATE)).isEqualTo(1);
assertThat(types.get(NotificationType.TASK_ASSIGNED)).isEqualTo(1);
actions = delayedActionDao.getActions();
// each user (2) has 3 channels and one PAST_DUE_DATE message scheduled for future = 6
assertThat(countOccurrences(actions, DelayedAction::getStartedProcessing).getOrDefault(null, 0)).isEqualTo(6);
assertThat(countOccurrences(actions, DelayedAction::getCompleted).getOrDefault(null, 0)).isEqualTo(6);
// Removing USER2 user from assignees
Document patched = documentFacade.patchDocumentData(collection.getId(), doc.getId(), new DataDocument("a1", List.of("evžen@vystrčil.cz")));
delayedActionProcessor.process();
actions = delayedActionDao.getActions();
// USER2 has TASK_UNASSIGNED for each channel (3) = 3
// evžen@vystrčil.cz has previous PAST_DUE_DATE on each channel = 3
types = countOccurrences(actions, DelayedAction::getNotificationType);
assertThat(types.getOrDefault(NotificationType.STATE_UPDATE, 0)).isEqualTo(0);
assertThat(types.getOrDefault(NotificationType.TASK_ASSIGNED, 0)).isEqualTo(0);
assertThat(types.getOrDefault(NotificationType.TASK_UNASSIGNED, 0)).isEqualTo(3);
assertThat(types.getOrDefault(NotificationType.PAST_DUE_DATE, 0)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getStartedProcessing).getOrDefault(null, 0)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getCompleted).getOrDefault(null, 0)).isEqualTo(3);
assertThat(countOccurrences(actions, (action) -> action.getCheckAfter().isBefore(ZonedDateTime.now())).get(true)).isEqualTo(3);
// Adding USER2 user back to assignees
patched = documentFacade.patchDocumentData(collection.getId(), doc.getId(), new DataDocument("a1", List.of(user2.getEmail())));
delayedActionProcessor.process();
actions = delayedActionDao.getActions();
types = countOccurrences(actions, DelayedAction::getNotificationType);
assertThat(types.get(NotificationType.PAST_DUE_DATE)).isEqualTo(3);
assertThat(types.get(NotificationType.TASK_ASSIGNED)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getReceiver).get(user2.getEmail())).isEqualTo(6);
notifications = userNotificationDao.getRecentNotifications(user2.getId());
// there should be three additional notifications - TASK_UNASSIGNED, TASK_ASSIGNED, PAST_DUE_DATE
assertThat(notifications.size()).isEqualTo(2 + 3);
types = countOccurrences(notifications, UserNotification::getType);
// this was aggregated under ASSIGNED
assertThat(types.containsKey(NotificationType.STATE_UPDATE)).isFalse();
assertThat(types.get(NotificationType.PAST_DUE_DATE)).isEqualTo(1 + 1);
assertThat(types.get(NotificationType.TASK_ASSIGNED)).isEqualTo(1 + 1);
assertThat(types.get(NotificationType.TASK_UNASSIGNED)).isEqualTo(1);
// three previously processed actions are removed
assertThat(actions.size()).isEqualTo(9);
assertThat(countOccurrences(actions, DelayedAction::getStartedProcessing).getOrDefault(null, 0)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.PAST_DUE_DATE)).isEqualTo(3);
// Setting state to completed
patched = documentFacade.patchDocumentData(collection.getId(), doc.getId(), new DataDocument("a3", "Done"));
actions = delayedActionDao.getActions();
var newActions = actions.stream().filter(action -> action.getStartedProcessing() == null).collect(Collectors.toList());
// past due date actions were replaced with state update
assertThat(newActions.size()).isEqualTo(3);
assertThat(countOccurrences(newActions, DelayedAction::getNotificationType).get(NotificationType.STATE_UPDATE)).isEqualTo(3);
delayedActionProcessor.process();
// Setting due date in future, but the task is completed
patched = documentFacade.patchDocumentData(collection.getId(), doc.getId(), new DataDocument("a2", new Date(ZonedDateTime.now().plus(1, ChronoUnit.DAYS).toInstant().toEpochMilli())));
actions = delayedActionDao.getActions();
assertThat(actions.stream().filter(action -> action.getStartedProcessing() == null).count()).isEqualTo(0);
// not assigned to user, nothing has changed, except for the fact that the actions were processed
assertThat(actions.size()).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.STATE_UPDATE)).isEqualTo(3);
delayedActionProcessor.process();
// Setting state as incomplete
patched = documentFacade.patchDocumentData(collection.getId(), doc.getId(), new DataDocument("a3", "New"));
actions = delayedActionDao.getActions();
// assignment and past due actions are back
assertThat(actions.stream().filter(action -> action.getStartedProcessing() == null).count()).isEqualTo(9);
assertThat(actions.size()).isEqualTo(9);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.STATE_UPDATE)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.TASK_REOPENED)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.PAST_DUE_DATE)).isEqualTo(3);
// Setting due date in the future again so expecting new notifications
patched = documentFacade.patchDocumentData(collection.getId(), doc.getId(), new DataDocument("a2", new Date(ZonedDateTime.now().plus(4, ChronoUnit.DAYS).toInstant().toEpochMilli())));
actions = delayedActionDao.getActions();
// we can even have due soon + due date changed
assertThat(actions.stream().filter(action -> action.getStartedProcessing() == null).count()).isEqualTo(15);
assertThat(actions.size()).isEqualTo(15);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.STATE_UPDATE)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.TASK_REOPENED)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.PAST_DUE_DATE)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.DUE_DATE_SOON)).isEqualTo(3);
assertThat(countOccurrences(actions, DelayedAction::getNotificationType).get(NotificationType.DUE_DATE_CHANGED)).isEqualTo(3);
notifications = userNotificationDao.getRecentNotifications(user2.getId());
assertThat(countOccurrences(notifications, UserNotification::getType).getOrDefault(NotificationType.TASK_CHANGED, 0)).isEqualTo(0);
delayedActionProcessor.process();
notifications = userNotificationDao.getRecentNotifications(user2.getId());
assertThat(countOccurrences(notifications, UserNotification::getType).getOrDefault(NotificationType.TASK_CHANGED, 0)).isEqualTo(1);
delayedActionDao.deleteAllScheduledActions(organizationId);
actions = delayedActionDao.getActions();
assertThat(actions.size()).isEqualTo(0);
}
use of io.lumeer.api.model.DelayedAction in project engine by Lumeer.
the class AbstractPurposeChangeDetector method getDelayedActions.
protected List<DelayedAction> getDelayedActions(final DocumentEvent documentEvent, final Collection collection, final NotificationType notificationType, final ZonedDateTime when, final Set<Assignee> assignees) {
final List<DelayedAction> actions = new ArrayList<>();
if (assignees != null) {
assignees.stream().map(Assignee::getEmail).collect(Collectors.toSet()).stream().filter(// collect to set to have each value just once
assignee -> (notificationType == NotificationType.DUE_DATE_SOON || notificationType == NotificationType.PAST_DUE_DATE || !assignee.equals(currentUser.getEmail().toLowerCase()) && StringUtils.isNotEmpty(assignee))).forEach(assignee -> {
ZonedDateTime timeZonedWhen = when;
// but only when just date is visible
if ((notificationType == NotificationType.DUE_DATE_SOON || notificationType == NotificationType.PAST_DUE_DATE) && CollectionUtil.isDueDateInUTC(collection) && !CollectionUtil.hasDueDateFormatTimeOptions(collection)) {
final Optional<String> userTimeZone = assignees.stream().filter(a -> a.getEmail().equals(assignee) && StringUtils.isNotEmpty(a.getTimeZone())).map(Assignee::getTimeZone).findFirst();
if (userTimeZone.isPresent()) {
final TimeZone tz = TimeZone.getTimeZone(userTimeZone.get());
timeZonedWhen = when.withZoneSameLocal(tz.toZoneId());
}
}
// in the future, this can be removed and checked in DelayedActionProcessor
timeZonedWhen = roundTime(timeZonedWhen, NotificationFrequency.Immediately);
final String resourcePath = getResourcePath(documentEvent);
final String correlationId = requestDataKeeper.getAppId() != null ? requestDataKeeper.getAppId().getValue() : requestDataKeeper.getCorrelationId();
final DataDocument data = getData(documentEvent, collection, assignee, assignees);
for (NotificationChannel channel : NotificationChannel.values()) {
final DelayedAction action = new DelayedAction();
action.setInitiator(currentUser.getEmail());
action.setReceiver(assignee);
action.setResourcePath(resourcePath);
action.setNotificationType(notificationType);
action.setCheckAfter(timeZonedWhen);
action.setNotificationChannel(channel);
action.setCorrelationId(correlationId);
action.setData(data);
actions.add(action);
}
});
}
return actions;
}
use of io.lumeer.api.model.DelayedAction in project engine by Lumeer.
the class MongoDelayedActionDao method getActionsForProcessing.
@Override
public List<DelayedAction> getActionsForProcessing(final boolean skipDelay) {
FindOneAndUpdateOptions options = new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER);
final List<DelayedAction> result = new ArrayList<>();
// generate unique signature
final String signature = UUID.randomUUID().toString();
DelayedAction action;
do {
action = databaseCollection().findOneAndUpdate(Filters.and(Filters.not(Filters.exists(DelayedAction.STARTED_PROCESSING)), Filters.lt(DelayedAction.CHECK_AFTER, Date.from((skipDelay ? ZonedDateTime.now() : ZonedDateTime.now().minus(PROCESSING_DELAY_MINUTES, ChronoUnit.MINUTES)).toInstant()))), Updates.combine(Updates.set(DelayedAction.STARTED_PROCESSING, Date.from(ZonedDateTime.now().toInstant())), Updates.set(DelayedAction.PROCESSOR, signature)), options);
if (action != null) {
if (signature.equals(action.getProcessor())) {
// otherwise it has been taken by another node in cluster
result.add(action);
}
}
} while (action != null);
return result;
}
Aggregations