Pessimistic.php 9.6 KB
Newer Older
1
<?php
zYne's avatar
zYne committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
 *  $Id$
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.phpdoctrine.com>.
 */
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
/**
 * Class for handling the pessimistic offline locking of {@link Doctrine_Record}s.
 * Offline locking of records comes in handy where you need to make sure that
 * a time-consuming task on a record or many records, which is spread over several
 * page requests can't be interfered by other users. 
 * 
 * @author  Roman Borschel <roman@code-factory.org>
 * @license LGPL
 * @since   1.0
 */
class Doctrine_Locking_Manager_Pessimistic
{
    /**
     * The datasource that is used by the locking manager
     *
zYne's avatar
zYne committed
36
     * @var Doctrine_Connection object
37 38 39 40 41
     */
    private $_dataSource;
    /**
     * The database table name for the lock tracking
     */
doctrine's avatar
doctrine committed
42
    private $_lockTable = 'doctrine_lock_tracking';
43

zYne's avatar
zYne committed
44

45 46 47
    /**
     * Constructs a new locking manager object
     * 
zYne's avatar
zYne committed
48
     * When the CREATE_TABLES attribute of the connection on which the manager
49 50
     * is supposed to work on is set to true, the locking table is created.
     *
zYne's avatar
zYne committed
51
     * @param Doctrine_Connection $dataSource The database connection to use
52
     */
zYne's avatar
zYne committed
53
    public function __construct(Doctrine_Connection $dataSource)
54 55 56 57 58 59
    {
        $this->_dataSource = $dataSource;
        
        if($this->_dataSource->getAttribute(Doctrine::ATTR_CREATE_TABLES) === true)
        {
            $columns = array();
60 61
            $columns['object_type']        = array('string',  50, array('notnull' => true, 'primary' => true));
            $columns['object_key']         = array('string', 250, array('notnull' => true, 'primary' => true));
zYne's avatar
zYne committed
62 63
            $columns['user_ident']         = array('string',  50, array('notnull' => true));
            $columns['timestamp_obtained'] = array('integer', 10, array('notnull' => true));
64 65
            
            $dataDict = new Doctrine_DataDict($this->_dataSource->getDBH());
doctrine's avatar
doctrine committed
66
            $dataDict->createTable($this->_lockTable, $columns);
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
        }
               
    }

    /**
     * Obtains a lock on a {@link Doctrine_Record}
     *
     * @param  Doctrine_Record $record     The record that has to be locked
     * @param  mixed           $userIdent  A unique identifier of the locking user
     * @return boolean  TRUE if the locking was successful, FALSE if another user
     *                  holds a lock on this record
     * @throws Doctrine_Locking_Exception  If the locking failed due to database errors
     */
    public function getLock(Doctrine_Record $record, $userIdent)
    {
        $objectType = $record->getTable()->getComponentName();
83
        $key        = $record->obtainIdentifier();
84 85 86 87 88 89 90 91 92 93 94 95 96 97
        
        $gotLock = false;
        
        if(is_array($key))
        {
            // Composite key
            $key = implode('|', $key);
        }
        
        try
        {
            $dbh = $this->_dataSource->getDBH();
            $dbh->beginTransaction();
            
doctrine's avatar
doctrine committed
98
            $stmt = $dbh->prepare("INSERT INTO $this->_lockTable
99 100 101 102 103 104 105 106 107 108
                                          (object_type, object_key, user_ident, timestamp_obtained)
                                   VALUES (:object_type, :object_key, :user_ident, :ts_obtained)");
            $stmt->bindParam(':object_type', $objectType);
            $stmt->bindParam(':object_key', $key);
            $stmt->bindParam(':user_ident', $userIdent);
            $stmt->bindParam(':ts_obtained', time());
            
            try {
                $stmt->execute();
                $gotLock = true;
109 110 111
            
            // we catch an Exception here instead of PDOException since we might also be catching Doctrine_Exception
            } catch(Exception $pkviolation) {
112 113 114 115 116 117 118 119 120 121
                // PK violation occured => existing lock!
            }
            
            if(!$gotLock)
            {
                $lockingUserIdent = $this->_getLockingUserIdent($objectType, $key);
                if($lockingUserIdent !== null && $lockingUserIdent == $userIdent)
                {
                    $gotLock = true; // The requesting user already has a lock
                    // Update timestamp
doctrine's avatar
doctrine committed
122
                    $stmt = $dbh->prepare("UPDATE $this->_lockTable SET timestamp_obtained = :ts
123 124 125 126 127 128 129 130 131 132 133 134 135 136
                                           WHERE object_type = :object_type AND
                                                 object_key  = :object_key  AND
                                                 user_ident  = :user_ident");
                    $stmt->bindParam(':ts', time());
                    $stmt->bindParam(':object_type', $objectType);
                    $stmt->bindParam(':object_key', $key);
                    $stmt->bindParam(':user_ident', $lockingUserIdent);
                    $stmt->execute();
                }
            }

            $dbh->commit();
                       
        }
137
        catch(Exception $pdoe)
138
        {
139
            $dbh->rollback();
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
            throw new Doctrine_Locking_Exception($pdoe->getMessage());
        }
        
        return $gotLock;
    }

    /**
     * Releases a lock on a {@link Doctrine_Record}
     *
     * @param  Doctrine_Record $record    The record for which the lock has to be released
     * @param  mixed           $userIdent The unique identifier of the locking user
     * @return boolean  TRUE if a lock was released, FALSE if no lock was released
     * @throws Doctrine_Locking_Exception If the release procedure failed due to database errors
     */
    public function releaseLock(Doctrine_Record $record, $userIdent)
    {
        $objectType = $record->getTable()->getComponentName();
157
        $key        = $record->obtainIdentifier();
158 159 160 161 162 163 164 165 166 167
        
        if(is_array($key))
        {
            // Composite key
            $key = implode('|', $key);
        }
        
        try
        {
            $dbh = $this->_dataSource->getDBH();
doctrine's avatar
doctrine committed
168
            $stmt = $dbh->prepare("DELETE FROM $this->_lockTable WHERE
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
                                        object_type = :object_type AND
                                        object_key  = :object_key  AND
                                        user_ident  = :user_ident");
            $stmt->bindParam(':object_type', $objectType);
            $stmt->bindParam(':object_key', $key);
            $stmt->bindParam(':user_ident', $userIdent);
            $stmt->execute();
            
            $count = $stmt->rowCount();
            
            return ($count > 0);
                        
        }
        catch(PDOException $pdoe)
        {
            throw new Doctrine_Locking_Exception($pdoe->getMessage());
        }
    }

    /**
     * Gets the unique user identifier of a lock
     *
     * @param  string $objectType  The type of the object (component name)
     * @param  mixed  $key         The unique key of the object
     * @return mixed  The unique user identifier for the specified lock
     * @throws Doctrine_Locking_Exception If the query failed due to database errors
     */
    private function _getLockingUserIdent($objectType, $key)
    {
        if(is_array($key))
        {
            // Composite key
            $key = implode('|', $key);
        }
        
        try
        {
            $dbh = $this->_dataSource->getDBH();
            $stmt = $dbh->prepare("SELECT user_ident
doctrine's avatar
doctrine committed
208
                                   FROM $this->_lockTable
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
                                   WHERE object_type = :object_type AND object_key = :object_key");
            $stmt->bindParam(':object_type', $objectType);
            $stmt->bindParam(':object_key', $key);
            $success = $stmt->execute();
            
            if(!$success)
            {
                throw new Doctrine_Locking_Exception("Failed to determine locking user");
            }
            
            $user_ident = $stmt->fetchColumn();
        }
        catch(PDOException $pdoe)
        {
            throw new Doctrine_Locking_Exception($pdoe->getMessage());
        }
        
        return $user_ident;
    }

    /**
     * Releases locks older than a defined amount of seconds
     * 
     * When called without parameters all locks older than 15 minutes are released.
     *
     * @param  integer $age  The maximum valid age of locks in seconds
235
     * @return integer The number of locks that have been released
236 237 238 239 240 241 242 243 244
     * @throws Doctrine_Locking_Exception If the release process failed due to database errors
     */
    public function releaseAgedLocks($age = 900)
    {
        $age = time() - $age;
        
        try
        {
            $dbh = $this->_dataSource->getDBH();
doctrine's avatar
doctrine committed
245
            $stmt = $dbh->prepare("DELETE FROM $this->_lockTable WHERE timestamp_obtained < :age");
246 247 248 249 250
            $stmt->bindParam(':age', $age);
            $stmt->execute();
            
            $count = $stmt->rowCount();
            
251
            return $count;
252 253 254 255 256 257 258 259 260 261
        }
        catch(PDOException $pdoe)
        {
            throw new Doctrine_Locking_Exception($pdoe->getMessage());
        }
    }

}


262