Skip to content

Commit

Permalink
SimpleLock extension
Browse files Browse the repository at this point in the history
Reviewed By: jano

Differential Revision: D67992267

fbshipit-source-id: bc251d2d13facd1677ab7b965e84335e8d2c6f51
  • Loading branch information
paulbiss authored and facebook-github-bot committed Jan 14, 2025
1 parent 23fe75f commit ea44027
Show file tree
Hide file tree
Showing 20 changed files with 572 additions and 0 deletions.
162 changes: 162 additions & 0 deletions hphp/runtime/ext/simplelock/ext_simplelock.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
| Copyright (c) 1997-2010 The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/

#include "hphp/runtime/ext/asio/asio-external-thread-event.h"
#include "hphp/runtime/ext/asio/ext_static-wait-handle.h"
#include "hphp/runtime/ext/extension.h"

#include "hphp/util/hash-set.h"
#include "hphp/util/rds-local.h"

#include <folly/concurrency/ConcurrentHashMap.h>
#include <atomic>

namespace HPHP {

struct SimpleLockEvent;

namespace {

using LockAtom = std::atomic<SimpleLockEvent*>;
void unlock(LockAtom*);
SimpleLockEvent* const UNLOCKED = (SimpleLockEvent*)(-1);
folly::ConcurrentHashMapSIMD<std::string, LockAtom> s_locks;

RDS_LOCAL(hphp_fast_set<LockAtom*>, tl_heldLocks);

}

struct SimpleLockEvent : AsioExternalThreadEvent {
~SimpleLockEvent() override {
if (m_held) unlock(m_held);
}

void acquire(LockAtom* lock) {
m_held = lock;
markAsFinished();
}

void unserialize(TypedValue& result) override {
result = make_tv<KindOfNull>();
tl_heldLocks->emplace(m_held);
m_held = nullptr;
}

std::atomic<SimpleLockEvent*> m_next{nullptr};
private:
std::atomic<LockAtom*> m_held{nullptr};
};

namespace {

LockAtom* get_lock(const std::string& name) {
// ConcurrentHashMapSIMD guarantees reference stability across rehashes, so
// long as we don't erase values from the map it should be safe to continue
// accessing these references without holding a hazard pointer.
{
auto const it = s_locks.find(name);
if (it != s_locks.end()) return const_cast<LockAtom*>(&it->second);
}

auto [it, ins] = s_locks.emplace(name, UNLOCKED);
return const_cast<LockAtom*>(&it->second);
}

SimpleLockEvent* try_lock(LockAtom* lock) {
auto expected = UNLOCKED;
if (lock->compare_exchange_strong(expected, nullptr,
std::memory_order_acq_rel)) {
return nullptr;
}

auto ev = new SimpleLockEvent();
do {
ev->m_next = expected;
} while (!lock->compare_exchange_weak(expected,
expected != UNLOCKED ? ev : nullptr,
std::memory_order_acq_rel));

if (expected != UNLOCKED) return ev;
ev->abandon();
return nullptr;
}

void unlock(LockAtom* lock) {
SimpleLockEvent* expected = nullptr;
auto next = UNLOCKED;
while (!lock->compare_exchange_weak(expected, next,
std::memory_order_acq_rel)) {
next = expected ? expected->m_next.load(std::memory_order_relaxed)
: UNLOCKED;
}
if (expected) expected->acquire(lock);
}

Object HHVM_FUNCTION(lock_mutex, const String& name) {
auto const l = get_lock(name.toCppString());
if (auto ev = try_lock(l)) return Object{ev->getWaitHandle()};

tl_heldLocks->emplace(l);
return Object{c_StaticWaitHandle::CreateSucceeded(make_tv<KindOfNull>())};
}

bool HHVM_FUNCTION(try_lock_mutex, const String& name) {
auto const l = get_lock(name.toCppString());
auto exp = UNLOCKED;
if (l->compare_exchange_strong(exp, nullptr, std::memory_order_acq_rel)) {
tl_heldLocks->emplace(l);
return true;
}
return false;
}

void HHVM_FUNCTION(unlock_mutex, const String& name) {
auto const l = get_lock(name.toCppString());
if (!tl_heldLocks->erase(l)) {
SystemLib::throwInvalidOperationExceptionObject(
"cannot release unheld lock"
);
}
unlock(l);
}

struct SimpleLockExtension final : Extension {
SimpleLockExtension() : Extension("simplelock", "1.0", "sandbox_infra") {}

bool moduleEnabled() const override { return !Cfg::Repo::Authoritative; }

void moduleRegisterNative() override {
HHVM_NAMED_FE(HH\\SimpleLock\\lock, HHVM_FN(lock_mutex));
HHVM_NAMED_FE(HH\\SimpleLock\\try_lock, HHVM_FN(try_lock_mutex));
HHVM_NAMED_FE_STR(
"HH\\SimpleLock\\unlock", HHVM_FN(unlock_mutex), nativeFuncs()
);
}

void requestShutdown() override {
while (!tl_heldLocks->empty()) {
hphp_fast_set<LockAtom*> locks;
std::swap(*tl_heldLocks, locks);
for (auto l : locks) unlock(l);
}
}

} s_simple_lock_extension;

}

}
76 changes: 76 additions & 0 deletions hphp/runtime/ext/simplelock/ext_simplelock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?hh

namespace HH\SimpleLock {

/*
* Acquire a cross-request mutual exclusion lock with a unique string name.
*
* If the same lock is acquired more than once in a request subsequent attempts
* to acquire the lock will block until the it is unlocked. The returned
* Awaitable will resolve when the lock has been acquired.
*
* @param string $name - the name of the lock
*/
<<__Native>>
function lock(string $name): Awaitable<void>;

/*
* Release a cross-request mutual exclusion lock with a unique name.
*
* Immediately release a held lock acquired using HH\SimpleLock\lock(), if the
* lock was never acquired throws an InvalidOperationException.
*
* @param string $name - the name of the lock
*/
<<__Native>>
function unlock(string $name): void;

/*
* Attempt to acquired a cross-request mutual exclusion lock with a unique name.
*
* If the lock is unheld it is immediately acquired and true is returned,
* otherwise we return false and no lock is acquired.
*
* @param string $name - the name of the lock
* @return bool - whether the lock was acquired
*/
<<__Native>>
function try_lock(string $name): bool;

/*
* Acquired a cross-request mutual exclusion lock with a unique name and a
* timeout.
*
* Attempts to acquire a lock within a fixed amount of microseconds, if the
* timeout is reached without acquiring the lock, throws a RuntimeException.
*
* @param string $name - the name of the lock
* @param int $timeout - the number of microseconds to wait for
*/
async function lock_with_timeout(string $name, int $timeout): Awaitable<void> {
$lwh = lock($name);

if ($lwh->isFinished()) {
await $lwh;
return;
}

$swh = \HH\SleepWaitHandle::create($timeout);
concurrent {
await async {
try {
await $swh;
\HH\Asio\cancel(
$lwh,
new \RuntimeException("Timed out waiting for lock $name"),
);
} catch (\Exception $_) {}
};
await async {
await $lwh;
\HH\Asio\cancel($swh, new \Exception());
};
}
}

}
37 changes: 37 additions & 0 deletions hphp/test/slow/simplelock/abandon.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?hh

function thread_main() {
HH\SimpleLock\lock('abandon');
HH\SimpleLock\lock('abandon');
HH\SimpleLock\lock('abandon');
HH\SimpleLock\lock('abandon');
HH\SimpleLock\lock('abandon');

$_ = true;
$t = apc_inc('threads', 1, inout $_);
}

<<__EntryPoint>>
async function main() {
if (HH\execution_context() === "xbox") return;

await HH\SimpleLock\lock('abandon');
apc_store('threads', 0);

$funcs = vec[];
for ($i = 0; $i < 4; $i++) {
$funcs[] = fb_call_user_func_async(
__FILE__,
'thread_main'
);
}

$_ = true;
while (apc_fetch('threads', inout $_) !== count($funcs)) usleep(10);

HH\SimpleLock\unlock('abandon');
foreach ($funcs as $f) fb_end_user_func_async($f);

await HH\SimpleLock\lock('abandon');
echo "Main done.\n";
}
1 change: 1 addition & 0 deletions hphp/test/slow/simplelock/abandon.php.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Main done.
Empty file.
9 changes: 9 additions & 0 deletions hphp/test/slow/simplelock/basic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?hh

<<__EntryPoint>>
async function main() {
await HH\SimpleLock\lock("hello");
await HH\SimpleLock\lock("goodbye");
HH\SimpleLock\unlock("hello");
HH\SimpleLock\unlock("bad");
}
9 changes: 9 additions & 0 deletions hphp/test/slow/simplelock/basic.php.expectf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

Fatal error: Uncaught exception 'InvalidOperationException' with message 'cannot release unheld lock' in %s/basic.php:8
Stack trace:
#0 %s/basic.php(8): HH\SimpleLock\unlock()
#1 (): main()
#2 (): Closure$__SystemLib\enter_async_entry_point()
#3 (): HH\Asio\join()
#4 (): __SystemLib\enter_async_entry_point()
#5 {main}
Empty file.
47 changes: 47 additions & 0 deletions hphp/test/slow/simplelock/multilock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?hh

async function thread_func() {
echo "thread\n";

$_ = true;
$t = apc_inc('threads', 1, inout $_);

await HH\SimpleLock\lock("sync");

$t = apc_inc('counter', 1, inout $_);
for ($i = 0; $i < 10; $i++) {
echo "thread($t): $i\n";
await SleepWaitHandle::create(1000);
}

if ($t % 2) HH\SimpleLock\unlock("sync");
}

function thread_main() {
HH\Asio\join(thread_func());
}

<<__EntryPoint>>
async function main() {
if (HH\execution_context() === "xbox") return;

await HH\SimpleLock\lock("sync");
apc_store('counter', 0);
apc_store('threads', 0);

$funcs = vec[];
for ($i = 0; $i < 4; $i++) {
$funcs[] = fb_call_user_func_async(
__FILE__,
'thread_main'
);
}

$_ = true;
while (apc_fetch('threads', inout $_) !== 4) usleep(10);

HH\SimpleLock\unlock("sync");
foreach ($funcs as $f) fb_end_user_func_async($f);

echo "Main done.\n";
}
45 changes: 45 additions & 0 deletions hphp/test/slow/simplelock/multilock.php.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
thread
thread
thread
thread
thread(1): 0
thread(1): 1
thread(1): 2
thread(1): 3
thread(1): 4
thread(1): 5
thread(1): 6
thread(1): 7
thread(1): 8
thread(1): 9
thread(2): 0
thread(2): 1
thread(2): 2
thread(2): 3
thread(2): 4
thread(2): 5
thread(2): 6
thread(2): 7
thread(2): 8
thread(2): 9
thread(3): 0
thread(3): 1
thread(3): 2
thread(3): 3
thread(3): 4
thread(3): 5
thread(3): 6
thread(3): 7
thread(3): 8
thread(3): 9
thread(4): 0
thread(4): 1
thread(4): 2
thread(4): 3
thread(4): 4
thread(4): 5
thread(4): 6
thread(4): 7
thread(4): 8
thread(4): 9
Main done.
Empty file.
Loading

0 comments on commit ea44027

Please sign in to comment.