diff --git a/doc/core/requestContext.md b/doc/core/requestContext.md index 0cdeeb2dea..685ed9f770 100644 --- a/doc/core/requestContext.md +++ b/doc/core/requestContext.md @@ -67,3 +67,21 @@ $result = \Imi\RequestContext::remember('myKey3', function () { return 1 + 2; }); ``` + +### 推迟执行 + +当协程释放时触发,先进后出 + +```php +use function Yurun\Swoole\Coroutine\goWait; +$result = []; +goWait(static function () use (&$result): void { + RequestContext::defer(static function () use (&$result): void { + $result[] = 1; + }); + RequestContext::defer(static function () use (&$result): void { + $result[] = 2; + }); +}, -1, true); +var_dump($result); // [2, 1] +``` \ No newline at end of file diff --git a/src/Components/swoole/src/Context/CoroutineContextManager.php b/src/Components/swoole/src/Context/CoroutineContextManager.php index a0cda8ac1f..beb3765fcc 100644 --- a/src/Components/swoole/src/Context/CoroutineContextManager.php +++ b/src/Components/swoole/src/Context/CoroutineContextManager.php @@ -4,11 +4,11 @@ namespace Imi\Swoole\Context; +use Imi\Core\Context\ContextData; use Imi\Core\Context\Contract\IContextManager; use Imi\Core\Context\Exception\ContextExistsException; use Imi\Core\Context\Exception\ContextNotFoundException; use Imi\Event\Event; -use Imi\Log\Log; use Imi\Swoole\Util\Coroutine; /** @@ -19,58 +19,88 @@ class CoroutineContextManager implements IContextManager /** * 上下文对象集合. * - * @var \ArrayObject[] + * @var ContextData[] */ private array $contexts = []; /** * {@inheritDoc} */ - public function create(string $flag, array $data = []): \ArrayObject + public function create(string|int $id, array $data = []): ContextData { - if ($flag > -1) + if ($id > -1) { - $context = Coroutine::getContext((int) $flag); + $swooleContext = Coroutine::getContext((int) $id); // destroy - if (!($context['__bindDestroy'] ?? false)) + if (!($swooleContext[static::class]['destroyBinded'] ?? false)) { - $context['__bindDestroy'] = true; - Coroutine::defer($this->__destroy(...)); + $swooleContext[static::class]['destroyBinded'] = true; + Coroutine::defer(fn () => $this->destroy($id)); } - if ($data) + $context = $swooleContext[static::class]['context'] ?? null; + if ($context) { - foreach ($data as $k => $v) + if ($data) { - $context[$k] = $v; + foreach ($data as $k => $v) + { + $context[$k] = $v; + } } } + else + { + $context = $swooleContext[static::class]['context'] = new ContextData($data); + } return $context; } else { - if (isset($this->contexts[$flag])) + if (isset($this->contexts[$id])) { - throw new ContextExistsException(sprintf('Context %s already exists!', $flag)); + throw new ContextExistsException(sprintf('Context %s already exists!', $id)); } - return $this->contexts[$flag] = new \ArrayObject($data, \ArrayObject::ARRAY_AS_PROPS); + return $this->contexts[$id] = new ContextData($data); } } /** * {@inheritDoc} */ - public function destroy(string $flag): bool + public function destroy(string|int $id): bool { - if ($flag > -1) + if ($id > -1) { - return false; // 协程退出时自动销毁,无法手动销毁 + $swooleContext = Coroutine::getContext((int) $id); + if (!isset($swooleContext[static::class]['context'])) + { + return false; + } + // TODO: 实现新的连接管理器后移除 + Event::trigger('IMI.REQUEST_CONTENT.DESTROY'); + /** @var ContextData $context */ + $context = $swooleContext[static::class]['context']; + $deferCallbacks = $context->getDeferCallbacks(); + while (!$deferCallbacks->isEmpty()) + { + $deferCallbacks->pop()(); + } + unset($swooleContext[static::class]); + + return true; } - elseif (isset($this->contexts[$flag])) + elseif (isset($this->contexts[$id])) { + // TODO: 实现新的连接管理器后移除 Event::trigger('IMI.REQUEST_CONTENT.DESTROY'); - unset($this->contexts[$flag]); + $deferCallbacks = $this->contexts[$id]->getDeferCallbacks(); + while (!$deferCallbacks->isEmpty()) + { + $deferCallbacks->pop()(); + } + unset($this->contexts[$id]); return true; } @@ -83,82 +113,66 @@ public function destroy(string $flag): bool /** * {@inheritDoc} */ - public function get(string $flag, bool $autoCreate = false): \ArrayObject + public function get(string|int $id, bool $autoCreate = false): ContextData { - if ($flag > -1) + if ($id > -1) { - $context = Coroutine::getContext((int) $flag); + $swooleContext = Coroutine::getContext((int) $id); // destroy - if (!($context['__bindDestroy'] ?? false)) + if (!($swooleContext[static::class]['destroyBinded'] ?? false)) { - $context['__bindDestroy'] = true; - Coroutine::defer($this->__destroy(...)); + $swooleContext[static::class]['destroyBinded'] = true; + Coroutine::defer(fn () => $this->destroy($id)); } - return $context; + if (!isset($swooleContext[static::class]['context'])) + { + if ($autoCreate) + { + return $swooleContext[static::class]['context'] = new ContextData(); + } + throw new ContextNotFoundException(sprintf('Context %s does not exists!', $id)); + } + + return $swooleContext[static::class]['context']; } else { - if (!isset($this->contexts[$flag])) + if (!isset($this->contexts[$id])) { if ($autoCreate) { - return $this->create($flag); + return $this->create($id); } - throw new ContextNotFoundException(sprintf('Context %s does not exists!', $flag)); + throw new ContextNotFoundException(sprintf('Context %s does not exists!', $id)); } - return $this->contexts[$flag]; + return $this->contexts[$id]; } } /** * {@inheritDoc} */ - public function exists(string $flag): bool + public function exists(string|int $id): bool { - if ($flag > -1) + if ($id > -1) { - return Coroutine::exists($flag); + $swooleContext = Coroutine::getContext((int) $id); + + return $swooleContext && isset($swooleContext[static::class]['context']); } else { - return isset($this->contexts[$flag]); + return isset($this->contexts[$id]); } } /** * {@inheritDoc} */ - public function getCurrentFlag(): string + public function getCurrentId(): string|int { return (string) Coroutine::getCid(); } - - /** - * 销毁当前请求的上下文. - * - * 不要手动调用!不要手动调用!不要手动调用! - */ - public function __destroy(): void - { - try - { - Event::trigger('IMI.REQUEST_CONTENT.DESTROY'); - $context = Coroutine::getContext(); - if (!$context) - { - $coId = Coroutine::getCid(); - $contextMap = &$this->contextMap; - if (isset($contextMap[$coId])) - { - unset($contextMap[$coId]); - } - } - } - catch (\Throwable $th) - { - Log::error($th); - } - } } diff --git a/src/Components/swoole/tests/unit/Component/Tests/RequestContextTest.php b/src/Components/swoole/tests/unit/Component/Tests/RequestContextTest.php new file mode 100644 index 0000000000..40024b4101 --- /dev/null +++ b/src/Components/swoole/tests/unit/Component/Tests/RequestContextTest.php @@ -0,0 +1,49 @@ +assertEquals([2, 1], $result); + } + + public function testRemember(): void + { + $key = 'test_remember'; + $count = 0; + $countFun = static function () use (&$count) { + return ++$count; + }; + + RequestContext::unset($key); + $this->assertEquals(0, $count); + $this->assertEquals(1, RequestContext::remember($key, $countFun)); + $this->assertEquals(1, $count); + $this->assertEquals(1, RequestContext::remember($key, $countFun)); + $this->assertEquals(1, $count); + RequestContext::unset($key); + $this->assertEquals(2, RequestContext::remember($key, $countFun)); + $this->assertEquals(2, $count); + } +} diff --git a/src/Core/Context/ContextData.php b/src/Core/Context/ContextData.php new file mode 100644 index 0000000000..a15866f9c5 --- /dev/null +++ b/src/Core/Context/ContextData.php @@ -0,0 +1,32 @@ +deferCallbacks = new \SplStack(); + } + + /** + * 推迟执行,当协程释放时触发,先进后出. + */ + public function defer(callable $callback): void + { + $this->deferCallbacks[] = $callback; + } + + /** + * 获取推迟执行任务栈. + */ + public function getDeferCallbacks(): \SplStack + { + return $this->deferCallbacks; + } +} diff --git a/src/Core/Context/Contract/IContextManager.php b/src/Core/Context/Contract/IContextManager.php index 875566e3f8..f955fc1e7a 100644 --- a/src/Core/Context/Contract/IContextManager.php +++ b/src/Core/Context/Contract/IContextManager.php @@ -4,30 +4,32 @@ namespace Imi\Core\Context\Contract; +use Imi\Core\Context\ContextData; + interface IContextManager { /** * 创建上下文. */ - public function create(string $flag, array $data = []): \ArrayObject; + public function create(string|int $id, array $data = []): ContextData; /** * 销毁上下文. */ - public function destroy(string $flag): bool; + public function destroy(string|int $id): bool; /** * 获取上下文. */ - public function get(string $flag, bool $autoCreate = false): \ArrayObject; + public function get(string|int $id, bool $autoCreate = false): ContextData; /** * 上下文是否存在. */ - public function exists(string $flag): bool; + public function exists(string|int $id): bool; /** * 获取当前上下文标识. */ - public function getCurrentFlag(): string; + public function getCurrentId(): string|int; } diff --git a/src/Core/Context/DefaultContextManager.php b/src/Core/Context/DefaultContextManager.php index 059202a18d..ec88ed63f2 100644 --- a/src/Core/Context/DefaultContextManager.php +++ b/src/Core/Context/DefaultContextManager.php @@ -17,7 +17,7 @@ class DefaultContextManager implements IContextManager /** * 上下文对象集合. * - * @var \ArrayObject[] + * @var ContextData[] */ private array $contexts = []; @@ -26,11 +26,11 @@ class DefaultContextManager implements IContextManager /** * {@inheritDoc} */ - public function create(string $flag, array $data = []): \ArrayObject + public function create(string|int $id, array $data = []): ContextData { - if (isset($this->contexts[$flag])) + if (isset($this->contexts[$id])) { - throw new ContextExistsException(sprintf('Context %s already exists!', $flag)); + throw new ContextExistsException(sprintf('Context %s already exists!', $id)); } // 脚本执行结束时自动销毁上下文 @@ -40,18 +40,24 @@ public function create(string $flag, array $data = []): \ArrayObject $this->bindAutoDestroy(); } - return $this->contexts[$flag] = new \ArrayObject($data, \ArrayObject::ARRAY_AS_PROPS); + return $this->contexts[$id] = new ContextData($data); } /** * {@inheritDoc} */ - public function destroy(string $flag): bool + public function destroy(string|int $id): bool { - if (isset($this->contexts[$flag])) + if (isset($this->contexts[$id])) { + // TODO: 实现新的连接管理器后移除 Event::trigger('IMI.REQUEST_CONTENT.DESTROY'); - unset($this->contexts[$flag]); + $deferCallbacks = $this->contexts[$id]->getDeferCallbacks(); + while (!$deferCallbacks->isEmpty()) + { + $deferCallbacks->pop()(); + } + unset($this->contexts[$id]); return true; } @@ -64,32 +70,32 @@ public function destroy(string $flag): bool /** * {@inheritDoc} */ - public function get(string $flag, bool $autoCreate = false): \ArrayObject + public function get(string|int $id, bool $autoCreate = false): ContextData { - if (!isset($this->contexts[$flag])) + if (!isset($this->contexts[$id])) { if ($autoCreate) { - return $this->create($flag); + return $this->create($id); } - throw new ContextNotFoundException(sprintf('Context %s does not exists!', $flag)); + throw new ContextNotFoundException(sprintf('Context %s does not exists!', $id)); } - return $this->contexts[$flag]; + return $this->contexts[$id]; } /** * {@inheritDoc} */ - public function exists(string $flag): bool + public function exists(string|int $id): bool { - return isset($this->contexts[$flag]); + return isset($this->contexts[$id]); } /** * {@inheritDoc} */ - public function getCurrentFlag(): string + public function getCurrentId(): string|int { return 'default'; } @@ -99,9 +105,9 @@ protected function bindAutoDestroy(): void register_shutdown_function(function (): void { if ($this->contexts) { - foreach ($this->contexts as $flag => $_) + foreach ($this->contexts as $id => $_) { - $this->destroy($flag); + $this->destroy($id); } } }); diff --git a/src/Lock/Handler/BaseLock.php b/src/Lock/Handler/BaseLock.php index 58d3909adf..498f9803ac 100644 --- a/src/Lock/Handler/BaseLock.php +++ b/src/Lock/Handler/BaseLock.php @@ -82,7 +82,7 @@ public function lock(?callable $taskCallable = null, ?callable $afterLockCallabl return false; } $this->isLocked = true; - $this->lockCoId = RequestContext::getCurrentFlag(); + $this->lockCoId = RequestContext::getCurrentId(); $this->beginTime = microtime(true); if (null === $taskCallable) { @@ -121,7 +121,7 @@ public function tryLock(?callable $taskCallable = null): bool return false; } $this->isLocked = true; - $this->lockCoId = RequestContext::getCurrentFlag(); + $this->lockCoId = RequestContext::getCurrentId(); $this->beginTime = microtime(true); if (null !== $taskCallable) { @@ -185,7 +185,7 @@ public function unlock(): bool */ public function isLocked(): bool { - return $this->isLocked && $this->lockCoId === RequestContext::getCurrentFlag(); + return $this->isLocked && $this->lockCoId === RequestContext::getCurrentId(); } /** diff --git a/src/RequestContext.php b/src/RequestContext.php index cbc1846588..acc0a6e83f 100644 --- a/src/RequestContext.php +++ b/src/RequestContext.php @@ -5,6 +5,7 @@ namespace Imi; use Imi\Bean\Container; +use Imi\Core\Context\ContextData; use Imi\Core\Context\Contract\IContextManager; use Imi\Core\Context\DefaultContextManager; use Imi\Server\Contract\IServer; @@ -36,37 +37,37 @@ public static function getInstance(): IContextManager /** * 获取当前上下文标识. */ - public static function getCurrentFlag(): string + public static function getCurrentId(): string|int { - return static::getInstance()->getCurrentFlag(); + return static::getInstance()->getCurrentId(); } /** * 为当前请求创建上下文,返回当前协程ID. */ - public static function create(array $data = []): \ArrayObject + public static function create(array $data = []): ContextData { $instance = static::getInstance(); - return $instance->create($instance->getCurrentFlag(), $data); + return $instance->create($instance->getCurrentId(), $data); } /** * 销毁上下文. */ - public static function destroy(?string $flag = null): bool + public static function destroy(string|int|null $id = null): bool { $instance = static::getInstance(); - return $instance->destroy($flag ?? $instance->getCurrentFlag()); + return $instance->destroy($id ?? $instance->getCurrentId()); } /** * 上下文是否存在. */ - public static function exists(string $flag): bool + public static function exists(string|int $id): bool { - return static::getInstance()->exists($flag); + return static::getInstance()->exists($id); } /** @@ -75,7 +76,7 @@ public static function exists(string $flag): bool public static function get(string $name, mixed $default = null): mixed { $instance = static::getInstance(); - $context = $instance->get($instance->getCurrentFlag(), true); + $context = $instance->get($instance->getCurrentId(), true); return $context[$name] ?? $default; } @@ -86,7 +87,7 @@ public static function get(string $name, mixed $default = null): mixed public static function set(string $name, mixed $value): void { $instance = static::getInstance(); - $context = $instance->get($instance->getCurrentFlag(), true); + $context = $instance->get($instance->getCurrentId(), true); $context[$name] = $value; } @@ -96,7 +97,7 @@ public static function set(string $name, mixed $value): void public static function muiltiSet(array $data): void { $instance = static::getInstance(); - $context = $instance->get($instance->getCurrentFlag(), true); + $context = $instance->get($instance->getCurrentId(), true); foreach ($data as $k => $v) { $context[$k] = $v; @@ -109,7 +110,7 @@ public static function muiltiSet(array $data): void public static function use(callable $callback): mixed { $instance = static::getInstance(); - $context = $instance->get($instance->getCurrentFlag(), true); + $context = $instance->get($instance->getCurrentId(), true); return $callback($context); } @@ -137,14 +138,19 @@ public static function unset(string $key): void } } + public static function defer(callable $callback): void + { + self::getContext()->defer($callback); + } + /** * 获取当前上下文. */ - public static function getContext(): \ArrayObject + public static function getContext(): ContextData { $instance = static::getInstance(); - return $instance->get($instance->getCurrentFlag(), true); + return $instance->get($instance->getCurrentId(), true); } /** diff --git a/tests/unit/Component/Tests/BaseLockTestCase.php b/tests/unit/Component/Tests/BaseLockTestCase.php index 7254cfd4c2..a52223c0fa 100644 --- a/tests/unit/Component/Tests/BaseLockTestCase.php +++ b/tests/unit/Component/Tests/BaseLockTestCase.php @@ -31,7 +31,7 @@ public function testLockAndUnlock(): void { Assert::assertTrue($result); Assert::assertTrue(Lock::isLocked($this->lockConfigId, $lockId)); - Assert::assertEquals(RequestContext::getCurrentFlag(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); + Assert::assertEquals(RequestContext::getCurrentId(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); } finally { @@ -54,7 +54,7 @@ public function testTryLock(): void { Assert::assertTrue($result); Assert::assertTrue(Lock::isLocked($this->lockConfigId, $lockId)); - Assert::assertEquals(RequestContext::getCurrentFlag(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); + Assert::assertEquals(RequestContext::getCurrentId(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); } finally { @@ -73,7 +73,7 @@ public function testLockCallable(): void Assert::assertFalse(Lock::isLocked($this->lockConfigId, $lockId)); $result = Lock::lock($this->lockConfigId, function () use ($lockId): void { Assert::assertTrue(Lock::isLocked($this->lockConfigId, $lockId)); - Assert::assertEquals(RequestContext::getCurrentFlag(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); + Assert::assertEquals(RequestContext::getCurrentId(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); }, null, $lockId); Assert::assertTrue($result); Assert::assertFalse(Lock::isLocked($this->lockConfigId, $lockId)); @@ -89,7 +89,7 @@ public function testTryLockCallable(): void Assert::assertEquals('', Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); $result = Lock::tryLock($this->lockConfigId, function () use ($lockId): void { Assert::assertTrue(Lock::isLocked($this->lockConfigId, $lockId)); - Assert::assertEquals(RequestContext::getCurrentFlag(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); + Assert::assertEquals(RequestContext::getCurrentId(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); }, $lockId); Assert::assertTrue($result); Assert::assertFalse(Lock::isLocked($this->lockConfigId, $lockId)); @@ -107,7 +107,7 @@ public function testCancelLockCallabale(): void $result = Lock::lock($this->lockConfigId, static function (): void { Assert::assertTrue(false); }, function () use ($lockId) { - Assert::assertEquals(RequestContext::getCurrentFlag(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); + Assert::assertEquals(RequestContext::getCurrentId(), Lock::getInstance($this->lockConfigId, $lockId)->getLockFlag()); return true; }, $lockId); diff --git a/tests/unit/Component/Tests/RequestContextTest.php b/tests/unit/Component/Tests/RequestContextTest.php index c678da0814..0864638fcd 100644 --- a/tests/unit/Component/Tests/RequestContextTest.php +++ b/tests/unit/Component/Tests/RequestContextTest.php @@ -12,6 +12,19 @@ */ class RequestContextTest extends BaseTest { + public function testDefer(): void + { + $result = []; + RequestContext::defer(static function () use (&$result): void { + $result[] = 1; + }); + RequestContext::defer(static function () use (&$result): void { + $result[] = 2; + }); + RequestContext::destroy(); + $this->assertEquals([2, 1], $result); + } + public function testRemember(): void { $key = 'test_remember';