/** * Connect to an AMQP broker. * * @recoil-coroutine * * @param ConnectionOptions $options,... Options that configure the connection. * * @return Connection The AMQP connection. * @throws ConnectionException The connection could not be established. */ public function connect(ConnectionOptions $options, ConnectionOptions ...$more) : Generator { $options = func_get_args(); try { return (yield Recoil::any(...array_map([$this->connector, 'connect'], $options))); } catch (CompositeException $e) { throw ClusterConnectionException::couldNotConnect($options, $e->exceptions()); } }
/** * [COROUTINE] Close this stream. * * Closing a stream indicates that no more data will be read from the * stream. */ public function close() { if ($this->strand) { $this->strand->resumeWithException(new StreamClosedException()); $this->strand = null; } $this->stream->close(); $this->buffer = ''; (yield Recoil::noop()); }
/** * @recoil-coroutine */ public function start() : Generator { // start a strand that deals with the main connection logic ... $strand = (yield Recoil::execute($this->readLoop())); // if heartbeats are enabled, start an additional strand that checks // the heartbeat state at the negotiated interval ... if ($this->tune->heartbeat !== HandshakeManager::HEARTBEAT_DISABLED) { (yield Recoil::link($strand, (yield Recoil::execute($this->heartbeatLoop())))); } }
/** * [COROUTINE] Close this channel. * * Closing a channel indicates that no more values will be read from or * written to the channel. Any future read/write operations will fail. */ public function close() { $this->closed = true; while (!$this->writeStrands->isEmpty()) { $this->writeStrands->pop()->resumeWithException(new ChannelClosedException()); } while (!$this->readStrands->isEmpty()) { $this->readStrands->pop()->resumeWithException(new ChannelClosedException()); } (yield Recoil::noop()); }
/** * [COROUTINE] Close this stream. * * Closing a stream indicates that no more data will be read from the * stream. */ public function close() { if ($this->strand) { $this->strand->kernel()->eventLoop()->removeReadStream($this->stream); $this->strand->resumeWithException(new StreamClosedException()); $this->strand = null; } if (is_resource($this->stream)) { fclose($this->stream); } (yield Recoil::noop()); }
/** * @recoil-coroutine */ private function connectSocket(ConnectionOptions $options) : Generator { $errorCode = null; $errorMessage = null; $stream = @stream_socket_client(\sprintf('tcp://%s:%s', $options->host(), $options->port()), $errorCode, $errorMessage, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); if ($stream === false) { throw SingleConnectionException::couldNotConnect($options, $errorCode . ': ' . $errorMessage); } // Wait for the socket to open or fail ... stream_set_blocking($stream, false); list(, $writable) = (yield Recoil::select([$stream], [$stream])); if (empty($writable)) { throw SingleConnectionException::couldNotConnect($options, 'connection refused'); } return $stream; }
/** * Publish a message. * * @recoil-coroutine * * @param OutgoingMessage $message The message to publish. * * The returned promise is resolved when the publisher has sent the message * over the network. This does not necessarily mean that the message was * successfully routed to a queue or delivered to a consumer, or even that * it reached the broker. * * Additional messages may be published before the promise is resolved, in * which case they will be queued in memory. * * @throws ConnectionException The connection is closed. */ public function publish(OutgoingMessage $message) : Generator { if ($this->isClosed) { throw ChannelClosedException::channelClosed(); } // There is already a message being publish, suspend this strand // until we're ready to publish this message ... if ($this->currentMessage !== null) { (yield Recoil::suspend(function ($strand) { $this->strandQueue[$strand->id()] = $strand; }, function ($strand) { unset($this->strandQueue[$strand->id()]); })); } $this->currentMessage = $message; if ($this->channelManager === null) { $this->channelManager = (yield $this->connectionManager->acquireChannel()); } (yield $this->channelManager->send(BasicPublishFrame::create($this->exchangeName, $this->fixedRoutingKey ?: $message->routingKey(), $this->options->mandatory, $this->options->immediate))); $frame = new BasicHeaderFrame(); $frame->contentLength = $message->contentLength(); $message->properties()->to091HeaderFrame($frame); (yield $this->channelManager->send($frame)); if ($frame->contentLength > 0) { if ($message instanceof BufferedOutgoingMessage) { (yield $this->channelManager->sendContent($message->content())); } else { assert(false, 'not implemented'); // $stream = $this->currentMessage->contentStream(); // $stream->on('data', [$this, 'onStreamData']); // $stream->on('close', [$this, 'onStreamClose']); // $stream->on('error', [$this, 'onStreamError']); // $stream->resume(); } } $this->currentMessage = null; if (!empty($this->strandQueue)) { foreach ($this->strandQueue as $id => $strand) { unset($this->strandQueue[$id]); $strand->send(); break; } } elseif ($this->channel === null) { $this->channelManager->release(); $this->channelManager = null; } }
/** * [COROUTINE] Read a value from this channel. * * Execution of the current strand is suspended until a value is available. * * If the channel is already closed, or is closed while a read operation is * pending a ChannelClosedException is thrown. * * Read operations must be exclusive only if the underlying stream requires * exclusive reads. * * @return mixed The value read from the channel. * @throws ChannelClosedException if the channel has been closed. * @throws ChannelLockedException if concurrent reads are unsupported. */ public function read() { while (!$this->unserializer->hasValue()) { try { $buffer = (yield $this->stream->read($this->bufferSize)); } catch (StreamClosedException $e) { throw new ChannelClosedException($e); } catch (StreamLockedException $e) { throw new ChannelLockedException($e); } $this->unserializer->feed($buffer); if ($this->stream->isClosed()) { $this->unserializer->finalize(); } } (yield Recoil::return_($this->unserializer->unserialize())); // @codeCoverageIgnoreStart }
/** * Adapt a value into a coroutine. * * @param StrandInterface $strand The currently executing strand. * @param mixed $value The value to adapt. * * @return CoroutineInterface * @throws InvalidArgumentException if now valid adaptation can be made. */ public function adapt(StrandInterface $strand, $value) { while ($value instanceof CoroutineProviderInterface) { $value = $value->coroutine($strand); } if ($value instanceof CoroutineInterface) { return $value; } elseif ($value instanceof Generator) { return new GeneratorCoroutine($value); } elseif ($value instanceof PromiseInterface) { return new PromiseCoroutine($value); } elseif (is_array($value)) { return Recoil::all($value); } elseif (null === $value) { return Recoil::cooperate(); } throw new InvalidArgumentException('Unable to adapt ' . Repr::repr($value) . ' into a coroutine.'); }
/** * [COROUTINE] Close this stream. * * Closing a stream indicates that no more data will be written to the * stream. */ public function close() { if ($this->strand) { $this->strand->resumeWithException(new StreamClosedException()); $this->strand = null; $this->stream->close(); } else { (yield Recoil::suspend(function ($strand) { $this->stream->once('close', function () use($strand) { $strand->resumeWithValue(null); }); $this->stream->end(); })); } }
/** * Write a frame. * * @recoil-coroutine * * @param OutgoingFrame $frame The frame to write. * * @throws ChannelClosedException The channel has been closed. * @throws InvalidArgumentException The type of $value is unsupported. */ public function write($frame) : Generator { if ($this->stream === null) { throw ChannelClosedException::channelClosed(); } $buffer = $this->serializer->serialize($frame); assert(Debug::dumpOutgoingFrame('#' . (int) $this->stream, $frame)); assert(Debug::dumpOutgoingHex('#' . (int) $this->stream, $buffer)); $this->hasSentFrame = true; (yield Recoil::write($this->stream, $buffer)); }
/** * @recoil-coroutine */ public function onCloseByBroker(ChannelCloseFrame $frame) : Generator { $this->connectionManager->removeChannel($this->channelId); $exception = AmqpException::create($frame->replyText, $frame->replyCode); // The closure was triggered by a previously sent frame. Find the // offending strand and resume it with an exception specific to this // closure ... if ($frame->classId && $frame->methodId) { $sentMethod = $frame->classId << 16 | $frame->methodId; ($callers =& $this->callersBySentMethod[$sentMethod]) ?? null; if ($callers !== null) { foreach ($callers as $id => $waitMethod) { $strand = $this->callers[$waitMethod][$id]; unset($this->callers[$waitMethod][$id], $this->callersBySentMethod[$sentMethod][$id], $this->callersByWaitMethod[$waitMethod][$id]); (yield Recoil::throw($strand, $exception)); break; } } } // Acknowledge the closure ... $frame = new ChannelCloseOkFrame(); $frame->frameChannelId = $this->channelId; (yield $this->transport->write($frame)); // Procee to the regular on-close logic. To all remaining callers and // listeners this is an unexpected closure ... (yield $this->onClose(ChannelException::closedUnexpectedly($this->channelId, $exception))); }