Explode Response to Frames
This commit is contained in:
@@ -8,7 +8,14 @@ use Nsq\Config\ClientConfig;
|
||||
use Nsq\Config\ConnectionConfig;
|
||||
use Nsq\Exception\AuthenticationRequired;
|
||||
use Nsq\Exception\ConnectionFail;
|
||||
use Nsq\Exception\UnexpectedResponse;
|
||||
use Nsq\Exception\NsqError;
|
||||
use Nsq\Exception\BadResponse;
|
||||
use Nsq\Exception\NsqException;
|
||||
use Nsq\Exception\NullReceived;
|
||||
use Nsq\Protocol\Error;
|
||||
use Nsq\Protocol\Frame;
|
||||
use Nsq\Protocol\Message;
|
||||
use Nsq\Protocol\Response;
|
||||
use Nsq\Reconnect\ExponentialStrategy;
|
||||
use Nsq\Reconnect\ReconnectStrategy;
|
||||
use PHPinnacle\Buffer\ByteBuffer;
|
||||
@@ -77,20 +84,27 @@ abstract class Connection
|
||||
|
||||
$body = json_encode($this->clientConfig, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT);
|
||||
|
||||
$response = $this->command('IDENTIFY', data: $body)->response();
|
||||
|
||||
$this->connectionConfig = ConnectionConfig::fromArray($response->toArray());
|
||||
$this->connectionConfig = ConnectionConfig::fromArray(
|
||||
$this
|
||||
->command('IDENTIFY', data: $body)
|
||||
->readResponse()
|
||||
->toArray()
|
||||
);
|
||||
|
||||
if ($this->connectionConfig->snappy || $this->connectionConfig->deflate) {
|
||||
$this->response()->okOrFail();
|
||||
$this->checkIsOK();
|
||||
}
|
||||
|
||||
if ($this->connectionConfig->authRequired) {
|
||||
if (null === $this->clientConfig->authSecret) {
|
||||
throw new AuthenticationRequired('NSQ requires authorization, set ClientConfig::$authSecret before connecting');
|
||||
throw new AuthenticationRequired();
|
||||
}
|
||||
|
||||
$authResponse = $this->command('AUTH', data: $this->clientConfig->authSecret)->response()->toArray();
|
||||
$authResponse = $this
|
||||
->command('AUTH', data: $this->clientConfig->authSecret)
|
||||
->readResponse()
|
||||
->toArray()
|
||||
;
|
||||
|
||||
$this->logger->info('Authorization response: '.http_build_query($authResponse));
|
||||
}
|
||||
@@ -171,7 +185,7 @@ abstract class Connection
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
public function receive(float $timeout = null): ?Response
|
||||
protected function readFrame(float $timeout = null): ?Frame
|
||||
{
|
||||
$socket = $this->socket();
|
||||
|
||||
@@ -206,12 +220,23 @@ abstract class Connection
|
||||
|
||||
$this->logger->debug('Received buffer: '.addcslashes($buffer->bytes(), PHP_EOL));
|
||||
|
||||
$response = new Response($buffer);
|
||||
$frame = match ($type = $buffer->consumeUint32()) {
|
||||
0 => new Response($buffer->flush()),
|
||||
1 => new Error($buffer->flush()),
|
||||
2 => new Message(
|
||||
timestamp: $buffer->consumeInt64(),
|
||||
attempts: $buffer->consumeUint16(),
|
||||
id: $buffer->consume(Bytes::BYTES_ID),
|
||||
body: $buffer->flush(),
|
||||
consumer: $this instanceof Consumer ? $this : throw new NsqException('what?'),
|
||||
),
|
||||
default => throw new NsqException('Unexpected frame type: '.$type)
|
||||
};
|
||||
|
||||
if ($response->isHeartBeat()) {
|
||||
if ($frame instanceof Response && $frame->isHeartBeat()) {
|
||||
$this->command('NOP');
|
||||
|
||||
return $this->receive(
|
||||
return $this->readFrame(
|
||||
($currentTime = microtime(true)) > $deadline ? 0 : $deadline - $currentTime
|
||||
);
|
||||
}
|
||||
@@ -224,12 +249,35 @@ abstract class Connection
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return $response;
|
||||
return $frame;
|
||||
}
|
||||
|
||||
protected function response(): Response
|
||||
protected function checkIsOK(): void
|
||||
{
|
||||
return $this->receive() ?? throw UnexpectedResponse::null();
|
||||
$response = $this->readResponse();
|
||||
|
||||
if (!$response->isOk()) {
|
||||
throw new BadResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
private function readResponse(): Response
|
||||
{
|
||||
$frame = $this->readFrame() ?? throw new NullReceived();
|
||||
|
||||
if ($frame instanceof Response) {
|
||||
return $frame;
|
||||
}
|
||||
|
||||
if ($frame instanceof Error) {
|
||||
if ($frame->type->terminateConnection) {
|
||||
$this->disconnect();
|
||||
}
|
||||
|
||||
throw new NsqError($frame);
|
||||
}
|
||||
|
||||
throw new NsqException('Unreachable statement.');
|
||||
}
|
||||
|
||||
private function socket(): Socket
|
||||
|
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
use Nsq\Exception\NsqError;
|
||||
use Nsq\Exception\NsqException;
|
||||
use Nsq\Protocol\Error;
|
||||
use Nsq\Protocol\Message;
|
||||
|
||||
final class Consumer extends Connection
|
||||
{
|
||||
private int $rdy = 0;
|
||||
@@ -13,7 +18,26 @@ final class Consumer extends Connection
|
||||
*/
|
||||
public function sub(string $topic, string $channel): void
|
||||
{
|
||||
$this->command('SUB', [$topic, $channel])->response()->okOrFail();
|
||||
$this->command('SUB', [$topic, $channel])->checkIsOK();
|
||||
}
|
||||
|
||||
public function readMessage(): ?Message
|
||||
{
|
||||
$frame = $this->readFrame();
|
||||
|
||||
if ($frame instanceof Message || null === $frame) {
|
||||
return $frame;
|
||||
}
|
||||
|
||||
if ($frame instanceof Error) {
|
||||
if ($frame->type->terminateConnection) {
|
||||
$this->disconnect();
|
||||
}
|
||||
|
||||
throw new NsqError($frame);
|
||||
}
|
||||
|
||||
throw new NsqException('Unreachable statement.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class AuthenticationRequired extends RuntimeException implements NsqException
|
||||
final class AuthenticationRequired extends NsqException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('NSQ requires authorization, set ClientConfig::$authSecret before connecting');
|
||||
}
|
||||
}
|
||||
|
15
src/Exception/BadResponse.php
Normal file
15
src/Exception/BadResponse.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Exception;
|
||||
|
||||
use Nsq\Protocol\Response;
|
||||
|
||||
final class BadResponse extends NsqException
|
||||
{
|
||||
public function __construct(Response $response)
|
||||
{
|
||||
parent::__construct($response->msg);
|
||||
}
|
||||
}
|
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class ConnectionFail extends RuntimeException implements NsqException
|
||||
final class ConnectionFail extends NsqException
|
||||
{
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
|
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Exception;
|
||||
|
||||
use Nsq\Message;
|
||||
use RuntimeException;
|
||||
use Nsq\Protocol\Message;
|
||||
|
||||
final class MessageAlreadyFinished extends RuntimeException implements NsqException
|
||||
final class MessageAlreadyFinished extends NsqException
|
||||
{
|
||||
public static function finish(Message $message): self
|
||||
{
|
||||
|
@@ -4,8 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use Nsq\Protocol\Error;
|
||||
|
||||
final class NsqError extends RuntimeException implements NsqException
|
||||
final class NsqError extends NsqException
|
||||
{
|
||||
public function __construct(Error $error)
|
||||
{
|
||||
parent::__construct($error->rawData);
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Exception;
|
||||
|
||||
use Throwable;
|
||||
use RuntimeException;
|
||||
|
||||
interface NsqException extends Throwable
|
||||
class NsqException extends RuntimeException
|
||||
{
|
||||
}
|
||||
|
9
src/Exception/NullReceived.php
Normal file
9
src/Exception/NullReceived.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Exception;
|
||||
|
||||
final class NullReceived extends NsqException
|
||||
{
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class UnexpectedResponse extends RuntimeException implements NsqException
|
||||
{
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function null(): self
|
||||
{
|
||||
return new self('Response was expected, but null received.');
|
||||
}
|
||||
}
|
@@ -15,7 +15,7 @@ final class Producer extends Connection
|
||||
*/
|
||||
public function pub(string $topic, string $body): void
|
||||
{
|
||||
$this->command('PUB', $topic, $body)->response()->okOrFail();
|
||||
$this->command('PUB', $topic, $body)->checkIsOK();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ final class Producer extends Connection
|
||||
return pack('N', \strlen($body)).$body;
|
||||
}, $bodies));
|
||||
|
||||
$this->command('MPUB', $topic, $num.$mb)->response()->okOrFail();
|
||||
$this->command('MPUB', $topic, $num.$mb)->checkIsOK();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,6 +39,6 @@ final class Producer extends Connection
|
||||
*/
|
||||
public function dpub(string $topic, string $body, int $delay): void
|
||||
{
|
||||
$this->command('DPUB', [$topic, $delay], $body)->response()->okOrFail();
|
||||
$this->command('DPUB', [$topic, $delay], $body)->checkIsOK();
|
||||
}
|
||||
}
|
||||
|
23
src/Protocol/Error.php
Normal file
23
src/Protocol/Error.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Protocol;
|
||||
|
||||
use Nsq\Bytes;
|
||||
use function explode;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class Error extends Frame
|
||||
{
|
||||
public ErrorType $type;
|
||||
|
||||
public function __construct(public string $rawData)
|
||||
{
|
||||
parent::__construct(\strlen($this->rawData) + Bytes::BYTES_TYPE);
|
||||
|
||||
$this->type = new ErrorType(explode(' ', $this->rawData)[0]);
|
||||
}
|
||||
}
|
100
src/Protocol/ErrorType.php
Normal file
100
src/Protocol/ErrorType.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Protocol;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class ErrorType
|
||||
{
|
||||
/**
|
||||
* A generic error type without any more hints.
|
||||
*/
|
||||
public const E_INVALID = true;
|
||||
/**
|
||||
* This error might be returned during multiple occasions. It can be returned for IDENTIFY, AUTH or MPUB messages.
|
||||
* It is caused for payloads that do not meet certain requirements. For IDENTIFY and AUTH, this is usually a bug in
|
||||
* the library and should be reported. For MPUB, this error can occur if the payload is larger than the maximum
|
||||
* payload size specified in the nsqd config.
|
||||
*/
|
||||
public const E_BAD_BODY = true;
|
||||
/**
|
||||
* This error indicates that the topic sent to nsqd is not valid.
|
||||
*/
|
||||
public const E_BAD_TOPIC = true;
|
||||
/**
|
||||
* This error indicates that the channel sent to nsqd is not valid.
|
||||
*/
|
||||
public const E_BAD_CHANNEL = true;
|
||||
/**
|
||||
* This error is returned by nsqd if the message in the payload of a publishing operation does not meet the
|
||||
* requirements of the server. This might be caused by too big payloads being sent to nsqd. You should consider
|
||||
* adding a limit to the payload size or increasing it in the nsqd config.
|
||||
*/
|
||||
public const E_BAD_MESSAGE = true;
|
||||
/**
|
||||
* This error may happen if a error condition is met after validating the input on the nsqd side. This is usually a
|
||||
* temporary error and can be caused by topics being added, deleted or cleared.
|
||||
*/
|
||||
public const E_PUB_FAILED = true;
|
||||
/**
|
||||
* This error may happen if a error condition is met after validating the input on the nsqd side. This is usually a
|
||||
* temporary error and can be caused by topics being added, deleted or cleared.
|
||||
*/
|
||||
public const E_MPUB_FAILED = true;
|
||||
/**
|
||||
* This error may happen if a error condition is met after validating the input on the nsqd side. This is usually a
|
||||
* temporary error and can be caused by topics being added, deleted or cleared.
|
||||
*/
|
||||
public const E_DPUB_FAILED = true;
|
||||
/**
|
||||
* This error may happen if a error condition is met after validating the input on the nsqd side. This can
|
||||
* happen in particular for messages that are no longer queued on the server side.
|
||||
*/
|
||||
public const E_FIN_FAILED = false;
|
||||
/**
|
||||
* This error may happen if a error condition is met after validating the input on the nsqd side. This can
|
||||
* happen in particular for messages that are no longer queued on the server side.
|
||||
*/
|
||||
public const E_REQ_FAILED = false;
|
||||
/**
|
||||
* This error may happen if a error condition is met after validating the input on the nsqd side. This can
|
||||
* happen in particular for messages that are no longer queued on the server side.
|
||||
*/
|
||||
public const E_TOUCH_FAILED = false;
|
||||
/**
|
||||
* This error indicates that the authorization of the client failed on the server side. This might be related
|
||||
* to connection issues to the authorization server. Depending on the authorization server implementation, this
|
||||
* might also indicate that the given auth secret in the [ClientConfig] is not known on the server or the server
|
||||
* denied authentication with the current connection properties (i.e. TLS status and IP).
|
||||
*/
|
||||
public const E_AUTH_FAILED = true;
|
||||
/**
|
||||
* This error happens if something breaks on the nsqd side while performing the authorization. This might be
|
||||
* caused by bugs in nsqd, the authorization server or network issues.
|
||||
*/
|
||||
public const E_AUTH_ERROR = true;
|
||||
/**
|
||||
* This error is sent by nsqd if the client attempts an authentication, but the server does not support it. This
|
||||
* should never happen using this library as authorization requests are only sent if the server supports it.
|
||||
* It is safe to expect that this error is never thrown.
|
||||
*/
|
||||
public const E_AUTH_DISABLED = true;
|
||||
/**
|
||||
* This error indicates that the client related to the authorization secret set in the [ClientConfig] is not
|
||||
* allowed to do the operation it tried to do.
|
||||
*/
|
||||
public const E_UNAUTHORIZED = true;
|
||||
|
||||
/**
|
||||
* A boolean indicating whether or not an [Error] with this type terminates the connection or not.
|
||||
*/
|
||||
public bool $terminateConnection;
|
||||
|
||||
public function __construct(public string $type)
|
||||
{
|
||||
$this->terminateConnection = \constant('self::'.$this->type) ?? self::E_INVALID;
|
||||
}
|
||||
}
|
16
src/Protocol/Frame.php
Normal file
16
src/Protocol/Frame.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Protocol;
|
||||
|
||||
abstract class Frame
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* @psalm-readonly
|
||||
*/
|
||||
public int $length,
|
||||
) {
|
||||
}
|
||||
}
|
@@ -2,11 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
namespace Nsq\Protocol;
|
||||
|
||||
use Nsq\Bytes;
|
||||
use Nsq\Consumer;
|
||||
use Nsq\Exception\MessageAlreadyFinished;
|
||||
|
||||
final class Message
|
||||
final class Message extends Frame
|
||||
{
|
||||
/**
|
||||
* @psalm-readonly
|
||||
@@ -34,6 +36,14 @@ final class Message
|
||||
|
||||
public function __construct(int $timestamp, int $attempts, string $id, string $body, Consumer $consumer)
|
||||
{
|
||||
parent::__construct(
|
||||
Bytes::BYTES_TYPE
|
||||
+ Bytes::BYTES_TIMESTAMP
|
||||
+ Bytes::BYTES_ATTEMPTS
|
||||
+ Bytes::BYTES_ID
|
||||
+ \strlen($body)
|
||||
);
|
||||
|
||||
$this->timestamp = $timestamp;
|
||||
$this->attempts = $attempts;
|
||||
$this->id = $id;
|
41
src/Protocol/Response.php
Normal file
41
src/Protocol/Response.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq\Protocol;
|
||||
|
||||
use Nsq\Bytes;
|
||||
use function json_decode;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class Response extends Frame
|
||||
{
|
||||
public const OK = 'OK';
|
||||
public const HEARTBEAT = '_heartbeat_';
|
||||
|
||||
public function __construct(public string $msg)
|
||||
{
|
||||
parent::__construct(\strlen($this->msg) + Bytes::BYTES_TYPE);
|
||||
}
|
||||
|
||||
public function isOk(): bool
|
||||
{
|
||||
return self::OK === $this->msg;
|
||||
}
|
||||
|
||||
public function isHeartBeat(): bool
|
||||
{
|
||||
return self::HEARTBEAT === $this->msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return json_decode($this->msg, true, flags: JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nsq;
|
||||
|
||||
use Nsq\Exception\NsqError;
|
||||
use Nsq\Exception\UnexpectedResponse;
|
||||
use PHPinnacle\Buffer\ByteBuffer;
|
||||
use function json_decode;
|
||||
use function sprintf;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
final class Response
|
||||
{
|
||||
private const OK = 'OK';
|
||||
private const HEARTBEAT = '_heartbeat_';
|
||||
private const TYPE_RESPONSE = 0;
|
||||
private const TYPE_ERROR = 1;
|
||||
private const TYPE_MESSAGE = 2;
|
||||
|
||||
private int $type;
|
||||
|
||||
private ByteBuffer $buffer;
|
||||
|
||||
public function __construct(ByteBuffer $buffer)
|
||||
{
|
||||
$this->type = $buffer->consumeUint32();
|
||||
$this->buffer = $buffer;
|
||||
}
|
||||
|
||||
public function okOrFail(): void
|
||||
{
|
||||
if (self::TYPE_ERROR === $this->type) {
|
||||
throw new NsqError($this->buffer->bytes());
|
||||
}
|
||||
|
||||
if (self::TYPE_RESPONSE !== $this->type) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new UnexpectedResponse(sprintf('"%s" type expected, but "%s" received.', self::TYPE_RESPONSE, $this->type));
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if (self::OK !== $this->buffer->bytes()) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new UnexpectedResponse(sprintf('OK response expected, but "%s" received.', $this->buffer->bytes()));
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
public function isHeartBeat(): bool
|
||||
{
|
||||
return self::TYPE_RESPONSE === $this->type && self::HEARTBEAT === $this->buffer->bytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
if (self::TYPE_RESPONSE !== $this->type) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new UnexpectedResponse(sprintf('"%s" type expected, but "%s" received.', self::TYPE_RESPONSE, $this->type));
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return json_decode($this->buffer->bytes(), true, flags: JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public function toMessage(Consumer $reader): Message
|
||||
{
|
||||
if (self::TYPE_MESSAGE !== $this->type) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new UnexpectedResponse(sprintf('Expecting "%s" type, but NSQ return: "%s"', self::TYPE_MESSAGE, $this->type));
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$buffer = new ByteBuffer($this->buffer->bytes());
|
||||
|
||||
$timestamp = $buffer->consumeInt64();
|
||||
$attempts = $buffer->consumeUint16();
|
||||
$id = $buffer->consume(Bytes::BYTES_ID);
|
||||
$body = $buffer->flush();
|
||||
|
||||
return new Message($timestamp, $attempts, $id, $body, $reader);
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Nsq;
|
||||
|
||||
use Generator;
|
||||
use Nsq\Protocol\Message;
|
||||
|
||||
final class Subscriber
|
||||
{
|
||||
@@ -27,7 +28,7 @@ final class Subscriber
|
||||
while (true) {
|
||||
$this->reader->rdy(1);
|
||||
|
||||
$command = yield $this->reader->receive()?->toMessage($this->reader);
|
||||
$command = yield $this->reader->readMessage();
|
||||
|
||||
if (self::STOP === $command) {
|
||||
break;
|
||||
|
Reference in New Issue
Block a user