From 06332c7511337eed37499e290aecf981b26539e0 Mon Sep 17 00:00:00 2001 From: Christophe Vandeplas Date: Tue, 16 Apr 2024 16:52:36 +0200 Subject: [PATCH] new: [EventLock] creation of EventLockBehavior (#9687) --- src/Model/Behavior/EventLockBehavior.php | 204 ++++++++++++++++++ tests/TestCase/Model/Behavior/.gitkeep | 0 .../Model/Behavior/EventLockBehaviorTest.php | 115 ++++++++++ 3 files changed, 319 insertions(+) create mode 100644 src/Model/Behavior/EventLockBehavior.php delete mode 100644 tests/TestCase/Model/Behavior/.gitkeep create mode 100644 tests/TestCase/Model/Behavior/EventLockBehaviorTest.php diff --git a/src/Model/Behavior/EventLockBehavior.php b/src/Model/Behavior/EventLockBehavior.php new file mode 100644 index 000000000..f0d319301 --- /dev/null +++ b/src/Model/Behavior/EventLockBehavior.php @@ -0,0 +1,204 @@ +insertLockToRedis($eventId, "user:{$user['id']}", [ + 'type' => 'user', + 'timestamp' => time(), + 'User' => [ + 'id' => $user['id'], + 'org_id' => $user['org_id'], + 'email' => $user['email'], + ], + ]); + } + + /** + * @param int $eventId event ID + * @param int $jobId job ID to insert + * @return bool True if insert was successful. + */ + public function insertLockBackgroundJob(int $eventId, int $jobId) + { + return $this->insertLockToRedis($eventId, "job:$jobId", [ + 'type' => 'job', + 'timestamp' => time(), + 'job_id' => $jobId, + ]); + } + + /** + * @param int $eventId event ID + * @param array $user user array + * @return int|null Lock ID + */ + public function insertLockApi(int $eventId, array $user) + { + $apiLockId = mt_rand(); + if ( + $this->insertLockToRedis( + $eventId, + "api:{$user['id']}:$apiLockId", + [ + 'type' => 'api', + 'user_id' => $user['id'], + 'timestamp' => time(), + ] + ) + ) { + return $apiLockId; + } + + return null; + } + + /** + * @param int $eventId event ID + * @param array $user user array + * @return bool + */ + public function deleteLock(int $eventId, array $user) + { + try { + $redis = RedisTool::init(); + } catch (Exception $e) { + return false; + } + + $deleted = $redis->hdel(self::PREFIX . $eventId, "user:{$user['id']}"); + + return $deleted > 0; + } + + /** + * @deprecated use deleteLockApi instead + * @param int $eventId event ID + * @param int $apiLockId api lock ID + * @param array $user user array + * @return bool + */ + public function deleteApiLock(int $eventId, int $apiLockId, array $user) + { + return $this->deleteLockApi($eventId, $apiLockId, $user); + } + + /** + * @param int $eventId event ID + * @param int $apiLockId api lock ID + * @param array $user user array + * @return bool + */ + public function deleteLockApi(int $eventId, int $apiLockId, array $user) + { + try { + $redis = RedisTool::init(); + } catch (Exception $e) { + return false; + } + + $deleted = $redis->hdel(self::PREFIX . $eventId, "api:{$user['id']}:$apiLockId"); + + return $deleted > 0; + } + + /** + * @deprecated use deleteLockBackgroundJob instead + * @param int $eventId event ID + * @param int $jobId job ID + * @return bool + */ + public function deleteBackgroundJobLock(int $eventId, int $jobId) + { + return $this->deleteLockBackgroundJob($eventId, $jobId); + } + + /** + * @param int $eventId event ID + * @param int $jobId job ID + * @return bool + */ + public function deleteLockBackgroundJob(int $eventId, int $jobId) + { + try { + $redis = RedisTool::init(); + } catch (Exception $e) { + return false; + } + + $deleted = $redis->hDel(self::PREFIX . $eventId, "job:$jobId"); + + return $deleted > 0; + } + + /** + * @param array $user user array + * @param int $eventId event ID + * @return array[] + * @throws \JsonException + */ + public function checkLock(array $user, int $eventId) + { + try { + $redis = RedisTool::init(); + } catch (Exception $e) { + return []; + } + + $keys = $redis->hGetAll(self::PREFIX . $eventId); + if (empty($keys)) { + return []; + } + + $output = []; + $now = time(); + foreach ($keys as $value) { + $value = RedisTool::deserialize($value); + if ($value['timestamp'] + self::DEFAULT_TTL > $now) { + $output[] = $value; + } + } + + return $output; + } + + /** + * @param int $eventId event ID + * @param string $lockId lock ID + * @param array $data data to insert + * @return bool + * @throws \JsonException + * @throws \RedisException + */ + private function insertLockToRedis(int $eventId, $lockId, array $data) + { + try { + $redis = RedisTool::init(); + } catch (Exception $e) { + return false; + } + + $pipeline = $redis->pipeline(); + $pipeline->hSet(self::PREFIX . $eventId, $lockId, RedisTool::serialize($data)); + $pipeline->expire(self::PREFIX . $eventId, self::DEFAULT_TTL); // prolong TTL + + return $pipeline->exec()[0] !== false; + } +} diff --git a/tests/TestCase/Model/Behavior/.gitkeep b/tests/TestCase/Model/Behavior/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/TestCase/Model/Behavior/EventLockBehaviorTest.php b/tests/TestCase/Model/Behavior/EventLockBehaviorTest.php new file mode 100644 index 000000000..cfb08e7d5 --- /dev/null +++ b/tests/TestCase/Model/Behavior/EventLockBehaviorTest.php @@ -0,0 +1,115 @@ +EventLockBehavior = new EventLockBehavior($table); + + $this->user = [ + 'id' => UsersFixture::USER_REGULAR_USER_ID, + 'org_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'email' => UsersFixture::USER_REGULAR_USER_EMAIL, + ]; + $this->eventId = EventsFixture::EVENT_1_ID; + } + + /** + * tearDown method + * + * @return void + */ + protected function tearDown(): void + { + unset($this->EventLockBehavior); + parent::tearDown(); + } + + /** + * Test insertLockBackgroundJob method + * + * @return void + * @uses \App\Model\Behavior\EventLockBehavior::insertLockBackgroundJob() + * @uses \App\Model\Behavior\EventLockBehavior::deleteLockBackgroundJob() + */ + public function testInsertDeleteLockBackgroundJob(): void + { + try { + $jobId = 1; + $result = $this->EventLockBehavior->insertLockBackgroundJob($this->eventId, $jobId); + $this->assertTrue($result); + } finally { + $result = $this->EventLockBehavior->deleteLockBackgroundJob($this->eventId, $jobId); + $this->assertTrue($result); + } + } + + /** + * Test insertLockApi, deleteApiLock methods + * + * @return void + * @uses \App\Model\Behavior\EventLockBehavior::insertLockApi() + * @uses \App\Model\Behavior\EventLockBehavior::deleteLockApi() + */ + public function testInsertDeleteLockApi(): void + { + try { + $apiLockId = $this->EventLockBehavior->insertLockApi($this->eventId, $this->user); + $this->assertNotEmpty($apiLockId); + } finally { + $output = $this->EventLockBehavior->deleteLockApi($this->eventId, $apiLockId, $this->user); + $this->assertTrue($output); + } + } + + /** + * Test insertLock, checkLock, deleteLock methods + * + * @return void + * @uses \App\Model\Behavior\EventLockBehavior::insertLock() + * @uses \App\Model\Behavior\EventLockBehavior::checkLock() + * @uses \App\Model\Behavior\EventLockBehavior::deleteLock() + */ + public function testInsertCheckDeleteLock(): void + { + try { + $result = $this->EventLockBehavior->insertLock($this->user, $this->eventId); + $this->assertTrue($result); + + $output = $this->EventLockBehavior->checkLock($this->user, $this->eventId); + $this->assertNotEmpty($output); + } finally { + $result = $this->EventLockBehavior->deleteLock($this->eventId, $this->user); + $this->assertTrue($result); + } + } +}