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

io.automatiko.engine.addons.persistence.cassandra.job.CassandraJobService Maven / Gradle / Ivy

There is a newer version: 0.38.0
Show newest version
package io.automatiko.engine.addons.persistence.cassandra.job;

import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom;
import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.createKeyspace;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.servererrors.QueryExecutionException;
import com.datastax.oss.driver.api.core.type.DataTypes;
import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
import com.datastax.oss.driver.api.querybuilder.SchemaBuilder;
import com.datastax.oss.driver.api.querybuilder.delete.Delete;
import com.datastax.oss.driver.api.querybuilder.insert.Insert;
import com.datastax.oss.driver.api.querybuilder.schema.CreateIndex;
import com.datastax.oss.driver.api.querybuilder.schema.CreateKeyspace;
import com.datastax.oss.driver.api.querybuilder.schema.CreateTable;
import com.datastax.oss.driver.api.querybuilder.select.Select;

import io.automatiko.engine.api.Application;
import io.automatiko.engine.api.Model;
import io.automatiko.engine.api.audit.AuditEntry;
import io.automatiko.engine.api.audit.Auditor;
import io.automatiko.engine.api.auth.IdentityProvider;
import io.automatiko.engine.api.auth.TrustedIdentityProvider;
import io.automatiko.engine.api.config.CassandraJobsConfig;
import io.automatiko.engine.api.jobs.ExpirationTime;
import io.automatiko.engine.api.jobs.JobsService;
import io.automatiko.engine.api.jobs.ProcessInstanceJobDescription;
import io.automatiko.engine.api.jobs.ProcessJobDescription;
import io.automatiko.engine.api.uow.UnitOfWorkManager;
import io.automatiko.engine.api.workflow.Process;
import io.automatiko.engine.api.workflow.ProcessInstance;
import io.automatiko.engine.api.workflow.ProcessInstanceReadMode;
import io.automatiko.engine.api.workflow.Processes;
import io.automatiko.engine.services.time.TimerInstance;
import io.automatiko.engine.services.uow.UnitOfWorkExecutor;
import io.automatiko.engine.workflow.Sig;
import io.automatiko.engine.workflow.audit.BaseAuditEntry;
import io.automatiko.engine.workflow.base.core.timer.CronExpirationTime;
import io.automatiko.engine.workflow.base.core.timer.NoOpExpirationTime;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.interceptor.Interceptor;

@ApplicationScoped
public class CassandraJobService implements JobsService {

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

    private static final String INSTANCE_ID_FIELD = "JobInstanceId";
    private static final String FIRE_AT_FIELD = "JobFireAt";
    private static final String OWNER_INSTANCE_ID_FIELD = "JobOwnerInstanceId";
    private static final String OWNER_DEF_ID_FIELD = "JobOwnerDefinitionId";
    private static final String TRIGGER_TYPE_FIELD = "JobTriggerType";
    private static final String STATUS_FIELD = "JobStatus";
    private static final String FIRE_LIMIT_FIELD = "JobFireLimit";
    private static final String REPEAT_INTERVAL_FIELD = "JobRepeatInterval";
    private static final String EXPRESSION_FIELD = "JobExpression";

    protected final CqlSession cqlSession;

    protected final UnitOfWorkManager unitOfWorkManager;

    protected final Auditor auditor;

    protected final ScheduledThreadPoolExecutor scheduler;

    protected final ScheduledThreadPoolExecutor loadScheduler;

    protected Map> mappedProcesses = new HashMap<>();
    protected ConcurrentHashMap> scheduledJobs = new ConcurrentHashMap<>();

    protected final String tableName = "ATK_JOBS";

    private Optional createKeyspace;

    private Optional createTables;

    private Optional keyspace;

    private Optional interval;

    private Optional threads;

    @Inject
    public CassandraJobService(CqlSession cqlSession, Processes processes, Application application, Auditor auditor,
            @ConfigProperty(name = "quarkus.automatiko.persistence.disabled") Optional persistenceDisabled,
            @ConfigProperty(name = CassandraJobsConfig.CREATE_KEYSPACE_KEY) Optional createKeyspace,
            @ConfigProperty(name = CassandraJobsConfig.CREATE_TABLES_KEY) Optional createTables,
            @ConfigProperty(name = CassandraJobsConfig.KEYSPACE_KEY) Optional keyspace,
            @ConfigProperty(name = CassandraJobsConfig.INTERVAL_KEY) Optional interval,
            @ConfigProperty(name = CassandraJobsConfig.THREADS_KEY) Optional threads) {

        this.createKeyspace = createKeyspace;
        this.createTables = createTables;
        this.keyspace = keyspace;
        this.interval = interval;
        this.threads = threads;
        if (!persistenceDisabled.orElse(false)) {
            this.cqlSession = cqlSession;

            processes.processIds().forEach(id -> mappedProcesses.put(id, processes.processById(id)));

            if (this.createTables.orElse(Boolean.TRUE)) {
                createTable();
            }

            this.unitOfWorkManager = application.unitOfWorkManager();
            this.auditor = auditor;

            this.scheduler = new ScheduledThreadPoolExecutor(this.threads.orElse(1),
                    r -> new Thread(r, "automatiko-jobs-executor"));
            this.loadScheduler = new ScheduledThreadPoolExecutor(1, r -> new Thread(r, "automatiko-jobs-loader"));
        } else {
            this.cqlSession = null;
            this.unitOfWorkManager = null;
            this.auditor = null;
            this.scheduler = null;
            this.loadScheduler = null;
        }
    }

    public void start(@Observes @Priority(Interceptor.Priority.LIBRARY_AFTER) StartupEvent event) {
        if (this.cqlSession != null) {
            loadScheduler.scheduleAtFixedRate(() -> {
                try {
                    long next = LocalDateTime.now().plus(Duration.ofMinutes(interval.orElse(10L)))
                            .atZone(ZoneId.systemDefault()).toInstant()
                            .toEpochMilli();
                    Select select = selectFrom(keyspace.orElse("automatiko"), tableName).all()
                            .whereColumn(FIRE_AT_FIELD).isLessThan(literal(next)).allowFiltering();

                    ResultSet rs = cqlSession.execute(select.build());
                    List jobs = rs.all();
                    LOGGER.debug("Loaded jobs ({}) to be executed before {}", jobs.size(), next);
                    for (Row job : jobs) {

                        if (job.getString(OWNER_INSTANCE_ID_FIELD) == null) {
                            ProcessJobDescription description = ProcessJobDescription.of(build(job.getString(EXPRESSION_FIELD)),
                                    null,
                                    job.getString(OWNER_DEF_ID_FIELD));

                            scheduledJobs.computeIfAbsent(job.getString(INSTANCE_ID_FIELD), k -> {
                                return log(job.getString(INSTANCE_ID_FIELD),
                                        scheduler.schedule(new StartProcessOnExpiredTimer(job.getString(INSTANCE_ID_FIELD),
                                                job.getString(OWNER_DEF_ID_FIELD), -1, description),
                                                Duration.between(LocalDateTime.now(),
                                                        ZonedDateTime.ofInstant(
                                                                Instant.ofEpochMilli(job.getLong(FIRE_AT_FIELD)),
                                                                ZoneId.systemDefault()))
                                                        .toMillis(),
                                                TimeUnit.MILLISECONDS));
                            });
                        } else {
                            ProcessInstanceJobDescription description = ProcessInstanceJobDescription.of(
                                    job.getString(INSTANCE_ID_FIELD),
                                    job.getString(TRIGGER_TYPE_FIELD),
                                    build(job.getString(EXPRESSION_FIELD)), job.getString(OWNER_INSTANCE_ID_FIELD),
                                    job.getString(OWNER_DEF_ID_FIELD), null);

                            scheduledJobs.computeIfAbsent(job.getString(INSTANCE_ID_FIELD), k -> {
                                return log(job.getString(INSTANCE_ID_FIELD), scheduler.schedule(
                                        new SignalProcessInstanceOnExpiredTimer(job.getString(INSTANCE_ID_FIELD),
                                                job.getString(TRIGGER_TYPE_FIELD),
                                                job.getString(OWNER_DEF_ID_FIELD),
                                                job.getString(OWNER_INSTANCE_ID_FIELD),
                                                job.getInt(FIRE_LIMIT_FIELD), description),
                                        Duration.between(LocalDateTime.now(), ZonedDateTime.ofInstant(
                                                Instant.ofEpochMilli(job.getLong(FIRE_AT_FIELD)),
                                                ZoneId.systemDefault())).toMillis(),
                                        TimeUnit.MILLISECONDS));
                            });
                        }
                    }
                } catch (Exception e) {
                    LOGGER.error("Error while loading jobs from cassandra", e);
                }
            }, 1, interval.orElse(10L) * 60, TimeUnit.SECONDS);
        }
    }

    public void shutdown(@Observes ShutdownEvent event) {
        if (loadScheduler != null) {
            this.loadScheduler.shutdownNow();
        }
        if (scheduler != null) {
            this.scheduler.shutdown();
        }
    }

    @Override
    public String scheduleProcessJob(ProcessJobDescription description) {
        LOGGER.debug("ScheduleProcessJob: {}", description);
        Insert insert;
        if (description.expirationTime().repeatInterval() != null) {
            insert = insertInto(keyspace.orElse("automatiko"), tableName)
                    .value(INSTANCE_ID_FIELD, literal(description.id()))
                    .value(OWNER_DEF_ID_FIELD, literal(description.processId() + version(description.processVersion())))
                    .value(STATUS_FIELD, literal("scheduled"))
                    .value(FIRE_AT_FIELD,
                            literal(description.expirationTime().get().toLocalDateTime().atZone(ZoneId.systemDefault())
                                    .toInstant()
                                    .toEpochMilli()))
                    .value(FIRE_LIMIT_FIELD, literal(description.expirationTime().repeatLimit()))
                    .value(REPEAT_INTERVAL_FIELD, literal(description.expirationTime().repeatInterval()))
                    .value(EXPRESSION_FIELD, literal(description.expirationTime().expression()));
            Supplier entry = () -> BaseAuditEntry.timer(description)
                    .add("message", "Scheduled repeatable timer job that creates new workflow instances");

            auditor.publish(entry);
        } else {
            insert = insertInto(keyspace.orElse("automatiko"), tableName)
                    .value(INSTANCE_ID_FIELD, literal(description.id()))
                    .value(OWNER_DEF_ID_FIELD, literal(description.processId() + version(description.processVersion())))
                    .value(STATUS_FIELD, literal("scheduled"))
                    .value(FIRE_AT_FIELD,
                            literal(description.expirationTime().get().toLocalDateTime().atZone(ZoneId.systemDefault())
                                    .toInstant()
                                    .toEpochMilli()))
                    .value(FIRE_LIMIT_FIELD, literal(description.expirationTime().repeatLimit()))
                    .value(EXPRESSION_FIELD, literal(description.expirationTime().expression()));
            Supplier entry = () -> BaseAuditEntry.timer(description)
                    .add("message", "Scheduled one time timer job that creates new workflow instances");

            auditor.publish(entry);
        }
        cqlSession.execute(insert.build());
        if (description.expirationTime().get().toLocalDateTime()
                .isBefore(LocalDateTime.now().plusMinutes(interval.orElse(10L)))) {

            scheduledJobs.computeIfAbsent(description.id(), k -> {
                return scheduler.schedule(processJobByDescription(description),
                        calculateDelay(description.expirationTime().get()), TimeUnit.MILLISECONDS);
            });
        }

        return description.id();
    }

    @Override
    public String scheduleProcessInstanceJob(ProcessInstanceJobDescription description) {

        Insert insert;
        if (description.expirationTime().repeatInterval() != null) {

            insert = insertInto(keyspace.orElse("automatiko"), tableName)
                    .value(INSTANCE_ID_FIELD, literal(description.id()))
                    .value(TRIGGER_TYPE_FIELD, literal(description.triggerType()))
                    .value(OWNER_DEF_ID_FIELD, literal(description.processId() + version(description.processVersion())))
                    .value(OWNER_INSTANCE_ID_FIELD, literal(description.processInstanceId()))
                    .value(STATUS_FIELD, literal("scheduled"))
                    .value(FIRE_AT_FIELD,
                            literal(description.expirationTime().get().toLocalDateTime().atZone(ZoneId.systemDefault())
                                    .toInstant()
                                    .toEpochMilli()))
                    .value(FIRE_LIMIT_FIELD, literal(description.expirationTime().repeatLimit()))
                    .value(REPEAT_INTERVAL_FIELD, literal(description.expirationTime().repeatInterval()))
                    .value(EXPRESSION_FIELD, literal(description.expirationTime().expression()));
            Supplier entry = () -> BaseAuditEntry.timer(description)
                    .add("message", "Scheduled repeatable timer job for existing workflow instance");

            auditor.publish(entry);
        } else {
            insert = insertInto(keyspace.orElse("automatiko"), tableName)
                    .value(INSTANCE_ID_FIELD, literal(description.id()))
                    .value(TRIGGER_TYPE_FIELD, literal(description.triggerType()))
                    .value(OWNER_DEF_ID_FIELD, literal(description.processId() + version(description.processVersion())))
                    .value(OWNER_INSTANCE_ID_FIELD, literal(description.processInstanceId()))
                    .value(STATUS_FIELD, literal("scheduled"))
                    .value(FIRE_AT_FIELD,
                            literal(description.expirationTime().get().toLocalDateTime().atZone(ZoneId.systemDefault())
                                    .toInstant()
                                    .toEpochMilli()))
                    .value(FIRE_LIMIT_FIELD, literal(description.expirationTime().repeatLimit()))
                    .value(EXPRESSION_FIELD, literal(description.expirationTime().expression()));
            Supplier entry = () -> BaseAuditEntry.timer(description)
                    .add("message", "Scheduled one time timer job for existing workflow instance");

            auditor.publish(entry);
        }

        cqlSession.execute(insert.build());

        if (description.expirationTime().get().toLocalDateTime()
                .isBefore(LocalDateTime.now().plusMinutes(interval.orElse(10L)))) {

            scheduledJobs.computeIfAbsent(description.id(), k -> {
                return log(description.id(), scheduler.schedule(
                        new SignalProcessInstanceOnExpiredTimer(description.id(), description.triggerType(),
                                description.processId() + version(description.processVersion()),
                                description.processInstanceId(), description.expirationTime().repeatLimit(), description),
                        calculateDelay(description.expirationTime().get()),
                        TimeUnit.MILLISECONDS));
            });
        }

        return description.id();
    }

    @Override
    public boolean cancelJob(String id) {

        removeScheduledJob(id);

        return true;
    }

    @Override
    public ZonedDateTime getScheduledTime(String id) {
        Select select = selectFrom(keyspace.orElse("automatiko"), tableName).column(FIRE_AT_FIELD)
                .whereColumn(INSTANCE_ID_FIELD).isEqualTo(literal(id));

        ResultSet rs = cqlSession.execute(select.build());
        Row row = rs.one();
        if (row != null) {
            Long fireAt = row.getLong(FIRE_AT_FIELD);

            return ZonedDateTime.ofInstant(Instant.ofEpochMilli(fireAt), ZoneId.systemDefault());

        } else {
            return null;
        }

    }

    protected long calculateDelay(ZonedDateTime expirationDate) {
        return Duration.between(ZonedDateTime.now(), expirationDate).toMillis();
    }

    protected Runnable processJobByDescription(ProcessJobDescription description) {
        return new StartProcessOnExpiredTimer(description.id(),
                description.process().id(), description.expirationTime().repeatLimit(), description);

    }

    protected String version(String version) {
        if (version != null && !version.trim().isEmpty()) {
            return "_" + version.replaceAll("\\.", "_");
        }
        return "";
    }

    protected void removeScheduledJob(String id) {
        Supplier entry = () -> {
            Select select = selectFrom(keyspace.orElse("automatiko"), tableName)
                    .columns(EXPRESSION_FIELD, REPEAT_INTERVAL_FIELD, FIRE_LIMIT_FIELD, OWNER_DEF_ID_FIELD,
                            OWNER_INSTANCE_ID_FIELD, TRIGGER_TYPE_FIELD)
                    .whereColumn(INSTANCE_ID_FIELD).isEqualTo(literal(id));

            ResultSet rs = cqlSession.execute(select.build());
            Row row = rs.one();
            if (row != null) {
                return BaseAuditEntry.timer()
                        .add("message", "Cancelled job for existing workflow instance")
                        .add("jobId", id)
                        .add("timerExpression", row.getString(EXPRESSION_FIELD))
                        .add("timerInterval", row.getLong(REPEAT_INTERVAL_FIELD))
                        .add("timerRepeatLimit", row.getInt(FIRE_LIMIT_FIELD))
                        .add("workflowDefinitionId", row.getString(OWNER_DEF_ID_FIELD))
                        .add("workflowInstanceId", row.getString(OWNER_INSTANCE_ID_FIELD))
                        .add("triggerType", row.getString(TRIGGER_TYPE_FIELD));

            } else {
                return BaseAuditEntry.timer()
                        .add("message", "Cancelled job for existing workflow instance")
                        .add("jobId", id);
            }
        };

        auditor.publish(entry);
        Delete deleteStatement = deleteFrom(keyspace.orElse("automatiko"), tableName).whereColumn(INSTANCE_ID_FIELD)
                .isEqualTo(literal(id)).ifExists();

        cqlSession.execute(deleteStatement.build());
    }

    protected void updateRepeatableJob(String id) {
        Select select = selectFrom(keyspace.orElse("automatiko"), tableName).column(FIRE_AT_FIELD)
                .whereColumn(INSTANCE_ID_FIELD).isEqualTo(literal(id));

        ResultSet rs = cqlSession.execute(select.build());
        Row job = rs.one();
        if (job != null) {

            Integer limit = job.getInt(FIRE_LIMIT_FIELD) - 1;
            Long repeat = job.getLong(REPEAT_INTERVAL_FIELD);
            ZonedDateTime fireTime = ZonedDateTime.ofInstant(
                    Instant.ofEpochMilli(job.getLong(FIRE_AT_FIELD)),
                    ZoneId.systemDefault());

            SimpleStatement statement = QueryBuilder.update(keyspace.orElse("automatiko"), tableName)
                    .setColumn(STATUS_FIELD, literal("scheduled"))
                    .setColumn(FIRE_LIMIT_FIELD, literal(limit))
                    .setColumn(FIRE_AT_FIELD, literal(fireTime.plus(repeat, ChronoUnit.MILLIS).toInstant().toEpochMilli()))
                    .whereColumn(INSTANCE_ID_FIELD).isEqualTo(literal(id))
                    .build();

            cqlSession.execute(statement);

            if (job.getString(OWNER_INSTANCE_ID_FIELD) == null) {
                ProcessJobDescription description = ProcessJobDescription.of(build(job.getString(EXPRESSION_FIELD)), null,
                        job.getString(OWNER_DEF_ID_FIELD));

                scheduledJobs.computeIfAbsent(job.getString(INSTANCE_ID_FIELD), k -> {
                    return log(job.getString(INSTANCE_ID_FIELD),
                            scheduler.schedule(new StartProcessOnExpiredTimer(job.getString(INSTANCE_ID_FIELD),
                                    job.getString(OWNER_DEF_ID_FIELD), limit, description),
                                    Duration.between(LocalDateTime.now(), fireTime).toMillis(),
                                    TimeUnit.MILLISECONDS));
                });
            } else {
                ProcessInstanceJobDescription description = ProcessInstanceJobDescription.of(job.getString(INSTANCE_ID_FIELD),
                        job.getString(TRIGGER_TYPE_FIELD),
                        build(job.getString(EXPRESSION_FIELD)), job.getString(OWNER_INSTANCE_ID_FIELD),
                        job.getString(OWNER_DEF_ID_FIELD), null);

                scheduledJobs.computeIfAbsent(job.getString(INSTANCE_ID_FIELD), k -> {
                    return log(job.getString(INSTANCE_ID_FIELD), scheduler.scheduleAtFixedRate(
                            new SignalProcessInstanceOnExpiredTimer(job.getString(INSTANCE_ID_FIELD),
                                    job.getString(TRIGGER_TYPE_FIELD),
                                    job.getString(OWNER_DEF_ID_FIELD),
                                    job.getString(OWNER_INSTANCE_ID_FIELD), limit, description),
                            Duration.between(LocalDateTime.now(), fireTime).toMillis(), repeat,
                            TimeUnit.MILLISECONDS));
                });
            }
        }
    }

    protected ScheduledFuture log(String jobId, ScheduledFuture future) {
        LOGGER.debug("Next fire of job {} is in {} seconds ", jobId, future.getDelay(TimeUnit.SECONDS));

        return future;
    }

    protected ExpirationTime build(String expression) {
        if (expression != null) {
            return CronExpirationTime.of(expression);
        }

        return new NoOpExpirationTime();
    }

    protected void createTable() {
        if (createKeyspace.orElse(true)) {
            CreateKeyspace createKs = createKeyspace(keyspace.orElse("automatiko")).ifNotExists()
                    .withSimpleStrategy(1);
            cqlSession.execute(createKs.build());
        }

        CreateTable createTable = SchemaBuilder.createTable(keyspace.orElse("automatiko"), tableName)
                .ifNotExists()
                .withPartitionKey(INSTANCE_ID_FIELD, DataTypes.TEXT)
                .withColumn(FIRE_AT_FIELD, DataTypes.BIGINT)
                .withColumn(OWNER_INSTANCE_ID_FIELD, DataTypes.TEXT)
                .withColumn(OWNER_DEF_ID_FIELD, DataTypes.TEXT)
                .withColumn(TRIGGER_TYPE_FIELD, DataTypes.TEXT)
                .withColumn(STATUS_FIELD, DataTypes.TEXT)
                .withColumn(FIRE_LIMIT_FIELD, DataTypes.INT)
                .withColumn(REPEAT_INTERVAL_FIELD, DataTypes.BIGINT)
                .withColumn(EXPRESSION_FIELD, DataTypes.TEXT);

        cqlSession.execute(createTable.build());

        CreateIndex index = SchemaBuilder.createIndex(tableName + "_IDX").ifNotExists()
                .onTable(keyspace.orElse("automatiko"), tableName).andColumn(FIRE_AT_FIELD);
        cqlSession.execute(index.build());
    }

    private class SignalProcessInstanceOnExpiredTimer implements Runnable {

        private final String id;
        private final String processId;
        private String processInstanceId;

        private final String trigger;
        private Integer limit;

        private ProcessInstanceJobDescription description;

        private SignalProcessInstanceOnExpiredTimer(String id, String trigger, String processId, String processInstanceId,
                Integer limit, ProcessInstanceJobDescription description) {
            this.id = id;
            this.processId = processId;
            this.processInstanceId = processInstanceId;
            this.trigger = trigger;
            this.limit = limit;

            this.description = description;
        }

        @Override
        public void run() {
            LOGGER.debug("Job {} started", id);

            SimpleStatement statement = QueryBuilder.update(keyspace.orElse("automatiko"), tableName)
                    .setColumn(STATUS_FIELD, literal("taken"))
                    .whereColumn(INSTANCE_ID_FIELD).isEqualTo(literal(id))
                    .ifColumn(STATUS_FIELD).isEqualTo(literal("scheduled"))
                    .build();

            try {
                boolean applied = cqlSession.execute(statement).wasApplied();

                if (!applied) {
                    scheduledJobs.remove(id).cancel(true);
                    return;
                }

                Process process = mappedProcesses.get(processId);
                if (process == null) {
                    LOGGER.warn("No process found for process id {}", processId);
                    return;
                }

                IdentityProvider.set(new TrustedIdentityProvider("System"));
                Supplier entry = () -> BaseAuditEntry.timer(description)
                        .add("message", "Executing timer job for existing workflow instance");

                auditor.publish(entry);
                UnitOfWorkExecutor.executeInUnitOfWork(unitOfWorkManager, () -> {
                    Optional> processInstanceFound = process.instances()
                            .findById(processInstanceId, ProcessInstanceReadMode.MUTABLE_WITH_LOCK);
                    if (processInstanceFound.isPresent()) {
                        ProcessInstance processInstance = processInstanceFound.get();
                        String[] ids = id.split("_");
                        processInstance
                                .send(Sig.of(trigger, TimerInstance.with(Long.parseLong(ids[1]), id, limit)));
                        scheduledJobs.remove(id).cancel(false);
                        if (description.expirationTime().next() != null) {
                            removeScheduledJob(id);
                            scheduleProcessInstanceJob(description);
                        } else if (limit > 0) {
                            updateRepeatableJob(id);
                        } else {
                            removeScheduledJob(id);
                        }
                    } else {
                        // since owning process instance does not exist cancel timers
                        scheduledJobs.remove(id).cancel(false);
                        removeScheduledJob(id);
                    }

                    return null;
                });
                LOGGER.debug("Job {} completed", id);
            } catch (QueryExecutionException rnf) {
                scheduledJobs.remove(id).cancel(true);
            }

        }
    }

    private class StartProcessOnExpiredTimer implements Runnable {

        private final String id;
        private final String processId;

        private Integer limit;

        private ProcessJobDescription description;

        private StartProcessOnExpiredTimer(String id, String processId, Integer limit, ProcessJobDescription description) {
            this.id = id;
            this.processId = processId;
            this.limit = limit;

            this.description = description;
        }

        @SuppressWarnings({ "unchecked", "rawtypes" })
        @Override
        public void run() {
            LOGGER.debug("Job {} started", id);
            SimpleStatement statement = QueryBuilder.update(keyspace.orElse("automatiko"), tableName)
                    .setColumn(STATUS_FIELD, literal("taken"))
                    .whereColumn(INSTANCE_ID_FIELD).isEqualTo(literal(id))
                    .ifColumn(STATUS_FIELD).isEqualTo(literal("scheduled"))
                    .build();

            try {
                boolean applied = cqlSession.execute(statement).wasApplied();

                if (!applied) {
                    scheduledJobs.remove(id).cancel(true);
                    return;
                }
                Process process = mappedProcesses.get(processId);
                if (process == null) {
                    LOGGER.warn("No process found for process id {}", processId);
                    return;
                }
                IdentityProvider.set(new TrustedIdentityProvider("System"));
                Supplier entry = () -> BaseAuditEntry.timer(description)
                        .add("message", "Executing timer job to create new workflow instance");

                auditor.publish(entry);
                UnitOfWorkExecutor.executeInUnitOfWork(unitOfWorkManager, () -> {
                    ProcessInstance pi = process.createInstance(process.createModel());
                    if (pi != null) {
                        pi.start("timer", null, null);
                    }
                    scheduledJobs.remove(id).cancel(false);
                    limit--;
                    if (description.expirationTime().next() != null) {
                        removeScheduledJob(id);
                        scheduleProcessJob(description);
                    } else if (limit > 0) {
                        updateRepeatableJob(id);
                    } else {
                        removeScheduledJob(id);
                    }
                    return null;
                });

                LOGGER.debug("Job {} completed", id);
            } catch (QueryExecutionException rnf) {
                scheduledJobs.remove(id).cancel(true);
            }
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy