All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.gravitee.node.notifier.trigger.NotificationTrigger Maven / Gradle / Ivy

There is a newer version: 6.4.3
Show newest version
/**
 * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.gravitee.node.notifier.trigger;

import io.gravitee.node.api.notifier.NotificationAcknowledge;
import io.gravitee.node.api.notifier.NotificationAcknowledgeRepository;
import io.gravitee.node.api.notifier.NotificationCondition;
import io.gravitee.node.api.notifier.NotificationDefinition;
import io.gravitee.node.api.notifier.ResendNotificationCondition;
import io.gravitee.node.notifier.plugin.NotifierPluginFactory;
import io.gravitee.notifier.api.Notification;
import io.gravitee.notifier.api.Notifier;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import io.vertx.core.Context;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.support.CronExpression;

/**
 * @author Eric LELEU (eric.leleu at graviteesource.com)
 * @author GraviteeSource Team
 */
public class NotificationTrigger implements Handler {

    private static final Logger LOGGER = LoggerFactory.getLogger(NotificationTrigger.class);

    private final Vertx vertx;

    private final NotificationAcknowledgeRepository notificationAcknowledgeRepository;

    private final NotifierPluginFactory notifierFactory;

    private final NotificationDefinition definition;

    private final NotificationCondition condition;

    private final ResendNotificationCondition resendCondition;

    private final CronExpression cronExpression;

    private final int randomDelayInMs;

    private Long scheduledTaskId;

    private final AtomicBoolean started;

    public NotificationTrigger(
        Vertx vertx,
        NotificationAcknowledgeRepository notificationAcknowledgeRepository,
        NotifierPluginFactory notifierFactory,
        NotificationDefinition definition,
        NotificationCondition condition,
        ResendNotificationCondition resendCondition,
        boolean tryToAvoidMultipleNotif
    ) {
        this.vertx = vertx;
        this.notificationAcknowledgeRepository = notificationAcknowledgeRepository;
        this.notifierFactory = notifierFactory;
        this.definition = definition;
        this.condition = condition;
        this.resendCondition = resendCondition;
        this.cronExpression = CronExpression.parse(definition.getCron());
        started = new AtomicBoolean();
        if (tryToAvoidMultipleNotif) {
            this.randomDelayInMs = Math.max(1, new Random().nextInt(10)) * 1000;
        } else {
            this.randomDelayInMs = -1;
        }
    }

    public void start() {
        started.set(true);
        scheduleNextAttempt();
    }

    private void scheduleNextAttempt() {
        if (started.get()) {
            this.scheduledTaskId = this.vertx.setTimer(computeNextAttempt(), this);
        }
    }

    public void stop() {
        started.set(false);
        if (this.scheduledTaskId != null) {
            this.vertx.cancelTimer(this.scheduledTaskId);
        }
        this.scheduledTaskId = null;
        LOGGER.debug("Notification Trigger cancelled !");
    }

    private long computeNextAttempt() {
        final LocalDateTime now = LocalDateTime.now();
        final long delay = toEpochMillis(this.cronExpression.next(now)) - toEpochMillis(now);
        return randomDelayInMs > 0 ? delay + randomDelayInMs : delay;
    }

    private long toEpochMillis(LocalDateTime dateTime) {
        return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }

    @Override
    public void handle(Long event) {
        if (condition.test(definition)) {
            // keep EventLoop context in order to use it to execute the notifier's send method
            final Context triggerContext = vertx.getOrCreateContext();
            this.notificationAcknowledgeRepository.findByResourceIdAndTypeAndAudienceId(
                    this.definition.getResourceId(),
                    this.definition.getResourceType(),
                    definition.getType(),
                    definition.getAudienceId()
                )
                .map(Optional::ofNullable)
                .switchIfEmpty(Maybe.just(Optional.empty()))
                .subscribe(
                    notifAck -> {
                        final boolean firstNotification = notifAck.isEmpty();
                        if (firstNotification || resendCondition.apply(definition, notifAck.get())) {
                            // instantiate the plugin and send notification
                            final Optional optNotifier = notifierFactory.create(definition);
                            if (optNotifier.isPresent()) {
                                LOGGER.debug(
                                    "Send Notification with notifier type {} to audience {} about resource {}",
                                    definition.getType(),
                                    definition.getAudienceId(),
                                    definition.getResourceId()
                                );
                                final Notifier notifier = optNotifier.get();
                                // execute the send method into the triggerContext to avoid NullPointer as some notifiers rely on Vertx.currentContext
                                // that will be null as this block of code is executed in a RxJava ThreadPool and not into the Vertx EventLoop
                                triggerContext.runOnContext(aVoid ->
                                    notifier
                                        .send(buildNotification(definition), definition.getData())
                                        .whenComplete((Void anotherVoid, Throwable throwable) -> {
                                            if (throwable != null) {
                                                LOGGER.error(
                                                    "An error occurs while sending notification to {}",
                                                    definition.getType(),
                                                    throwable
                                                );
                                                // trigger a new attempt
                                                this.scheduleNextAttempt();
                                            } else {
                                                LOGGER.debug("A notification has been sent to {}", definition.getType());

                                                NotificationAcknowledge notificationAcknowledge = notifAck.orElse(
                                                    new NotificationAcknowledge()
                                                );
                                                Date now = new Date();
                                                notificationAcknowledge.setUpdatedAt(now);
                                                if (firstNotification) {
                                                    // Acknowledge has just been created
                                                    notificationAcknowledge.setCreatedAt(now);
                                                    notificationAcknowledge.setType(definition.getType());
                                                    notificationAcknowledge.setAudienceId(definition.getAudienceId());
                                                    notificationAcknowledge.setResourceId(definition.getResourceId());
                                                    notificationAcknowledge.setResourceType(definition.getResourceType());
                                                } else {
                                                    // existing acknowledge, increment the number of notifications
                                                    notificationAcknowledge.incrementCounter();
                                                }

                                                final Single saveAcknowledge = firstNotification
                                                    ? notificationAcknowledgeRepository.create(notificationAcknowledge)
                                                    : notificationAcknowledgeRepository.update(notificationAcknowledge);

                                                //trigger a new attempt
                                                saveAcknowledge
                                                    .onErrorResumeNext(error -> {
                                                        LOGGER.warn(
                                                            "Unable to store acknowledge for notification with audience {} and resource {}",
                                                            definition.getAudienceId(),
                                                            definition.getResourceId(),
                                                            error
                                                        );
                                                        return Single.just(notificationAcknowledge);
                                                    })
                                                    .doFinally(this::scheduleNextAttempt)
                                                    .subscribe();
                                            }
                                        })
                                );
                            } else {
                                LOGGER.warn(
                                    "Notifier {} not found, unable to send notification to target {} about resource {}",
                                    definition.getType(),
                                    definition.getAudienceId(),
                                    definition.getResourceId()
                                );
                            }
                        } else {
                            LOGGER.debug(
                                "Notification about resource {} already sent to target {}",
                                definition.getResourceId(),
                                definition.getAudienceId()
                            );
                            // trigger a new attempt.
                            // this will allow a new notification
                            // if DB entries are purged as we will have no
                            // way to know if the notification has been sent or not
                            this.scheduleNextAttempt();
                        }
                    },
                    error -> {
                        //trigger a new attempt
                        this.scheduleNextAttempt();
                        LOGGER.warn("Notification can't be send : {}", error.getMessage());
                    }
                );
        } else {
            // if rule isn't respected, trigger a new attempt
            this.scheduleNextAttempt();
        }
    }

    private final Notification buildNotification(NotificationDefinition definition) {
        Notification notification = new Notification();
        notification.setConfiguration(definition.getConfiguration());
        notification.setType(definition.getType());
        return notification;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy