Initial commit
This commit is contained in:
122
.github/workflows/ci.yaml
vendored
Normal file
122
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,122 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
php:
|
||||
- '7.4'
|
||||
- '8.0'
|
||||
dependencies:
|
||||
- lowest
|
||||
- highest
|
||||
|
||||
services:
|
||||
nsqd:
|
||||
image: nsqio/nsq:v1.2.0
|
||||
options: --entrypoint /nsqd
|
||||
ports:
|
||||
- 4150:4150
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
coverage: pcov
|
||||
|
||||
- name: Setup Problem Matchers for PHPUnit
|
||||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||
|
||||
- name: Determine Composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "::set-output name=directory::$(composer config cache-dir)"
|
||||
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.directory }}
|
||||
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.dependencies }}-composer-
|
||||
|
||||
- name: Install highest dependencies
|
||||
run: composer update --no-progress --no-interaction --prefer-dist
|
||||
if: ${{ matrix.dependencies == 'highest' }}
|
||||
|
||||
- name: Install lowest dependencies
|
||||
run: composer update --no-progress --no-interaction --prefer-dist --prefer-lowest
|
||||
if: ${{ matrix.dependencies == 'lowest' }}
|
||||
|
||||
- name: Run tests
|
||||
run: vendor/bin/phpunit --coverage-clover=build/coverage-report.xml
|
||||
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: build/coverage-report.xml
|
||||
|
||||
php-cs-fixer:
|
||||
name: PHP-CS-Fixer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '7.4'
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer update --no-progress --no-interaction --prefer-dist
|
||||
|
||||
- name: Run script
|
||||
run: composer phpcs
|
||||
|
||||
phpstan:
|
||||
name: PHPStan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '7.4'
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer update --no-progress --no-interaction --prefer-dist
|
||||
|
||||
- name: Run script
|
||||
run: composer phpstan
|
||||
|
||||
psalm:
|
||||
name: Psalm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '7.4'
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer update --no-progress --no-interaction --prefer-dist
|
||||
|
||||
- name: Run script
|
||||
run: composer psalm
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/vendor/
|
||||
/composer.lock
|
19
.php_cs.dist
Normal file
19
.php_cs.dist
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PhpCsFixer' => true,
|
||||
'@PhpCsFixer:risky' => true,
|
||||
'@PSR12' => true,
|
||||
'@PSR12:risky' => true,
|
||||
'declare_strict_types' => true,
|
||||
'php_unit_internal_class' => false,
|
||||
'php_unit_test_class_requires_covers' => false,
|
||||
'yoda_style' => true,
|
||||
])
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()
|
||||
->in(__DIR__)
|
||||
);
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 nsqphp
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
42
README.md
Normal file
42
README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Nsq PHP
|
||||
|
||||
<img src="https://github.com/nsqphp/nsqphp/raw/main/logo.png" alt="" align="left" width="150">
|
||||
|
||||
A NSQ Client library for PHP.
|
||||
|
||||
[](//packagist.org/packages/nsq/nsq) [](//packagist.org/packages/nsq/nsq) [](//packagist.org/packages/nsq/nsq) [](//packagist.org/packages/nsq/nsq)
|
||||
[](https://codecov.io/gh/nsqphp/nsqphp)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
This library is installable via [Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
composer require nsq/nsq
|
||||
```
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
This library requires PHP 7.4 or later.
|
||||
|
||||
Although not required, it is recommended that you install the [phpinnacle/ext-buffer](https://github.com/phpinnacle/ext-buffer) to speed up [phpinnacle/buffer](https://github.com/phpinnacle/buffer) .
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- [x] SUB
|
||||
- [x] PUB
|
||||
- [ ] Feature Negotiation
|
||||
- [ ] Discovery
|
||||
- [ ] Backoff
|
||||
- [ ] TLS
|
||||
- [ ] Snappy
|
||||
- [ ] Sampling
|
||||
- [ ] AUTH
|
||||
|
||||
License:
|
||||
--------
|
||||
|
||||
The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained by [Spiral Scout](https://spiralscout.com).
|
52
composer.json
Normal file
52
composer.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "nsq/nsq",
|
||||
"type": "library",
|
||||
"description": "NSQ Client for PHP",
|
||||
"homepage": "https://github.com/nsqphp/nsqphp",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Grachev",
|
||||
"email": "me@grachevko.ru"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"ext-json": "*",
|
||||
"clue/socket-raw": "^1.5",
|
||||
"phpinnacle/buffer": "^1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "9999999-dev",
|
||||
"friendsofphp/php-cs-fixer": "^2.18",
|
||||
"phpstan/phpstan": "^0.12.68",
|
||||
"phpstan/phpstan-phpunit": "^0.12.17",
|
||||
"phpstan/phpstan-strict-rules": "^0.12.9",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"vimeo/psalm": "^4.4"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Nsq\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"scripts": {
|
||||
"phpcs": [
|
||||
"vendor/bin/php-cs-fixer fix --verbose --diff --dry-run"
|
||||
],
|
||||
"phpstan": [
|
||||
"vendor/bin/phpstan analyse"
|
||||
],
|
||||
"psalm": [
|
||||
"vendor/bin/psalm"
|
||||
],
|
||||
"tests": [
|
||||
"vendor/bin/phpunit --verbose"
|
||||
]
|
||||
}
|
||||
}
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
nsqd:
|
||||
image: nsqio/nsq:v1.2.0
|
||||
command: /nsqd
|
||||
ports:
|
||||
- 4150:4150
|
||||
|
||||
nsqadmin:
|
||||
image: nsqio/nsq:v1.2.0
|
||||
command: /nsqadmin --nsqd-http-address=nsqd:4151 --http-address=0.0.0.0:4171
|
||||
ports:
|
||||
- 4171:4171
|
10
phpstan.neon
Normal file
10
phpstan.neon
Normal file
@ -0,0 +1,10 @@
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-phpunit/extension.neon
|
||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
|
||||
|
||||
parameters:
|
||||
level: 7
|
||||
paths:
|
||||
- src
|
||||
- tests
|
20
phpunit.xml.dist
Normal file
20
phpunit.xml.dist
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
|
||||
colors="true"
|
||||
>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./src/</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
</php>
|
||||
<testsuites>
|
||||
<testsuite name="Nsq Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
16
psalm.xml
Normal file
16
psalm.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
allowPhpStormGenerics="true"
|
||||
ignoreInternalFunctionFalseReturn="false"
|
||||
ignoreInternalFunctionNullReturn="false"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="src"/>
|
||||
<ignoreFiles>
|
||||
<directory name="vendor"/>
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
</psalm>
|
19
src/Config.php
Normal file
19
src/Config.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class Config
|
||||
{
|
||||
public string $address;
|
||||
|
||||
public function __construct(
|
||||
string $address
|
||||
) {
|
||||
$this->address = $address;
|
||||
}
|
||||
}
|
136
src/Connection.php
Normal file
136
src/Connection.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
use LogicException;
|
||||
use PHPinnacle\Buffer\ByteBuffer;
|
||||
use Socket\Raw\Factory;
|
||||
use Socket\Raw\Socket;
|
||||
use Throwable;
|
||||
use function json_encode;
|
||||
use function pack;
|
||||
use function sprintf;
|
||||
use const JSON_FORCE_OBJECT;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use const PHP_EOL;
|
||||
|
||||
final class Connection
|
||||
{
|
||||
private const OK = 'OK';
|
||||
private const HEARTBEAT = '_heartbeat_';
|
||||
private const CLOSE_WAIT = 'CLOSE_WAIT';
|
||||
private const TYPE_RESPONSE = 0;
|
||||
private const TYPE_ERROR = 1;
|
||||
private const TYPE_MESSAGE = 2;
|
||||
private const BYTES_SIZE = 4;
|
||||
private const BYTES_TYPE = 4;
|
||||
private const BYTES_ATTEMPTS = 2;
|
||||
private const BYTES_TIMESTAMP = 8;
|
||||
private const BYTES_ID = 16;
|
||||
private const MAGIC_V2 = ' V2';
|
||||
|
||||
public Socket $socket;
|
||||
|
||||
public bool $closed = false;
|
||||
|
||||
private function __construct(Socket $socket)
|
||||
{
|
||||
$this->socket = $socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnsafeInstantiation
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function connect(Config $config): self
|
||||
{
|
||||
$socket = (new Factory())->createClient($config->address);
|
||||
$socket->write(self::MAGIC_V2);
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return new self($socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<string, string|numeric> $arr
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseOperand
|
||||
*/
|
||||
public function identify(array $arr): string
|
||||
{
|
||||
$body = json_encode($arr, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT);
|
||||
$size = pack('N', \strlen($body));
|
||||
|
||||
return 'IDENTIFY '.PHP_EOL.$size.$body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress PossiblyFalseOperand
|
||||
*/
|
||||
public function auth(string $secret): string
|
||||
{
|
||||
$size = pack('N', \strlen($secret));
|
||||
|
||||
return 'AUTH'.PHP_EOL.$size.$secret;
|
||||
}
|
||||
|
||||
public function write(string $buffer): void
|
||||
{
|
||||
if ($this->closed) {
|
||||
throw new LogicException('This connection is closed, create new one.');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->socket->write($buffer);
|
||||
} catch (Throwable $e) {
|
||||
$this->closed = true;
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function read(): ?Message
|
||||
{
|
||||
$socket = $this->socket;
|
||||
|
||||
$buffer = new ByteBuffer($socket->read(self::BYTES_SIZE + self::BYTES_TYPE));
|
||||
$size = $buffer->consumeUint32();
|
||||
$type = $buffer->consumeUint32();
|
||||
|
||||
$buffer->append($socket->read($size - self::BYTES_TYPE));
|
||||
|
||||
if (self::TYPE_RESPONSE === $type) {
|
||||
$response = $buffer->consume($size - self::BYTES_TYPE);
|
||||
|
||||
if (self::OK === $response || self::CLOSE_WAIT === $response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (self::HEARTBEAT === $response) {
|
||||
$socket->write('NOP'.PHP_EOL);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new LogicException(sprintf('Unexpected response from nsq: "%s"', $response));
|
||||
}
|
||||
|
||||
if (self::TYPE_ERROR === $type) {
|
||||
throw new LogicException(sprintf('NSQ return error: "%s"', $socket->read($size)));
|
||||
}
|
||||
|
||||
if (self::TYPE_MESSAGE !== $type) {
|
||||
throw new LogicException(sprintf('Expecting "%s" type, but NSQ return: "%s"', self::TYPE_MESSAGE, $type));
|
||||
}
|
||||
|
||||
$timestamp = $buffer->consumeInt64();
|
||||
$attempts = $buffer->consumeUint16();
|
||||
$id = $buffer->consume(self::BYTES_ID);
|
||||
$body = $buffer->consume($size - self::BYTES_TYPE - self::BYTES_TIMESTAMP - self::BYTES_ATTEMPTS - self::BYTES_ID);
|
||||
|
||||
return new Message($timestamp, $attempts, $id, $body);
|
||||
}
|
||||
}
|
51
src/Envelope.php
Normal file
51
src/Envelope.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class Envelope
|
||||
{
|
||||
public Message $message;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $acknowledge;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $requeue;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $touching;
|
||||
|
||||
public function __construct(Message $message, callable $ack, callable $req, callable $touch)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->acknowledge = $ack;
|
||||
$this->requeue = $req;
|
||||
$this->touching = $touch;
|
||||
}
|
||||
|
||||
public function ack(): void
|
||||
{
|
||||
\call_user_func($this->acknowledge);
|
||||
}
|
||||
|
||||
public function retry(int $timeout): void
|
||||
{
|
||||
\call_user_func($this->requeue, $timeout);
|
||||
}
|
||||
|
||||
public function touch(): void
|
||||
{
|
||||
\call_user_func($this->touching);
|
||||
}
|
||||
}
|
27
src/Message.php
Normal file
27
src/Message.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class Message
|
||||
{
|
||||
public int $timestamp;
|
||||
|
||||
public int $attempts;
|
||||
|
||||
public string $id;
|
||||
|
||||
public string $body;
|
||||
|
||||
public function __construct(int $timestamp, int $attempts, string $id, string $body)
|
||||
{
|
||||
$this->timestamp = $timestamp;
|
||||
$this->attempts = $attempts;
|
||||
$this->id = $id;
|
||||
$this->body = $body;
|
||||
}
|
||||
}
|
99
src/Reader.php
Normal file
99
src/Reader.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
use Throwable;
|
||||
use function sprintf;
|
||||
use const PHP_EOL;
|
||||
|
||||
class Reader
|
||||
{
|
||||
private Connection $connection;
|
||||
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a topic/channel.
|
||||
*/
|
||||
public function sub(string $topic, string $channel): void
|
||||
{
|
||||
$buffer = sprintf('SUB %s %s', $topic, $channel).PHP_EOL;
|
||||
|
||||
$this->connection->write($buffer);
|
||||
$this->connection->read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update RDY state (indicate you are ready to receive N messages).
|
||||
*/
|
||||
public function rdy(int $count): void
|
||||
{
|
||||
$this->connection->write('RDY '.$count.PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish a message (indicate successful processing).
|
||||
*/
|
||||
public function fin(string $id): void
|
||||
{
|
||||
$this->connection->write('FIN '.$id.PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-queue a message (indicate failure to process)
|
||||
* The re-queued message is placed at the tail of the queue, equivalent to having just published it,
|
||||
* but for various implementation specific reasons that behavior should not be explicitly relied upon and may change in the future.
|
||||
* Similarly, a message that is in-flight and times out behaves identically to an explicit REQ.
|
||||
*/
|
||||
public function req(string $id, int $timeout): void
|
||||
{
|
||||
$this->connection->write(sprintf('REQ %s %s', $id, $timeout).PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the timeout for an in-flight message.
|
||||
*/
|
||||
public function touch(string $id): void
|
||||
{
|
||||
$this->connection->write('TOUCH '.$id.PHP_EOL);
|
||||
}
|
||||
|
||||
public function consume(?float $timeout = null): ?Message
|
||||
{
|
||||
if (false === $this->connection->socket->selectRead($timeout)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->connection->read() ?? $this->consume(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanly close your connection (no more messages are sent).
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if ($this->connection->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connection->closed = true;
|
||||
|
||||
$this->connection->socket->write('CLS'.PHP_EOL);
|
||||
$this->connection->read();
|
||||
|
||||
try {
|
||||
$this->connection->socket->close();
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
74
src/Subscriber.php
Normal file
74
src/Subscriber.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
use Generator;
|
||||
use LogicException;
|
||||
|
||||
final class Subscriber
|
||||
{
|
||||
private Reader $reader;
|
||||
|
||||
public function __construct(Reader $reader)
|
||||
{
|
||||
$this->reader = $reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return Generator<int, Envelope|null, true|null, void>
|
||||
*/
|
||||
public function subscribe(string $topic, string $channel, ?float $timeout = 0): Generator
|
||||
{
|
||||
$reader = $this->reader;
|
||||
$reader->sub($topic, $channel);
|
||||
$reader->rdy(1);
|
||||
|
||||
while (true) {
|
||||
$message = $reader->consume($timeout);
|
||||
|
||||
if (null === $message) {
|
||||
if (true === yield null) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$finished = false;
|
||||
$envelop = new Envelope(
|
||||
$message,
|
||||
static function () use ($reader, $message, &$finished): void {
|
||||
if ($finished) {
|
||||
throw new LogicException('Can\'t ack, message already finished.');
|
||||
}
|
||||
|
||||
$finished = true;
|
||||
|
||||
$reader->fin($message->id);
|
||||
},
|
||||
static function (int $timeout) use ($reader, $message, &$finished): void {
|
||||
if ($finished) {
|
||||
throw new LogicException('Can\'t retry, message already finished.');
|
||||
}
|
||||
|
||||
$finished = true;
|
||||
|
||||
$reader->req($message->id, $timeout);
|
||||
},
|
||||
static function () use ($reader, $message): void {
|
||||
$reader->touch($message->id);
|
||||
},
|
||||
);
|
||||
|
||||
if (true === yield $envelop) {
|
||||
break;
|
||||
}
|
||||
|
||||
$reader->rdy(1);
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
}
|
||||
}
|
68
src/Writer.php
Normal file
68
src/Writer.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
use function array_map;
|
||||
use function implode;
|
||||
use function pack;
|
||||
use function sprintf;
|
||||
use const PHP_EOL;
|
||||
|
||||
final class Writer
|
||||
{
|
||||
private Connection $connection;
|
||||
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress PossiblyFalseOperand
|
||||
*/
|
||||
public function pub(string $topic, string $body): void
|
||||
{
|
||||
$size = pack('N', \strlen($body));
|
||||
|
||||
$buffer = 'PUB '.$topic.PHP_EOL.$size.$body;
|
||||
|
||||
$this->connection->write($buffer);
|
||||
$this->connection->read();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param array<mixed, mixed> $bodies
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseOperand
|
||||
*/
|
||||
public function mpub(string $topic, array $bodies): void
|
||||
{
|
||||
$num = pack('N', \count($bodies));
|
||||
|
||||
$mb = implode('', array_map(static function ($body): string {
|
||||
return pack('N', \strlen($body)).$body;
|
||||
}, $bodies));
|
||||
|
||||
$size = pack('N', \strlen($num.$mb));
|
||||
|
||||
$buffer = 'MPUB '.$topic.PHP_EOL.$size.$num.$mb;
|
||||
|
||||
$this->connection->write($buffer);
|
||||
$this->connection->read();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress PossiblyFalseOperand
|
||||
*/
|
||||
public function dpub(string $topic, int $deferTime, string $body): void
|
||||
{
|
||||
$size = pack('N', \strlen($body));
|
||||
|
||||
$buffer = sprintf('DPUB %s %s', $topic, $deferTime).PHP_EOL.$size.$body;
|
||||
|
||||
$this->connection->write($buffer);
|
||||
$this->connection->read();
|
||||
}
|
||||
}
|
35
tests/NsqTest.php
Normal file
35
tests/NsqTest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Nsq\Config;
|
||||
use Nsq\Connection;
|
||||
use Nsq\Envelope;
|
||||
use Nsq\Reader;
|
||||
use Nsq\Subscriber;
|
||||
use Nsq\Writer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class NsqTest extends TestCase
|
||||
{
|
||||
public function test(): void
|
||||
{
|
||||
$config = new Config('tcp://localhost:4150');
|
||||
|
||||
$writer = new Writer(Connection::connect($config));
|
||||
$writer->pub(__FUNCTION__, __FUNCTION__);
|
||||
|
||||
$subscriber = new Subscriber(new Reader(Connection::connect($config)));
|
||||
$generator = $subscriber->subscribe(__FUNCTION__, __FUNCTION__, 1);
|
||||
|
||||
$envelope = $generator->current();
|
||||
|
||||
static::assertInstanceOf(Envelope::class, $envelope);
|
||||
/** @var Envelope $envelope */
|
||||
static::assertSame(__FUNCTION__, $envelope->message->body);
|
||||
$envelope->ack();
|
||||
|
||||
$generator->next();
|
||||
static::assertNull($generator->current());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user