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

com.hpe.caf.services.job.api.DatabaseHelper Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016-2024 Open Text.
 *
 * 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 com.hpe.caf.services.job.api;

import com.hpe.caf.services.db.client.DatabaseConnectionProvider;
import com.hpe.caf.services.job.api.generated.model.Failure;
import com.hpe.caf.services.job.api.generated.model.Job;
import com.hpe.caf.services.configuration.AppConfig;
import com.hpe.caf.services.job.api.generated.model.JobStatus;
import com.hpe.caf.services.job.api.generated.model.SortDirection;
import com.hpe.caf.services.job.api.generated.model.SortField;
import com.hpe.caf.services.job.exceptions.BadRequestException;
import com.hpe.caf.services.job.exceptions.ForbiddenException;
import com.hpe.caf.services.job.exceptions.NotFoundException;
import com.hpe.caf.services.job.exceptions.ServiceUnavailableException;
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.*;
import java.text.ParseException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import org.codehaus.jettison.json.JSONException;

/**
 * The DatabaseHelper class is responsible for database operations.
 */
public final class DatabaseHelper
{
    private static final String FAILURE_PROPERTY_MISSING = "Unknown";

    // PostgreSQL Error Codes: https://www.postgresql.org/docs/current/errcodes-appendix.html
    private static final String POSTGRES_CONNECTION_EXCEPTION_ERROR_CODE_PREFIX = "08";
    private static final String POSTGRES_NO_DATA_ERROR_CODE = "02000";
    private static final String POSTGRES_NO_DATA_FOUND_ERROR_CODE = "P0002";
    private static final String POSTGRES_UNIQUE_VIOLATION_ERROR_CODE = "23505";

    private static AppConfig appConfig;

    private static final Logger LOG = LoggerFactory.getLogger(DatabaseHelper.class);

    /**
     * Instantiates a new DBUtil
     *
     * @param appConfig PostgreSQL database connection properties incl url (i.e. "jdbc:postgresql://PostgreSQLHost:portNumber/databaseName"), username and password
     */
    public DatabaseHelper(AppConfig appConfig)
    {
        DatabaseHelper.appConfig = appConfig;
    }

    public Job[] getJobs(final String partitionId, String jobIdStartsWith, String statusType, Integer limit,
                         Integer offset, final SortField sortField, final SortDirection sortDirection,
                         final List labels, final String filter) throws Exception {

        final Map jobs = new LinkedHashMap<>(); //Linked rather than hash to preserve order of results.

        try (
                final Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                final CallableStatement stmt = conn.prepareCall("{call get_jobs(?,?,?,?,?,?,?,?,?,?)}")
        ) {
            if (jobIdStartsWith == null) {
                jobIdStartsWith = "";
            }
            if (statusType == null) {
                statusType = "";
            }
            if (limit == null) {
                limit = 0;
            }
            if (offset == null) {
                offset = 0;
            }
            stmt.setString(1, partitionId);
            stmt.setString(2, jobIdStartsWith);
            stmt.setString(3, statusType);
            stmt.setInt(4, limit);
            stmt.setInt(5, offset);
            stmt.setString(6, sortField.getDbField());
            stmt.setString(7, sortField.getSortLabel());
            stmt.setBoolean(8, sortDirection.getDbValue());
            Array array;
            if (labels != null) {
                array = conn.createArrayOf("VARCHAR", labels.toArray());
            } else {
                array = conn.createArrayOf("VARCHAR", new String[0]);
            }
            stmt.setArray(9, array);
            stmt.setString(10, filter);

            //  Execute a query to return a list of all job definitions in the system.
            LOG.debug("Calling get_jobs() database function...");
            try (final ResultSet rs = stmt.executeQuery()) {
                while (rs.next()) {
                    final Job job = new Job();
                    job.setId(rs.getString("job_id"));
                    job.setName(rs.getString("name"));
                    job.setDescription(rs.getString("description"));
                    job.setExternalData(rs.getString("data"));
                    job.setCreateTime(getDate(rs.getString("create_date")));
                    job.setLastUpdateTime(getDate(rs.getString("last_update_date")));
                    job.setStatus(JobStatus.valueOf(rs.getString("status").toUpperCase(Locale.ENGLISH)));
                    job.setPercentageComplete(rs.getFloat("percentage_complete"));

                    //  Parse JSON failure sub-strings.
                    final String failureDetails = rs.getString("failure_details");
                    job.setFailures(getFailuresAsList(failureDetails));

                    final String label = rs.getString("label");
                    if (ApiServiceUtil.isNotNullOrEmpty(label)) {
                        job.getLabels().put(label, rs.getString("label_value"));
                    }
                    //We joined onto the labels table and there may be multiple rows for the same job, so merge their labels
                    jobs.merge(job.getId(), job, (orig, insert) -> {
                        orig.getLabels().putAll(insert.getLabels());
                        return orig;
                    });
                }
            } finally {
                if (array != null) {
                    array.free();
                }
            }
        } catch (final SQLException se) {
           throw mapSqlConnectionException(se);
        }

        //  Convert arraylist to array of jobs.
        Job[] jobArr = new Job[jobs.size()];
        jobArr = jobs.values().toArray(jobArr);
        return jobArr;
    }

    /**
     * Returns the number of job definitions in the system.
     */
    public long getJobsCount(final String partitionId, String jobIdStartsWith, String statusType, final String filter) throws Exception {

        long jobsCount = 0;

        try (
                Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                CallableStatement stmt = conn.prepareCall("{call get_jobs_count(?,?,?,?)}")
        ) {
            if (jobIdStartsWith == null) {
                jobIdStartsWith = "";
            }
            if (statusType == null) {
                statusType = "";
            }
            stmt.setString(1, partitionId);
            stmt.setString(2, jobIdStartsWith);
            stmt.setString(3, statusType);
            stmt.setString(4, filter);

            //  Execute a query to return a count of all job definitions in the system.
            LOG.debug("Calling get_jobs_count() database function...");
            try (final ResultSet rs = stmt.executeQuery()) {
                if (rs.next()) {
                    jobsCount = rs.getLong(1);
                }
            }
        } catch (final SQLException se) {
            throw mapSqlConnectionException(se);
        }

        return jobsCount;
    }

    /**
     * Returns the job definition for the specified job.
     */
    public Job getJob(final String partitionId, String jobId) throws Exception {

        Job job = null;

        try (
                Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                CallableStatement stmt = conn.prepareCall("{call get_job(?,?)}")
        ) {
            stmt.setString(1, partitionId);
            stmt.setString(2,jobId);

            //  Execute a query to return a list of all job definitions in the system.
            LOG.debug("Calling get_job() database function...");
            try (final ResultSet rs = stmt.executeQuery()) {
                job = new Job();
                while (rs.next()) {
                    job.setId(rs.getString("job_id"));
                    job.setName(rs.getString("name"));
                    job.setDescription(rs.getString("description"));
                    job.setExternalData(rs.getString("data"));
                    job.setCreateTime(getDate(rs.getString("create_date")));
                    job.setLastUpdateTime(getDate(rs.getString("last_update_date")));
                    job.setStatus(JobStatus.valueOf(rs.getString("status").toUpperCase(Locale.ENGLISH)));
                    job.setPercentageComplete(rs.getFloat("percentage_complete"));

                    //  Parse JSON failure sub-strings.
                    final String failureDetails = rs.getString("failure_details");
                    job.setFailures(getFailuresAsList(failureDetails));

                    final String label = rs.getString("label");
                    if (ApiServiceUtil.isNotNullOrEmpty(label)) {
                        job.getLabels().put(label, rs.getString("label_value"));
                    }
                }
            }
        } catch (final SQLException se) {
           throw mapSqlNoDataException(se);
        }

        return job;
    }

    /**
     * Call one of the `create_job` database functions and parse the result.
     *
     * @param statement Statement which calls the `create_job` function
     * @return Whether the job was created
     * @throws Exception
     */
    private boolean callCreateJobFunction(final CallableStatement statement) throws Exception {
        try {
            LOG.debug("Calling create_job() database function...");
            final ResultSet rs = statement.executeQuery();
            rs.next();
            return rs.getBoolean("job_created");
        } catch (final SQLException se) {
            //  Determine source of SQL exception and throw appropriate error.
            final String sqlState = se.getSQLState();

            if (sqlState.equals(POSTGRES_NO_DATA_ERROR_CODE)) {
                //  Job id has not been provided.
                throw new BadRequestException(se.getMessage(), se);
            } else if (sqlState.equals(POSTGRES_UNIQUE_VIOLATION_ERROR_CODE)) {
                throw new ForbiddenException("Job already exists", se);
            } else if (sqlState.startsWith(POSTGRES_CONNECTION_EXCEPTION_ERROR_CODE_PREFIX)) {
                throw new ServiceUnavailableException(se.getMessage(), se);
            } else {
                throw se;
            }
        }
    }

    /**
     * Creates the specified job.
     * @return Whether the job was created
     */
    public boolean createJob(final String partitionId, final String jobId, final String name, final String description,
            final String data, final int jobHash, final String taskClassifier,
            final int taskApiVersion, final byte[] taskData, final String taskPipe,
            final String targetPipe, final int delay, final Map labels) throws Exception {
        try (
                final Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                final CallableStatement stmt = conn.prepareCall("{call create_job(?,?,?,?,?,?,?,?,?,?,?,?,?)}")
        ) {
            final List labelArray = buildLabelSqlArray(labels);

            stmt.setString(1, partitionId);
            stmt.setString(2,jobId);
            stmt.setString(3,name);
            stmt.setString(4,description);
            stmt.setString(5,data);
            stmt.setInt(6,jobHash);
            stmt.setString(7,taskClassifier);
            stmt.setInt(8,taskApiVersion);
            stmt.setBytes(9,taskData);
            stmt.setString(10,taskPipe);
            stmt.setString(11,targetPipe);
            stmt.setInt(12,delay);

            Array array;
            if (!labelArray.isEmpty()) {
                array = conn.createArrayOf("VARCHAR", labelArray.toArray());
            } else {
                array = conn.createArrayOf("VARCHAR", new String[0]);
            }
            stmt.setArray(13, array);
            try {
                return callCreateJobFunction(stmt);
            } finally {
                array.free();
            }
        } catch (final SQLException se) {
            throw mapSqlConnectionException(se);
        }
    }

    /**
     * Creates the specified job.
     * @return Whether the job was created
     */
    public boolean createJobWithDependencies(final String partitionId, final String jobId, final String name, final String description,
                                          final String data, final int jobHash, final String taskClassifier,
                                          final int taskApiVersion, final byte[] taskData, final String taskPipe,
                                          final String targetPipe, final List prerequisiteJobIds,
                                          final int delay, final Map labels,
                                          final boolean partitionSuspended) throws Exception {
        try (
                final Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                final CallableStatement stmt = conn.prepareCall("{call create_job(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}")
        ) {
            final String[] prerequisiteJobIdStringArray = getPrerequisiteJobIds(prerequisiteJobIds);
            Array prerequisiteJobIdSQLArray = conn.createArrayOf("varchar", prerequisiteJobIdStringArray);

            final List labelArray = buildLabelSqlArray(labels);

            stmt.setString(1, partitionId);
            stmt.setString(2,jobId);
            stmt.setString(3,name);
            stmt.setString(4,description);
            stmt.setString(5,data);
            stmt.setInt(6,jobHash);
            stmt.setString(7,taskClassifier);
            stmt.setInt(8,taskApiVersion);
            stmt.setBytes(9,taskData);
            stmt.setString(10,taskPipe);
            stmt.setString(11,targetPipe);
            stmt.setArray(12,prerequisiteJobIdSQLArray);
            stmt.setInt(13,delay);

            Array array;
            if (!labelArray.isEmpty()) {
                array = conn.createArrayOf("VARCHAR", labelArray.toArray());
            } else {
                array = conn.createArrayOf("VARCHAR", new String[0]);
            }
            stmt.setArray(14, array);
            stmt.setBoolean(15, partitionSuspended);

            try {
                return callCreateJobFunction(stmt);
            } finally {
                array.free();
                prerequisiteJobIdSQLArray.free();
            }
        } catch (final SQLException se) {
            throw mapSqlConnectionException(se);
        }
    }

    private List buildLabelSqlArray(final Map labels) {
        return labels.entrySet().stream().map(entry -> new String[]{entry.getKey(), entry.getValue()})
                .collect(Collectors.toList());
    }

    private String[] getPrerequisiteJobIds(final List prerequisiteJobIds)
    {
        if (prerequisiteJobIds != null && !prerequisiteJobIds.isEmpty())
        {
            return prerequisiteJobIds.toArray(new String[prerequisiteJobIds.size()]);
        }
        else
        {
            return new String[0];
        }
    }

    /**
     * Deletes the specified job.
     */
    public void deleteJob(final String partitionId, String jobId) throws Exception {

        try (
                Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                CallableStatement stmt = conn.prepareCall("{call delete_job(?,?)}")
        ) {
            stmt.setString(1, partitionId);
            stmt.setString(2,jobId);
            LOG.debug("Calling delete_job() database function...");
            stmt.execute();
        } catch (final SQLException se) {
           throw mapSqlNoDataException(se);
        }
    }

    public int deleteJobs(final String partitionId, String jobIdStartsWith,
                          final List labels, final String filter) throws Exception
    {
        int successfulDeletions = 0;

        final int deleteBatchLimit = appConfig.getDeleteJobsBatchLimit();
        LOG.debug("cancelJobs: Set cancelBatchLimit to {}", deleteBatchLimit);

        try (
                final Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                final CallableStatement stmt = conn.prepareCall("{call delete_jobs(?,?,?,?,?,?)}")
        ) {
            // Expect number of successful deletions to be returned
            stmt.registerOutParameter(1, Types.INTEGER);

            do {
                if (jobIdStartsWith == null) {
                    jobIdStartsWith = "";
                }
                stmt.setString(1, partitionId);
                stmt.setString(2, jobIdStartsWith);
                stmt.setString(3, "");
                stmt.setInt(4, deleteBatchLimit);
                final Array labelsArray;
                if (labels != null) {
                    labelsArray = conn.createArrayOf("VARCHAR", labels.toArray());
                } else {
                    labelsArray = conn.createArrayOf("VARCHAR", new String[0]);
                }
                stmt.setArray(5, labelsArray);
                stmt.setString(6, filter);

                try {
                    LOG.debug("Calling delete_jobs() database function...");
                    stmt.execute();

                    successfulDeletions += stmt.getInt(1);
                } finally {
                    if (labelsArray != null) {
                        labelsArray.free();
                    }
                }
            } while (stmt.getInt(1) > 0);
        } catch (final SQLException se) {
            throw mapSqlNoDataException(se);
        }

        return successfulDeletions;
    }

    public JobStatus getJobStatus(final String partitionId, final String jobId) throws Exception
    {
        try (
            final Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
            final CallableStatement stmt = conn.prepareCall("{call get_job(?,?)}")) {
            stmt.setString(1, partitionId);
            stmt.setString(2, jobId);

            //  Execute a query to get the staus of the specified job.
            LOG.debug("Calling get_job() database function...");
            final ResultSet rs = stmt.executeQuery();
            rs.next();
            return JobStatus.valueOf(rs.getString("status").toUpperCase(Locale.ENGLISH));
        } catch (final SQLException se) {
            throw mapSqlNoDataException(se);
        }
    }

    /**
     * Returns TRUE if the specified job id is active, otherwise FALSE.
     */
    public boolean isJobActive(final String partitionId, String jobId) throws Exception {

        boolean active = false;

        try (
                Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                CallableStatement stmt = conn.prepareCall("{call get_job(?,?)}")
        ) {
            stmt.setString(1, partitionId);
            stmt.setString(2, jobId);

            //  Execute a query to determine if the specified job is active or not.
            LOG.debug("Calling get_job() database function...");
            ResultSet rs = stmt.executeQuery();
            if(rs.next()){
                final JobStatus status =
                    JobStatus.valueOf(rs.getString("status").toUpperCase(Locale.ENGLISH));
                active = status == JobStatus.ACTIVE || status == JobStatus.WAITING;
            }

        } catch (final SQLException se) {
            throwIfUnexpectedException(se);
        }

        return active;
    }

    /**
     * Cancels the specified job.
     */
    public void cancelJob(final String partitionId, String jobId) throws Exception {

        try (
                Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                CallableStatement stmt = conn.prepareCall("{call cancel_job(?,?)}")
        ) {
            stmt.setString(1, partitionId);
            stmt.setString(2,jobId);
            LOG.debug("Calling cancel_job() database function...");
            stmt.execute();
        } catch (final SQLException se) {
            throw mapSqlNoDataException(se);
        }
    }

    /**
     * Cancels the specified jobs
     */
    public int cancelJobs(final String partitionId, String jobIdStartsWith, final List labels, final String filter)
            throws Exception {
        int successfulCancellations = 0;

        final int cancelBatchLimit = appConfig.getCancelJobsBatchLimit();
        LOG.debug("cancelJobs: Set cancelBatchLimit to {}", cancelBatchLimit);

        try (
                final Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                final CallableStatement stmt = conn.prepareCall("{call cancel_jobs(?,?,?,?,?)}")
        ) {
            // Expect number of successful cancellations to be returned
            stmt.registerOutParameter(1, Types.INTEGER);

            do {
                if (jobIdStartsWith == null) {
                    jobIdStartsWith = "";
                }
                stmt.setString(1, partitionId);
                stmt.setString(2, jobIdStartsWith);
                stmt.setInt(3, cancelBatchLimit);
                final Array labelsArray;
                if (labels != null) {
                    labelsArray = conn.createArrayOf("VARCHAR", labels.toArray());
                } else {
                    labelsArray = conn.createArrayOf("VARCHAR", new String[0]);
                }
                stmt.setArray(4, labelsArray);
                stmt.setString(5, filter);

                try {
                    LOG.debug("Calling cancel_jobs() database function...");
                    stmt.execute();

                    successfulCancellations += stmt.getInt(1);
                } finally {
                    if (labelsArray != null) {
                        labelsArray.free();
                    }
                }
            } while (stmt.getInt(1) > 0);
        } catch (final SQLException se) {
            throw mapSqlNoDataException(se);
        }

        return successfulCancellations;
    }

    /**
     * Pauses the specified job.
     */
    public void pauseJob(final String partitionId, String jobId) throws Exception {

        try (
                Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                CallableStatement stmt = conn.prepareCall("{call pause_job(?,?)}")
        ) {
            stmt.setString(1, partitionId);
            stmt.setString(2, jobId);
            LOG.debug("Calling pause_job() database function...");
            stmt.execute();
        } catch (final SQLException se) {
            throw mapSqlNoDataException(se);
        }
    }

    /**
     * Resumes the specified job.
     */
    public void resumeJob(final String partitionId, String jobId) throws Exception {

        try (
                Connection conn = DatabaseConnectionProvider.getConnection(appConfig);
                CallableStatement stmt = conn.prepareCall("{call resume_job(?,?)}")
        ) {
            stmt.setString(1, partitionId);
            stmt.setString(2, jobId);
            LOG.debug("Calling resume_job() database function...");
            stmt.execute();
        } catch (final SQLException se) {
            throw mapSqlNoDataException(se);
        }
    }

    /**
     * Parses the failure details string returned from the database and returns as a list.
     */
    private static List getFailuresAsList (String failureDetails) throws Exception {

        List failures = new ArrayList<>();

        if(failureDetails == null || failureDetails.isEmpty()) {
            return failures;
        }

        //  Split on newline character.
        for (String failure: failureDetails.split("\\r?\\n")){
            if (failure.startsWith("{")) {
                JSONObject jFailure = new JSONObject(failure);
                final Failure f;
                if (jFailure.has("root_failure")) {
                    f = getFailureFromJsonObject(new JSONObject(jFailure.getString("failure_details")));
                    f.setFailureSource(jFailure.getString("root_failure") + ":" + f.getFailureSource());
                } else {
                    f = getFailureFromJsonObject(jFailure);
                }
                failures.add(f);
            } else {
                //  Valid failure JSON not detected.
                Failure f = new Failure();
                f.setFailureId(FAILURE_PROPERTY_MISSING);
                f.setFailureTime(System.currentTimeMillis());
                f.failureSource(FAILURE_PROPERTY_MISSING);
                f.failureMessage(failure);
                failures.add(f);
            }
        }

        return failures;

    }

    private static Failure getFailureFromJsonObject(final JSONObject json) throws JSONException, ParseException
    {
        final Failure f = new Failure();
        f.setFailureId(json.getString("failureId"));
        f.setFailureTime(getDate(json.getString("failureTime")));
        f.failureSource(json.getString("failureSource"));
        f.failureMessage(json.getString("failureMessage"));
        return f;
    }

    /**
     * Returns java.util.date from a string.
     */
    private static long getDate(String dateString) throws ParseException {

        Instant instant = Instant.parse ( dateString );
        return instant.toEpochMilli();

    }

    private static Exception mapSqlConnectionException(final SQLException se) throws Exception
    {
        final String sqlState = se.getSQLState();
        if (sqlState.startsWith(POSTGRES_CONNECTION_EXCEPTION_ERROR_CODE_PREFIX)) {
            return new ServiceUnavailableException(se.getMessage(), se);
        } else {
            return se;
        }
    }

    private static Exception mapSqlNoDataException(final SQLException se) throws Exception
    {
        final String sqlState = se.getSQLState();
        if (sqlState.equals(POSTGRES_NO_DATA_ERROR_CODE)) {
            //  Client error, such as not providing a job id, or trying to pause a cancelled job etc.
            return new BadRequestException(se.getMessage(), se);
        } else if (sqlState.equals(POSTGRES_NO_DATA_FOUND_ERROR_CODE)) {
            //  No data found for the specified job id.
            return new NotFoundException(se.getMessage(), se);
        } else if (sqlState.startsWith(POSTGRES_CONNECTION_EXCEPTION_ERROR_CODE_PREFIX)) {
            // Connection exception.
            return new ServiceUnavailableException(se.getMessage(), se);
        } else {
            return se;
        }
    }

    private static void throwIfUnexpectedException(final SQLException se) throws Exception
    {
        final String sqlState = se.getSQLState();
        if (sqlState.equals(POSTGRES_NO_DATA_FOUND_ERROR_CODE)) {
            // job missing - don't throw anything, return void
        } else if (sqlState.startsWith(POSTGRES_CONNECTION_EXCEPTION_ERROR_CODE_PREFIX)) {
            // Connection exception
            throw new ServiceUnavailableException(se.getMessage(), se);
        } else {
            throw se;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy