org.opendatakit.persistence.engine.pgres.TaskLockImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.persistence.engine.pgres.TaskLockImpl.java

Source

/*
 * Copyright (C) 2010 University of Washington
 *
 * 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 org.opendatakit.persistence.engine.pgres;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.commons.logging.LogFactory;
import org.opendatakit.persistence.CommonFieldsBase;
import org.opendatakit.persistence.DataField;
import org.opendatakit.persistence.Datastore;
import org.opendatakit.persistence.EntityKey;
import org.opendatakit.persistence.ITaskLockType;
import org.opendatakit.persistence.TaskLock;
import org.opendatakit.persistence.engine.DatastoreAccessMetrics;
import org.opendatakit.persistence.exception.ODKDatastoreException;
import org.opendatakit.persistence.exception.ODKEntityNotFoundException;
import org.opendatakit.persistence.exception.ODKTaskLockException;
import org.opendatakit.security.User;
import org.postgresql.util.PSQLException;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;

/**
 * 
 * @author wbrunette@gmail.com
 * @author mitchellsundt@gmail.com
 * 
 */
public class TaskLockImpl implements TaskLock {

    private static final String PERSISTENCE_LAYER_PROBLEM = "Persistence layer failure";

    final DatastoreAccessMetrics dam;
    final DatastoreImpl datastore;
    final User user;

    TaskLockImpl(DatastoreImpl datastore, DatastoreAccessMetrics dam, User user) {
        this.datastore = datastore;
        this.dam = dam;
        this.user = user;
    }

    private static final String K_BQ = "\"";

    private TaskLockTable doTransaction(TaskLockTable entity, long l)
            throws ODKEntityNotFoundException, ODKTaskLockException {
        boolean first;

        final List<String> stmts = new ArrayList<String>();

        String uri = entity.getUri();

        StringBuilder stringBuilder = new StringBuilder();
        String tableName = K_BQ + datastore.getDefaultSchemaName() + K_BQ + "." + K_BQ + TaskLockTable.TABLE_NAME
                + K_BQ;

        stringBuilder.append("'").append(user.getUriUser().replaceAll("'", "''")).append("'");
        String uriUserInline = stringBuilder.toString();
        stringBuilder.setLength(0);
        stringBuilder.append("'").append(uri.replaceAll("'", "''")).append("'");
        String uriLockInline = stringBuilder.toString();
        stringBuilder.setLength(0);
        stringBuilder.append("'").append(entity.getFormId().replaceAll("'", "''")).append("'");
        String formIdInline = stringBuilder.toString();
        stringBuilder.setLength(0);
        stringBuilder.append("'").append(entity.getTaskType().replaceAll("'", "''")).append("'");
        String taskTypeInline = stringBuilder.toString();
        stringBuilder.setLength(0);
        stringBuilder.append("interval '").append(l).append(" milliseconds'");
        String lifetimeIntervalMilliseconds = stringBuilder.toString();
        stringBuilder.setLength(0);

        stringBuilder.append("LOCK TABLE ").append(tableName).append(" IN ACCESS EXCLUSIVE MODE");
        stmts.add(stringBuilder.toString());
        stringBuilder.setLength(0);

        dam.recordPutUsage(TaskLockTable.TABLE_NAME);
        if (!entity.isFromDatabase()) {
            // insert a new record (prospective lock)
            stringBuilder.append("INSERT INTO ");
            stringBuilder.append(tableName);
            stringBuilder.append(" (");
            first = true;
            for (DataField dataField : entity.getFieldList()) {
                if (!first) {
                    stringBuilder.append(",");
                }
                first = false;
                stringBuilder.append(K_BQ);
                stringBuilder.append(dataField.getName());
                stringBuilder.append(K_BQ);
            }
            first = true;
            stringBuilder.append(") VALUES ( ");
            for (DataField dataField : entity.getFieldList()) {
                if (!first) {
                    stringBuilder.append(",");
                }
                first = false;
                if (dataField.equals(entity.creationDate) || dataField.equals(entity.lastUpdateDate)) {
                    stringBuilder.append("NOW()");
                } else if (dataField.equals(entity.creatorUriUser) || dataField.equals(entity.lastUpdateUriUser)) {
                    stringBuilder.append(uriUserInline);
                } else if (dataField.equals(entity.formId)) {
                    stringBuilder.append(formIdInline);
                } else if (dataField.equals(entity.taskType)) {
                    stringBuilder.append(taskTypeInline);
                } else if (dataField.equals(entity.primaryKey)) {
                    stringBuilder.append(uriLockInline);
                } else if (dataField.equals(entity.expirationDateTime)) {
                    stringBuilder.append(" NOW() + ");
                    stringBuilder.append(lifetimeIntervalMilliseconds);
                } else {
                    throw new IllegalStateException("unexpected case " + dataField.getName());
                }
            }
            stringBuilder.append(")");
            stmts.add(stringBuilder.toString());
            stringBuilder.setLength(0);
        } else {
            // update existing record (prospective lock)
            stringBuilder.append("UPDATE ");
            stringBuilder.append(tableName);
            stringBuilder.append(" SET ");
            first = true;
            for (DataField f : entity.getFieldList()) {
                if (f == entity.primaryKey)
                    continue;
                if (!first) {
                    stringBuilder.append(",");
                }
                first = false;
                stringBuilder.append(K_BQ);
                stringBuilder.append(f.getName());
                stringBuilder.append(K_BQ);
                stringBuilder.append(" = ");
                if (f.equals(entity.creationDate) || f.equals(entity.lastUpdateDate)) {
                    stringBuilder.append("NOW()");
                } else if (f.equals(entity.creatorUriUser) || f.equals(entity.lastUpdateUriUser)) {
                    stringBuilder.append(uriUserInline);
                } else if (f.equals(entity.formId)) {
                    stringBuilder.append(formIdInline);
                } else if (f.equals(entity.taskType)) {
                    stringBuilder.append(taskTypeInline);
                } else if (f.equals(entity.primaryKey)) {
                    stringBuilder.append(uriLockInline);
                } else if (f.equals(entity.expirationDateTime)) {
                    stringBuilder.append(" NOW() + ");
                    stringBuilder.append(lifetimeIntervalMilliseconds);
                } else {
                    throw new IllegalStateException("unexpected case " + f.getName());
                }
            }
            stringBuilder.append(" WHERE ");
            stringBuilder.append(K_BQ);
            stringBuilder.append(entity.primaryKey.getName());
            stringBuilder.append(K_BQ);
            stringBuilder.append(" = ");
            stringBuilder.append(uriLockInline);
            stmts.add(stringBuilder.toString());
            stringBuilder.setLength(0);
        }
        // delete stale locks (don't care who's)
        dam.recordDeleteUsage(TaskLockTable.TABLE_NAME);
        stringBuilder.append("DELETE FROM ").append(tableName).append(" WHERE ");
        stringBuilder.append(K_BQ).append(entity.expirationDateTime.getName()).append(K_BQ).append(" <= NOW()");
        stmts.add(stringBuilder.toString());
        stringBuilder.setLength(0);
        // delete prospective locks which are not the oldest for that resource and
        // task type
        dam.recordDeleteUsage(TaskLockTable.TABLE_NAME);
        stringBuilder.append("DELETE FROM ").append(tableName).append(" WHERE ");
        stringBuilder.append(K_BQ).append(entity.formId.getName()).append(K_BQ).append(" = ").append(formIdInline)
                .append(" AND ");
        stringBuilder.append(K_BQ).append(entity.taskType.getName()).append(K_BQ).append(" = ")
                .append(taskTypeInline).append(" AND ");
        stringBuilder.append(K_BQ).append(entity.expirationDateTime.getName()).append(K_BQ);
        stringBuilder.append(" > (SELECT MIN(t3.").append(K_BQ).append(entity.expirationDateTime.getName())
                .append(K_BQ);
        stringBuilder.append(") FROM ").append(tableName).append(" AS t3 WHERE t3.");
        stringBuilder.append(K_BQ).append(entity.formId.getName()).append(K_BQ).append(" = ").append(formIdInline)
                .append(" AND t3.");
        stringBuilder.append(K_BQ).append(entity.taskType.getName()).append(K_BQ).append(" = ")
                .append(taskTypeInline).append(")");
        stmts.add(stringBuilder.toString());
        stringBuilder.setLength(0);
        // delete our entry if it collides with another entry with exactly 
        // this time.
        stringBuilder.append("DELETE FROM ").append(tableName).append(" WHERE ");
        stringBuilder.append(K_BQ).append(entity.formId.getName()).append(K_BQ).append(" = ").append(formIdInline)
                .append(" AND ");
        stringBuilder.append(K_BQ).append(entity.taskType.getName()).append(K_BQ).append(" = ")
                .append(taskTypeInline).append(" AND ");
        stringBuilder.append(K_BQ).append(entity.primaryKey.getName()).append(K_BQ).append(" = ")
                .append(uriLockInline).append(" AND ");
        stringBuilder.append("1 < (SELECT COUNT(t3.").append(K_BQ).append(entity.expirationDateTime.getName())
                .append(K_BQ);
        stringBuilder.append(") FROM ").append(tableName).append(" AS t3 WHERE t3.");
        stringBuilder.append(K_BQ).append(entity.formId.getName()).append(K_BQ).append(" = ").append(formIdInline)
                .append(" AND t3.");
        stringBuilder.append(K_BQ).append(entity.taskType.getName()).append(K_BQ).append(" = ")
                .append(taskTypeInline).append(")");
        stmts.add(stringBuilder.toString());
        stringBuilder.setLength(0);
        // assert: only the lock that holds the resource for that task type appears
        // in the task lock table
        TaskLockTable relation;
        try {

            JdbcTemplate jdbc = datastore.getJdbcConnection();
            jdbc.execute(new ConnectionCallback<Object>() {

                @Override
                public Object doInConnection(Connection conn) throws SQLException, DataAccessException {
                    boolean oldAutoCommitValue = conn.getAutoCommit();
                    int oldTransactionValue = conn.getTransactionIsolation();
                    try {
                        conn.setAutoCommit(false);
                        conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
                        Statement stmt = conn.createStatement();
                        for (String s : stmts) {
                            // for debugging: LogFactory.getLog(TaskLockImpl.class).info(s);
                            stmt.execute(s);
                        }
                        conn.commit();
                    } catch (PSQLException e) {
                        e.printStackTrace();
                        conn.rollback();
                    } catch (Exception e) {
                        e.printStackTrace();
                        conn.rollback();
                    }
                    conn.setTransactionIsolation(oldTransactionValue);
                    conn.setAutoCommit(oldAutoCommitValue);
                    return null;
                }

            });

            relation = TaskLockTable.assertRelation(datastore, user);
        } catch (Exception e) {
            throw new ODKTaskLockException(PERSISTENCE_LAYER_PROBLEM, e);
        }
        return (TaskLockTable) datastore.getEntity(relation, entity.getUri(), user);
    }

    @Override
    public boolean obtainLock(String lockId, String formId, ITaskLockType taskType) {
        boolean result = false;
        try {
            TaskLockTable relation = TaskLockTable.assertRelation(datastore, user);
            TaskLockTable entity = datastore.createEntityUsingRelation(relation, user);
            entity.setStringField(entity.primaryKey, lockId);
            entity.setFormId(formId);
            entity.setTaskType(taskType.getName());
            entity = doTransaction(entity, taskType.getLockExpirationTimeout());
            result = true;
        } catch (ODKEntityNotFoundException e) {
            // didn't gain the lock...
        } catch (ODKTaskLockException e) {
            // unexpected failure...
            e.printStackTrace();
        } catch (ODKDatastoreException e) {
            // unexpected failure...
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public boolean renewLock(String lockId, String formId, ITaskLockType taskType) {
        boolean result = false;
        try {
            TaskLockTable relation = TaskLockTable.assertRelation(datastore, user);
            TaskLockTable entity = datastore.getEntity(relation, lockId, user);
            if (!(entity.getFormId().equals(formId) && entity.getTaskType().equals(taskType.getName()))) {
                throw new IllegalArgumentException("formId or taskType don't match datastore values");
            }
            entity = doTransaction(entity, taskType.getLockExpirationTimeout());
            result = true;
        } catch (IllegalArgumentException e) {
            // unexpected failure...
            e.printStackTrace();
        } catch (ODKEntityNotFoundException e) {
            // unexpected failure...
            e.printStackTrace();
        } catch (ODKDatastoreException e) {
            // unexpected failure...
            e.printStackTrace();
        } catch (ODKTaskLockException e) {
            // unexpected failure...
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public boolean releaseLock(String lockId, String formId, ITaskLockType taskType) {
        boolean result = false;
        try {
            TaskLockTable relation = TaskLockTable.assertRelation(datastore, user);
            // we don't have the record that we want to delete; construct
            // the entity key from the relation and the URI for the record.
            datastore.deleteEntity(new EntityKey(relation, lockId), user);
            result = true;
        } catch (ODKDatastoreException e) {
            // if we see a lot of these, we are running too long between renewals
            LogFactory.getLog(TaskLockImpl.class).info("delete of taskLock threw exception!");
            e.printStackTrace();
        }
        return result;
    }

    public static class TaskLockTable extends CommonFieldsBase {
        static final String TABLE_NAME = "_task_lock";

        private static final DataField FORM_ID = new DataField("FORM_ID", DataField.DataType.STRING, false, 4096L);
        private static final DataField TASK_TYPE = new DataField("TASK_TYPE", DataField.DataType.STRING, false,
                80L);
        private static final DataField EXPIRATION_DATETIME = new DataField("EXPIRATION_DATETIME",
                DataField.DataType.DATETIME, true);

        DataField formId;
        DataField taskType;
        DataField expirationDateTime;

        TaskLockTable(String schema) {
            super(schema, TABLE_NAME);
            fieldList.add(formId = new DataField(FORM_ID));
            fieldList.add(taskType = new DataField(TASK_TYPE));
            fieldList.add(expirationDateTime = new DataField(EXPIRATION_DATETIME));
        }

        TaskLockTable(TaskLockTable ref, User user) {
            super(ref, user);
            formId = ref.formId;
            taskType = ref.taskType;
            expirationDateTime = ref.expirationDateTime;
        }

        String getFormId() {
            return getStringField(formId);
        }

        void setFormId(String value) {
            if (!setStringField(formId, value)) {
                throw new IllegalStateException("overflow formId");
            }
        }

        String getTaskType() {
            return getStringField(taskType);
        }

        void setTaskType(String value) {
            if (!setStringField(taskType, value)) {
                throw new IllegalStateException("overflow taskType");
            }
        }

        Date getExpirationDateTime() {
            return getDateField(expirationDateTime);
        }

        void setExpirationDateTime(Date value) {
            setDateField(expirationDateTime, value);
        }

        @Override
        public CommonFieldsBase getEmptyRow(User user) {
            return new TaskLockTable(this, user);
        }

        static TaskLockTable relation = null;

        public static void resetSingletonReference() {
            relation = null;
        }

        public static synchronized final TaskLockTable assertRelation(Datastore datastore, User user)
                throws ODKDatastoreException {
            if (relation == null) {
                TaskLockTable relationPrototype;
                relationPrototype = new TaskLockTable(datastore.getDefaultSchemaName());
                datastore.assertRelation(relationPrototype, user);
                relation = relationPrototype;
            }
            return relation;
        }
    }
}