ClientPool.php #1

  • //
  • guest/
  • thomas_gray/
  • jambox/
  • main/
  • swarm/
  • library/
  • P4/
  • ClientPool/
  • ClientPool.php
  • View
  • Commits
  • Open Download .zip Download (15 KB)
<?php
/**
 * Perforce Swarm
 *
 * @copyright   2013 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */

namespace P4\ClientPool;

use P4\ClientPool\Exception;
use P4\Model\Connected\ConnectedAbstract;
use P4\Spec\Depot;
use P4\Uuid\Uuid;

/**
 * Manages a pool of client workspaces.
 */
class ClientPool extends ConnectedAbstract
{
    const MANAGEMENT_LOCK   = 'manage';
    const LOCK_EXTENSION    = '.lock';
    const SPIN_DELAY        = 50000;        // delay between locking attempts (50ms)

    protected $root         = null;
    protected $prefix       = null;
    protected $max          = 10;

    protected $handles      = array();
    protected $clients      = array();

    /**
     * Set the maximum number of clients to provision.
     *
     * Setting to 0 or null will leave it unlimited.
     *
     * @param   int|null        $max    the maximum number of clients to provision
     * @return  ClientPool      to maintain a fluent interface
     */
    public function setMax($max)
    {
        $this->max = $max;
        return $this;
    }

    /**
     * Retrieves the max client limit.
     *
     * @return int|null     the max number of clients 0/null for unlimited
     */
    public function getMax()
    {
        return $this->max;
    }

    /**
     * Specify the prefix used for client ids.
     *
     * @param   string|null $prefix     the prefix to apply to client ids (e.g. 'swarm-')
     * @return  ClientPool  to maintain a fluent interface
     */
    public function setPrefix($prefix)
    {
        $this->prefix = $prefix;
        return $this;
    }

    /**
     * Retreives the prefix used for client ids.
     *
     * @return  string   the prefix to apply to client ids (e.g. 'swarm-')
     */
    public function getPrefix()
    {
        return $this->prefix;
    }

    /**
     * Specify the root folder to store client workspaces and locks under.
     * Each generated client will store its data in a sub-folder named for
     * the client id. Also, client locks (<client-id>.lock) and the management
     * lock will be stored in this folder.
     *
     * @param   string|null     $root   the root folder to use for client workspaces
     * @return  ClientPool      to maintain a fluent interface
     */
    public function setRoot($root)
    {
        $this->root = $root !== null ? rtrim($root, '/') : null;
        return $this;
    }

    /**
     * The root folder for client workspaces. See setRoot for details.
     *
     * @return  string|null     the root folder to use for client workspaces
     */
    public function getRoot()
    {
        return $this->root;
    }

    /**
     * Will lock a client workspace for this thread to use and return the id.
     *
     * By default, the thread will just be given the existing locked client if one
     * is already being held. Getting multiple clients in a single client can be
     * achieved via the $reuse flag.
     *
     * It isn't guaranteed the returned client will have an up to date view map.
     * If you plan to sync files its advised you run 'reset' against the client first.
     *
     * @param   bool        $reuse      optional - if a client has already been requested by
     *                                  this thread its returned again if reuse is true (default).
     *                                  specifying reuse false will get an additional client.
     * @param   bool        $blocking   optional - by default, we will wait indefinitely to get a
     *                                  client. if false is passed we'll return false if we cannot
     *                                  get/add a client on our first attempt.
     * @return  bool|string             the client id to use or false if we were non-blocking and
     *                                  a client couldn't be locked/added on our first attempt.
     * @throws  Exception               if the root hasn't been set or file permissions prevent opening locks
     */
    public function grab($reuse = true, $blocking = true)
    {
        if (!$this->root) {
            throw new Exception(
                'No root has been set, unable to get client.'
            );
        }

        // if our root isn't already present, attempt to create it
        if (!is_dir($this->root)) {
            mkdir($this->root, 0700, true);
        }

        // if re-use is allowed and we have client(s) already simply return the first one
        if ($this->handles && $reuse) {
            reset($this->handles);
            return key($this->handles);
        }

        // we'll keep looping until we can successfully get a client
        // (unless non-blocking in which case we try once and give up).
        // first, we try to get a lock on an existing client; if that
        // fails we'll take a management lock and add a new client if
        // the server isn't already at 'max' clients.
        $p4 = $this->getConnection();
        while (1) {
            // retrieve the known locks, barring the management lock.
            $locks = glob($this->root . '/' . $this->prefix . '*' . static::LOCK_EXTENSION);
            $locks = array_map('basename', $locks);
            $locks = array_diff($locks, array(static::MANAGEMENT_LOCK . static::LOCK_EXTENSION));

            // attempt to get a lock on any existing client
            foreach ($locks as $lock) {
                $file = @fopen($this->root . '/' . $lock, 'c');

                if ($file === false) {
                    throw new Exception(
                        'Unable to open client pool lock, this likely indicates file permission problems'
                    );
                }

                // if we can get the lock, extract client id, record handle and return!
                if (flock($file, LOCK_EX | LOCK_NB)) {
                    $id = basename($lock, static::LOCK_EXTENSION);

                    // don't let handle fall out of scope or the lock will release.
                    $this->handles[$id] = $file;

                    // push connection's old client onto the clients
                    // array and set the grabbed client on it.
                    $this->clients += array(spl_object_hash($p4) => array());
                    $this->clients[spl_object_hash($p4)][] = array(
                        'old' => $p4->getClient(),
                        'new' => $id
                    );
                    $p4->setClient($id);

                    // if the client doesn't exist call reset to create it
                    $info = $p4->getInfo();
                    if ($info['clientName'] == '*unknown*') {
                        $this->reset();
                    }

                    return $id;
                }

                // didn't get a lock; close file handle
                fclose($file);
            }

            // if we aren't already maxed out on workspaces try and add one
            if (!$this->max || count($locks) < $this->max) {
                // if we are able to add a client, we continue which will loop
                // us around and attempt to lock our new client without delay.
                if ($this->provision()) {
                    continue;
                }
            }

            // if we were told to not be blocking; time to bail
            if (!$blocking) {
                return false;
            }

            // looks like we can't add another workspace and they are all taken.
            // sleep a bit and start all over.
            usleep(static::SPIN_DELAY);
        }
    }

    /**
     * Release previously locked client(s). It is recommended you release your
     * client as soon as you are done with it.
     *
     * @return  ClientPool      to maintain a fluent interface
     */
    public function release()
    {
        // if we have an entry for this object with this current client value
        // at the top of the stack set back the original client id.
        $client = null;
        $p4     = $this->getConnection();
        $hash   = spl_object_hash($p4);
        $client = $p4->getClient();
        if (isset($this->clients[$hash]) && $this->clients[$hash]) {
            $historic = array_pop($this->clients[$hash]);
            if ($p4->getClient() == $historic['new']) {
                $p4->setClient($historic['old']);
            }
        }

        // deal with closing handle(s)
        foreach ($this->handles as $key => $handle) {
            if ($client && $client != $key) {
                continue;
            }

            flock($handle, LOCK_UN);
            fclose($handle);
            unset($this->handles[$key]);
        }

        return $this;
    }

    /**
     * Resets the passed client identifier to the correct view, root and host.
     * If the specified client doesn't already exist it will be created.
     *
     * @param   bool                $clearFiles     optional - if true, default, will attempt to revert and remove
     *                                              files. if false, only the client settings are reset.
     * @param   string|null|bool    $stream         optional - a specific stream to point the client at
     *                                              if null/false (default) the client will map all depots.
     * @param   bool                $mapAllDepots   optional - include all mappable depots (excludes archive and unload)
    *                                               by default this is false and only local and stream depots are mapped
     * @return  ClientPool          to maintain a fluent interface
     */
    public function reset($clearFiles = true, $stream = null, $mapAllDepots = false)
    {
        $p4     = $this->getConnection();
        $client = $p4->getClient();
        $root   = $this->root . '/' . $client;
        $view   = array();

        // generate a view which includes all depots if $mapAllDepots is true, otherwise only local and stream depots
        if (!$stream) {
            foreach (Depot::fetchAll(null, $p4) as $depot) {
                $type = $depot->get('Type');

                // exclude 'archive' and 'unload' depots as these cannot be mapped in client view
                if ($type === 'archive' || $type === 'unload') {
                    continue;
                }

                if ($mapAllDepots || $type == 'local' || $type == 'stream') {
                    $view[] = '"//' . $depot->getId() . '/..." "//' . $client . '/' . $depot->getId() . '/..."';
                }
            }
        }

        // force the client to have current/correct settings
        $data = $p4->run('client', array('-o', $client))->expandSequences()->getData(-1);
        $p4->run(
            'client',
            '-i',
            array(
                'Host'   => '',
                'Root'   => $root,
                'View'   => $view,
                'Stream' => $stream
            ) + $data
        );

        // ensure the root folder and lock file exist
        is_dir($root) ?: mkdir($root);

        // clear files if needed and return
        return $clearFiles ? $this->clearFiles($p4) : $this;
    }

    /**
     * Revert and remove any file in this client.
     *
     * @return  ClientPool          to maintain a fluent interface
     */
    public function clearFiles()
    {
        // revert and flush (sync none without removing local files) the client
        $p4 = $this->getConnection();
        $p4->run('revert', array('-k', '//...'));
        $p4->run('flush', '//...#none');

        // remove the contents of the client
        $this->removeDirectory($this->root . '/' . $p4->getClient(), true, false);

        return $this;
    }

    /**
     * Takes a management lock and defines a new client id if the server isn't
     * already at 'max' clients.
     *
     * Note, the client won't actually exist in perforce until someone calls
     * 'reset' on it. This process will occur automatically the first time
     * the new client is 'grabbed'.
     *
     * @return  bool        true if client was provisioned, false otherwise
     * @throws  \Exception  if errors occur running revert/flush during hard reset
     * @throws  Exception   if management lock file cannot be opened (most likely due to file permissions)
     */
    protected function provision()
    {
        $file = @fopen($this->root . '/' . static::MANAGEMENT_LOCK . static::LOCK_EXTENSION, 'c');

        // if we lacked rights to open the file, bail
        if ($file === false) {
            throw new Exception(
                'Unable to open client pool management lock, this likely indicates file permission problems'
            );
        }

        $lock = flock($file, LOCK_EX | LOCK_NB);

        // if another process is already managing; can't provision bail out
        if (!$lock) {
            flock($file, LOCK_UN);
            fclose($file);
            return false;
        }

        // get the workspace list now that we have a lock to take an accurate counts
        $locks = glob($this->root . '/' . $this->prefix . '*' . static::LOCK_EXTENSION);
        unset($locks[array_search($this->root . '/' . static::MANAGEMENT_LOCK . static::LOCK_EXTENSION, $locks)]);

        // if we now appear to be at/above max just release lock and return failure
        if ($this->max && count($locks) >= $this->max) {
            flock($file, LOCK_UN);
            fclose($file);
            usleep(5000);
            return false;
        }

        // generate a new client identifier and touch the lock to define it
        $id = $this->prefix . new Uuid;
        touch($this->root . '/' . $id . static::LOCK_EXTENSION);

        // release our management lock and return success
        flock($file, LOCK_UN);
        fclose($file);
        return true;
    }

    /**
     * Recursively remove a directory and all of it's file contents.
     *
     * @param  string   $directory   The directory to remove.
     * @param  bool     $recursive   when true, recursively delete directories.
     * @param  bool     $removeRoot  when true, remove the root (passed) directory too
     */
    protected function removeDirectory($directory, $recursive = true, $removeRoot = true)
    {
        if (is_dir($directory)) {
            $files = new \RecursiveDirectoryIterator($directory);
            foreach ($files as $file) {
                if ($files->isDot()) {
                    continue;
                }
                if ($file->isFile()) {
                    chmod($file->getPathname(), 0777);
                    @unlink($file->getPathname());
                } elseif ($file->isDir() && $recursive) {
                    $this->removeDirectory($file->getPathname(), true, true);
                }
            }

            if ($removeRoot) {
                chmod($directory, 0777);
                @rmdir($directory);
            }
        }
    }
}
# Change User Description Committed
#1 18334 Liz Lam initial add of jambox