88#include " util/arguments.h"
99#include " util/async_scope.h"
1010#include " util/error.h"
11+ #include " util/uvimmediate.h"
1112#include " util/uvloop.h"
1213#include " util/uvwork.h"
1314
1617#include < unordered_set>
1718
1819namespace zmq {
20+ /* The maximum number of sync I/O operations that are allowed before the I/O
21+ methods will force the returned promise to be resolved in the next tick. */
22+ auto constexpr max_sync_operations = 1 << 9 ;
23+
1924/* Ordinary static cast for all available numeric types. */
2025template <typename T>
2126T NumberCast (const Napi::Number& num) {
@@ -524,56 +529,61 @@ Napi::Value Socket::Send(const Napi::CallbackInfo& info) {
524529#endif
525530
526531 if (send_timeout == 0 || HasEvents (ZMQ_POLLOUT)) {
527- /* We can send on the socket immediately. This is a separate code
528- path so we can avoid creating a lambda. */
529- auto res = Napi::Promise::Deferred::New (Env ());
530- Send (res, parts);
531-
532- /* This operation may have caused a state change, so we must update
533- the poller state manually! */
534- poller.Trigger ();
535-
536- return res.Promise ();
537- } else {
538- /* Check if we are already polling for writes. If so that means
539- two async read operations are started; which we do not allow.
540- This is not laziness; we should not introduce additional queueing
541- because it would break ZMQ semantics. */
542- if (poller.PollingWritable ()) {
543- ErrnoException (Env (), EAGAIN).ThrowAsJavaScriptException ();
544- return Env ().Undefined ();
532+ /* We can send on the socket immediately. This is a fast path. NOTE: We
533+ must make sure to not keep returning synchronously resolved promises,
534+ or we will starve the event loop. This can happen because ZMQ uses a
535+ background I/O thread, which could mean that the Node.js process is
536+ busy sending data to the I/O thread but is no longer able to respond
537+ to other events. This operation may have caused a state change, so we
538+ must also update the poller state manually! */
539+ if (sync_operations++ < max_sync_operations) {
540+ auto res = Napi::Promise::Deferred::New (Env ());
541+ Send (res, parts);
542+ poller.Trigger ();
543+ return res.Promise ();
545544 }
546545
547- return poller. WritePromise (send_timeout, std::move (parts) );
546+ SetImmediate ( Env (), [&]() { poller. Trigger (); } );
548547 }
548+
549+ /* Check if we are already polling for writes. If so that means two async
550+ read operations are started; which we do not allow. This is not laziness;
551+ we should not introduce additional queueing because it would break ZMQ
552+ semantics. */
553+ if (poller.PollingWritable ()) {
554+ ErrnoException (Env (), EAGAIN).ThrowAsJavaScriptException ();
555+ return Env ().Undefined ();
556+ }
557+
558+ return poller.WritePromise (send_timeout, std::move (parts));
549559}
550560
551561Napi::Value Socket::Receive (const Napi::CallbackInfo& info) {
552562 if (!ValidateArguments (info, {})) return Env ().Undefined ();
553563 if (!ValidateOpen ()) return Env ().Undefined ();
554564
555565 if (receive_timeout == 0 || HasEvents (ZMQ_POLLIN)) {
556- /* We can read from the socket immediately. This is a separate code
557- path so we can avoid creating a lambda. */
558- auto res = Napi::Promise::Deferred::New (Env ());
559- Receive (res);
560-
561- /* This operation may have caused a state change, so we must update
562- the poller state manually! */
563- poller.Trigger ();
564-
565- return res.Promise ();
566- } else {
567- /* Check if we are already polling for reads. Only one promise may
568- receive the next message, so we must ensure that receive
569- operations are in sequence. */
570- if (poller.PollingReadable ()) {
571- ErrnoException (Env (), EAGAIN).ThrowAsJavaScriptException ();
572- return Env ().Undefined ();
566+ /* We can read from the socket immediately. This is a fast path.
567+ Also see the related comments in Send(). */
568+ if (sync_operations++ < max_sync_operations) {
569+ auto res = Napi::Promise::Deferred::New (Env ());
570+ Receive (res);
571+ poller.Trigger ();
572+ return res.Promise ();
573573 }
574574
575- return poller.ReadPromise (receive_timeout );
575+ SetImmediate ( Env (), [&]() { poller.Trigger (); } );
576576 }
577+
578+ /* Check if we are already polling for reads. Only one promise may receive
579+ the next message, so we must ensure that receive operations are in
580+ sequence. */
581+ if (poller.PollingReadable ()) {
582+ ErrnoException (Env (), EAGAIN).ThrowAsJavaScriptException ();
583+ return Env ().Undefined ();
584+ }
585+
586+ return poller.ReadPromise (receive_timeout);
577587}
578588
579589void Socket::Join (const Napi::CallbackInfo& info) {
@@ -848,11 +858,13 @@ void Socket::Initialize(Module& module, Napi::Object& exports) {
848858}
849859
850860void Socket::Poller::ReadableCallback () {
861+ socket.sync_operations = 0 ;
851862 AsyncScope scope (read_deferred.Env (), socket.async_context );
852863 socket.Receive (read_deferred);
853864}
854865
855866void Socket::Poller::WritableCallback () {
867+ socket.sync_operations = 0 ;
856868 AsyncScope scope (write_deferred.Env (), socket.async_context );
857869 socket.Send (write_deferred, write_value);
858870 write_value.Clear ();
@@ -865,7 +877,7 @@ Napi::Value Socket::Poller::ReadPromise(int64_t timeout) {
865877}
866878
867879Napi::Value Socket::Poller::WritePromise (int64_t timeout, OutgoingMsg::Parts&& value) {
868- write_deferred = Napi::Promise::Deferred (read_deferred .Env ());
880+ write_deferred = Napi::Promise::Deferred (write_deferred .Env ());
869881 write_value = std::move (value);
870882 zmq::Poller<Poller>::PollWritable (timeout);
871883 return write_deferred.Promise ();
0 commit comments