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

org.bonitasoft.engine.message.MessagesHandlingService Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (C) 2019 Bonitasoft S.A.
 * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
 * This library is free software; you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Foundation
 * version 2.1 of the License.
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA 02110-1301, USA.
 **/
package org.bonitasoft.engine.message;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import lombok.extern.slf4j.Slf4j;
import org.bonitasoft.engine.api.utils.VisibleForTesting;
import org.bonitasoft.engine.builder.BuilderFactory;
import org.bonitasoft.engine.commons.TenantLifecycleService;
import org.bonitasoft.engine.commons.exceptions.SBonitaException;
import org.bonitasoft.engine.core.process.instance.api.event.EventInstanceService;
import org.bonitasoft.engine.core.process.instance.api.exceptions.event.trigger.SMessageInstanceReadException;
import org.bonitasoft.engine.core.process.instance.api.exceptions.event.trigger.SMessageModificationException;
import org.bonitasoft.engine.core.process.instance.api.exceptions.event.trigger.SWaitingEventModificationException;
import org.bonitasoft.engine.core.process.instance.api.exceptions.event.trigger.SWaitingEventReadException;
import org.bonitasoft.engine.core.process.instance.model.builder.event.handling.SMessageInstanceBuilder;
import org.bonitasoft.engine.core.process.instance.model.builder.event.handling.SWaitingMessageEventBuilderFactory;
import org.bonitasoft.engine.core.process.instance.model.event.handling.SBPMEventType;
import org.bonitasoft.engine.core.process.instance.model.event.handling.SMessageEventCouple;
import org.bonitasoft.engine.core.process.instance.model.event.handling.SMessageInstance;
import org.bonitasoft.engine.core.process.instance.model.event.handling.SWaitingMessageEvent;
import org.bonitasoft.engine.execution.work.BPMWorkFactory;
import org.bonitasoft.engine.lock.BonitaLock;
import org.bonitasoft.engine.lock.LockService;
import org.bonitasoft.engine.recorder.model.EntityUpdateDescriptor;
import org.bonitasoft.engine.sessionaccessor.SessionAccessor;
import org.bonitasoft.engine.transaction.BonitaTransactionSynchronization;
import org.bonitasoft.engine.transaction.STransactionNotFoundException;
import org.bonitasoft.engine.transaction.UserTransactionService;
import org.bonitasoft.engine.work.SWorkRegisterException;
import org.bonitasoft.engine.work.WorkService;

/**
 * @author Baptiste Mesta
 */
@Slf4j
public class MessagesHandlingService implements TenantLifecycleService {

    private static final int MAX_COUPLES = 100;
    private static final String LOCK_TYPE = "EVENTS";
    public static final String NUMBER_OF_MESSAGES_EXECUTED = "bonita.bpmengine.message.executed";
    public static final String NUMBER_OF_MESSAGES_POTENTIAL_MATCHED = "bonita.bpmengine.message.potential";
    public static final String NUMBER_OF_MESSAGES_MATCHING_RETRIGGERED_TASKS = "bonita.bpmengine.message.retriggeredtasks";
    private ThreadPoolExecutor threadPoolExecutor;
    private EventInstanceService eventInstanceService;
    private WorkService workService;
    private LockService lockService;
    private Long tenantId;
    private UserTransactionService userTransactionService;
    private SessionAccessor sessionAccessor;
    private BPMWorkFactory workFactory;

    private final Counter executedMessagesCounter;
    private final Counter matchedPotentialMessagesCounter;
    private final Counter retriggeredMatchingTasksCounter;

    public MessagesHandlingService(EventInstanceService eventInstanceService, WorkService workService,
            LockService lockService, Long tenantId, UserTransactionService userTransactionService,
            SessionAccessor sessionAccessor, BPMWorkFactory workFactory, MeterRegistry meterRegistry) {
        this.eventInstanceService = eventInstanceService;
        this.workService = workService;
        this.lockService = lockService;
        this.tenantId = tenantId;
        this.userTransactionService = userTransactionService;
        this.sessionAccessor = sessionAccessor;
        this.workFactory = workFactory;
        executedMessagesCounter = Counter.builder(NUMBER_OF_MESSAGES_EXECUTED)
                .tags(Tags.of("tenant", String.valueOf(tenantId)))
                .baseUnit("messages")
                .description("BPMN message couples executed")
                .register(meterRegistry);
        matchedPotentialMessagesCounter = Counter.builder(NUMBER_OF_MESSAGES_POTENTIAL_MATCHED)
                .tags(Tags.of("tenant", String.valueOf(tenantId)))
                .baseUnit("messages")
                .description("BPMN message couples potentially matched")
                .register(meterRegistry);
        retriggeredMatchingTasksCounter = Counter.builder(NUMBER_OF_MESSAGES_MATCHING_RETRIGGERED_TASKS)
                .tags(Tags.of("tenant", String.valueOf(tenantId)))
                .baseUnit("messages matching tasks")
                .description("BPMN message matching tasks retriggered")
                .register(meterRegistry);
    }

    @Override
    public void start() {
        log.info("Starting BPMN messages matcher thread");
        threadPoolExecutor = new ThreadPoolExecutor(1, 1, 1L, TimeUnit.HOURS,
                new ArrayBlockingQueue<>(5),
                r -> new Thread(r, "Bonita-Message-Matching"),
                (r, executor) -> log.debug("Message matching queue capacity reached"));
        log.info("Thread that handle messages matching successfully started");
    }

    @Override
    public void stop() {
        log.info("Stopping BPMN messages matcher thread");
        if (threadPoolExecutor == null) {
            log.info("BPMN messages matcher thread is already stopped");
            return;
        }
        threadPoolExecutor.shutdown();
        try {
            boolean termination = threadPoolExecutor.awaitTermination(5000, TimeUnit.MILLISECONDS);
            if (!termination) {
                log.warn("Failed to terminate the BPMN messages matcher thread." +
                        " This will not have functional impacts but it might produce warnings on server shutdown");
            }
        } catch (InterruptedException ignored) {
        }
        threadPoolExecutor = null;
        log.info("BPMN messages matcher thread successfully stopped");
    }

    @Override
    public void pause() {
        stop();
    }

    @Override
    public void resume() {
        start();
    }

    public void triggerMatchingOfMessages() throws STransactionNotFoundException {
        if (threadPoolExecutor == null) {
            log.warn("Cannot match messages when service is stopped. Maybe the engine is not yet started");
            return;
        }
        userTransactionService.registerBonitaSynchronization(new RegisterMessagesMatchingSynchronization());
    }

    @VisibleForTesting
    void matchEventCoupleAndTriggerExecution() throws Exception {
        userTransactionService.executeInTransaction(() -> {
            final List potentialMessageCouples = eventInstanceService.getMessageEventCouples(0,
                    MAX_COUPLES);
            final int potentialMessagesCount = potentialMessageCouples.size();
            log.info("Found {} potential message/event couples", potentialMessagesCount);
            matchedPotentialMessagesCounter.increment(potentialMessagesCount);
            final List uniqueCouples = getMessageUniqueCouples(potentialMessageCouples);
            if (!uniqueCouples.isEmpty()) {
                log.info("Triggering execution of unique {} message/event couples", uniqueCouples.size());
                executeUniqueMessageCouplesWork(uniqueCouples);
                log.info("Execution of message/event couples triggered");
            } else {
                log.debug("No message/event couples to be executed");
            }
            if (potentialMessagesCount == MAX_COUPLES) {
                log.debug("There are more than {} message/event couples to match. " +
                        "Will trigger the execution again now, to match more couples", MAX_COUPLES);
                triggerMatchingOfMessages();
                retriggeredMatchingTasksCounter.increment();
            }
            return null;
        });
    }

    private void executeUniqueMessageCouplesWork(final List uniqueCouples)
            throws SBonitaException {
        for (final SMessageEventCouple couple : uniqueCouples) {
            executeMessageCouple(couple.getMessageInstanceId(), couple.getWaitingMessageId());
        }
    }

    @VisibleForTesting
    void executeMessageCouple(long messageInstanceId, long waitingMessageId)
            throws SWaitingEventReadException, SMessageInstanceReadException,
            SMessageModificationException, SWaitingEventModificationException, SWorkRegisterException {
        log.debug("Registering message/event couple execution: message {} / event {}", messageInstanceId,
                waitingMessageId);

        // Mark messages that will be treated as "treatment in progress":
        final SWaitingMessageEvent waitingMsg = eventInstanceService.getWaitingMessage(waitingMessageId);
        final SMessageInstance messageInstance = eventInstanceService.getMessageInstance(messageInstanceId);
        markMessageAsInProgress(messageInstance);

        // EVENT_SUB_PROCESS of type non-interrupted should be considered as well, as soon as we support them
        if (!SBPMEventType.START_EVENT.equals(waitingMsg.getEventType())) {
            markWaitingMessageAsInProgress(waitingMsg);
        }
        executedMessagesCounter.increment();
        workService.registerWork(workFactory.createExecuteMessageCoupleWorkDescriptor(messageInstance, waitingMsg));
    }

    /**
     * From a list of couples that may contain duplicate waiting message candidates, select only one waiting message for
     * each message instance: the first
     * matching waiting message is arbitrary chosen.
     * In the case of SWaitingMessageEvent of types {@link SBPMEventType#START_EVENT} or
     * {@link SBPMEventType#EVENT_SUB_PROCESS}, it can be
     * selected several times to trigger multiple instances.
     *
     * @param potentialMessageCouples all the possible couples that match the potential correlation.
     * @return the reduced list of couple, where we insure that a unique message instance is associated with a unique
     *         waiting message.
     */
    List getMessageUniqueCouples(List potentialMessageCouples) {
        final List takenMessages = new ArrayList<>();
        final List takenWaitings = new ArrayList<>();
        final List uniqueMessageCouples = new ArrayList<>();
        for (final SMessageEventCouple couple : potentialMessageCouples) {
            final long messageInstanceId = couple.getMessageInstanceId();
            final long waitingMessageId = couple.getWaitingMessageId();
            final SBPMEventType waitingMessageEventType = couple.getWaitingMessageEventType();
            final boolean isMessageAlreadyTaken = takenMessages.contains(messageInstanceId);
            if (!isMessageAlreadyTaken && !takenWaitings.contains(waitingMessageId)) {
                takenMessages.add(messageInstanceId);
                // Starting events and Starting event sub-processes must not be considered as taken if they appear several times
                // EVENT_SUB_PROCESS of type non-interrupted should be considered as well, as soon as we support them
                if (!SBPMEventType.START_EVENT.equals(waitingMessageEventType)) {
                    takenWaitings.add(waitingMessageId);
                }
                uniqueMessageCouples.add(couple);
            } else if (log.isTraceEnabled()) {
                log.trace("Ignoring couple: message {} / event {}." +
                        " Duplication cause: message? {} / event? {}", couple.getMessageInstanceId(),
                        couple.getWaitingMessageId(), isMessageAlreadyTaken, takenWaitings.contains(waitingMessageId));
            }
        }
        return uniqueMessageCouples;
    }

    private void markMessageAsInProgress(final SMessageInstance messageInstance) throws SMessageModificationException {
        final EntityUpdateDescriptor descriptor = new EntityUpdateDescriptor();
        descriptor.addField(SMessageInstanceBuilder.HANDLED, true);
        eventInstanceService.updateMessageInstance(messageInstance, descriptor);
    }

    private void markWaitingMessageAsInProgress(final SWaitingMessageEvent waitingMsg)
            throws SWaitingEventModificationException {
        final EntityUpdateDescriptor descriptor = new EntityUpdateDescriptor();
        descriptor.addField(BuilderFactory.get(SWaitingMessageEventBuilderFactory.class).getProgressKey(),
                SWaitingMessageEventBuilderFactory.PROGRESS_IN_TREATMENT_KEY);
        eventInstanceService.updateWaitingMessage(waitingMsg, descriptor);
    }

    public void resetMessageCouple(long messageInstanceId, long waitingMessageId)
            throws SWaitingEventReadException, SWaitingEventModificationException, SMessageModificationException,
            SMessageInstanceReadException {
        resetWaitingMessage(waitingMessageId);
        resetMessageInstance(messageInstanceId);
    }

    private void resetMessageInstance(final long messageInstanceId)
            throws SMessageModificationException, SMessageInstanceReadException {
        final SMessageInstance messageInstance = eventInstanceService.getMessageInstance(messageInstanceId);
        if (messageInstance == null) {
            log.warn("Unable to reset message instance {} because it is not found", messageInstanceId);
            return;
        }
        final EntityUpdateDescriptor descriptor = new EntityUpdateDescriptor();
        descriptor.addField(SMessageInstanceBuilder.HANDLED, false);
        eventInstanceService.updateMessageInstance(messageInstance, descriptor);
    }

    private void resetWaitingMessage(final long waitingMessageId)
            throws SWaitingEventModificationException, SWaitingEventReadException {
        final SWaitingMessageEvent waitingMsg = eventInstanceService.getWaitingMessage(waitingMessageId);
        if (waitingMsg == null) {
            log.warn("Unable to reset waiting event because it is not found", waitingMessageId);
            return;
        }
        final EntityUpdateDescriptor descriptor = new EntityUpdateDescriptor();
        descriptor.addField(BuilderFactory.get(SWaitingMessageEventBuilderFactory.class).getProgressKey(),
                SWaitingMessageEventBuilderFactory.PROGRESS_FREE_KEY);
        eventInstanceService.updateWaitingMessage(waitingMsg, descriptor);
    }

    private class MessagesMatchingTask implements Callable {

        @Override
        public Void call() throws Exception {
            try {
                log.debug("Starting messages matching");
                // we use a lock in order to have only one execution at a time even in cluster
                BonitaLock eventLock = lockService.tryLock(1L, LOCK_TYPE, 1L, TimeUnit.MILLISECONDS, tenantId);
                if (eventLock == null) {
                    // It could happen that some messages were still not triggered because the work that is currently
                    // executing was started after the last message execution
                    log.debug(
                            "The task that matches BPMN messages is already running, this execution will be ignored");
                    return null;
                }
                try {
                    sessionAccessor.setTenantId(tenantId);
                    matchEventCoupleAndTriggerExecution();
                } finally {
                    lockService.unlock(eventLock, tenantId);
                }
                log.debug("Messages matching completed");
            } catch (Exception e) {
                log.error("Error while matching messages", e);
                throw e;
            }
            return null;
        }
    }

    private class RegisterMessagesMatchingSynchronization implements BonitaTransactionSynchronization {

        @Override
        public void afterCompletion(final int txState) {
            threadPoolExecutor.submit(new MessagesMatchingTask());
            log.debug("Messages matching task registered");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy