Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debugger: File breakpoint storage #887

Merged
merged 12 commits into from
Feb 8, 2018
40 changes: 37 additions & 3 deletions src/Core/Lock/FlockLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,32 @@ class FlockLock implements LockInterface
*/
private $handle;

/**
* @var bool If true, we should acquire an exclusive lock.
*/
private $exclusive;

/**
* @param string $fileName The name of the file to use as a lock.
* @param array $options [optional] {
* Configuration options.
*
* @type bool $exclusive If true, acquire an excluse (write) lock. If
* false, acquire a shared (read) lock. **Defaults to** true.
* }
* @throws \InvalidArgumentException If an invalid fileName is provided.
*/
public function __construct($fileName)
public function __construct($fileName, array $options = [])
{
if (!is_string($fileName)) {
throw new \InvalidArgumentException('$fileName must be a string.');
}

$options += [
'exclusive' => true
];
$this->exclusive = $options['exclusive'];

$this->filePath = sprintf(
self::FILE_PATH_TEMPLATE,
sys_get_temp_dir(),
Expand All @@ -62,18 +78,24 @@ public function __construct($fileName)
/**
* Acquires a lock that will block until released.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException If the lock fails to be acquired.
*/
public function acquire()
public function acquire(array $options = [])
{
if ($this->handle) {
return true;
}

$this->handle = $this->initializeHandle();

if (!flock($this->handle, LOCK_EX)) {
if (!flock($this->handle, $this->lockType($options))) {
fclose($this->handle);
$this->handle = null;

Expand Down Expand Up @@ -117,4 +139,16 @@ private function initializeHandle()

return $handle;
}

private function lockType(array $options)
{
$options += [
'blocking' => true
];
$lockType = $this->exclusive ? LOCK_EX : LOCK_SH;
if (!$options['blocking']) {
$lockType = $lockType | LOCK_UN;
}
return $lockType;
}
}
22 changes: 18 additions & 4 deletions src/Core/Lock/LockInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@
interface LockInterface
{
/**
* Acquires a lock that will block until released.
* Acquires a lock.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException
* @throws \RuntimeException If the lock fails to be acquired.
*/
public function acquire();
public function acquire(array $options = []);

/**
* Releases the lock.
Expand All @@ -41,7 +47,15 @@ public function release();
* Execute a callable within a lock.
*
* @param callable $func The callable to execute.
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* @type bool $exclusive If true, acquire an excluse (write) lock. If

This comment was marked as spam.

* false, acquire a shared (read) lock. **Defaults to** true.
* }
* @return mixed
*/
public function synchronize(callable $func);
public function synchronize(callable $func, array $options = []);
}
20 changes: 16 additions & 4 deletions src/Core/Lock/LockTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@ trait LockTrait
/**
* Acquires a lock that will block until released.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException
* @throws \RuntimeException If the lock fails to be acquired.
*/
abstract public function acquire();
abstract public function acquire(array $options = []);

/**
* Releases the lock.
Expand All @@ -43,14 +49,20 @@ abstract public function release();
* it.
*
* @param callable $func The callable to execute.
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return mixed
*/
public function synchronize(callable $func)
public function synchronize(callable $func, array $options = [])
{
$result = null;
$exception = null;

if ($this->acquire()) {
if ($this->acquire($options)) {
try {
$result = $func();
} catch (\Exception $ex) {
Expand Down
14 changes: 12 additions & 2 deletions src/Core/Lock/SemaphoreLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,28 @@ public function __construct($key)
/**
* Acquires a lock that will block until released.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException If the lock fails to be acquired.
*/
public function acquire()
public function acquire(array $options = [])
{
$options += [
'blocking' => true
];

if ($this->semaphoreId) {
return true;
}

$this->semaphoreId = $this->initializeId();

if (!sem_acquire($this->semaphoreId)) {
if (!sem_acquire($this->semaphoreId, !$options['blocking'])) {
$this->semaphoreId = null;

throw new \RuntimeException('Failed to acquire lock.');
Expand Down
16 changes: 13 additions & 3 deletions src/Core/Lock/SymfonyLockAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,23 @@ public function __construct(SymfonyLockInterface $lock)
/**
* Acquires a lock that will block until released.
*
* @param array $options [optional] {
* Configuration options.
*
* @type bool $blocking Whether the process should block while waiting
* to acquire the lock. **Defaults to** true.
* }
* @return bool
* @throws \RuntimeException
* @throws \RuntimeException If the lock fails to be acquired.
*/
public function acquire()
public function acquire(array $options = [])
{
$options += [
'blocking' => true
];

try {
return $this->lock->acquire(true);
return $this->lock->acquire($options['blocking']);
} catch (\Exception $ex) {
throw new \RuntimeException($ex->getMessage());
}
Expand Down
7 changes: 6 additions & 1 deletion src/Debugger/Agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

use Google\Cloud\Core\Batch\BatchRunner;
use Google\Cloud\Core\Batch\BatchTrait;
use Google\Cloud\Core\SysvTrait;
use Google\Cloud\Debugger\BreakpointStorage\BreakpointStorageInterface;
use Google\Cloud\Debugger\BreakpointStorage\FileBreakpointStorage;
use Google\Cloud\Debugger\BreakpointStorage\SysvBreakpointStorage;
use Google\Cloud\Logging\LoggingClient;
use Psr\Log\LoggerInterface;
Expand All @@ -39,6 +41,7 @@
class Agent
{
use BatchTrait;
use SysvTrait;

/**
* @var Debuggee
Expand Down Expand Up @@ -190,7 +193,9 @@ protected function getCallback()

private function defaultStorage()
{
return new SysvBreakpointStorage();
return $this->isSysvIPCLoaded()
? new SysvBreakpointStorage()
: new FileBreakpointStorage();
}

private function defaultDebuggee()
Expand Down
113 changes: 113 additions & 0 deletions src/Debugger/BreakpointStorage/FileBreakpointStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* 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.
*/

namespace Google\Cloud\Debugger\BreakpointStorage;

use Google\Cloud\Core\Lock\FlockLock;
use Google\Cloud\Debugger\Breakpoint;
use Google\Cloud\Debugger\Debuggee;

/**
* This implementation of BreakpointStorageInterface using a local file.
*/
class FileBreakpointStorage implements BreakpointStorageInterface
{
const DEFAULT_FILENAME = 'debugger-breakpoints';

/* @var string */
private $filename;

/* @var string */
private $lockFilename;

/**
* Create a new FileBreakpointStorage instance.
*
* @param string $filename [optional] The full path to the storage file.
* **Defaults to** a temporary file in the system's temp directory.
*/
public function __construct($filename = null)
{
$filename = $filename ?: self::DEFAULT_FILENAME;
$this->filename = implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), $filename]);
$this->lockFilename = $filename . '.lock';
}

/**
* Save the given debugger breakpoints.
*
* @param Debuggee $debuggee
* @param Breakpoint[] $breakpoints
* @return bool
*/
public function save(Debuggee $debuggee, array $breakpoints)
{
$data = [
'debuggeeId' => $debuggee->id(),
'breakpoints' => $breakpoints
];

$success = false;

// Acquire an exclusive write lock (blocking). There should only be a
// single Daemon that can call this.
try {
$success = $this->getLock(true)->synchronize(function () use ($data) {
return file_put_contents($this->filename, serialize($data)) !== false;
});
} catch (\RuntimeException $e) {
// Do nothing
}
return $success;
}

/**
* Load debugger breakpoints from the storage. Returns a 2-arity array
* with the debuggee id and the list of breakpoints.
*
* @return array
*/
public function load()
{
$debuggeeId = null;
$breakpoints = [];

// Acquire a read lock (non-blocking). If we fail (file is locked
// for writing), then we return an empty list of breakpoints and
// skip debugging for this request.
try {
$contents = $this->getLock()->synchronize(function() {
return file_get_contents($this->filename);
}, [
'blocking' => false
]);
$data = unserialize($contents);
$debuggeeId = $data['debuggeeId'];
$breakpoints = $data['breakpoints'];
} catch (\RuntimeException $e) {
// Do nothing
}
return [$debuggeeId, $breakpoints];
}

private function getLock($exclusive = false)
{
return new FlockLock($this->lockFilename, [
'exclusive' => $exclusive
]);
}
}
2 changes: 1 addition & 1 deletion src/Debugger/BreakpointStorage/SysvBreakpointStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public function load()
);
}
if (!shm_has_var($shmid, self::VAR_KEY)) {
$result = [];
$result = [null, []];
} else {
$result = shm_get_var($shmid, self::VAR_KEY);
}
Expand Down
Loading