Rewritten pub/sub, v20
This commit is contained in:
+35
-184
@@ -38,18 +38,15 @@ template <bool, bool, typename> struct WebSocket;
|
||||
template <bool SSL, typename USERDATA>
|
||||
struct WebSocketContextData {
|
||||
private:
|
||||
/* Used for prepending unframed messages when using dedicated compressors */
|
||||
struct MessageMetadata {
|
||||
unsigned int length;
|
||||
OpCode opCode;
|
||||
bool compress;
|
||||
/* Undefined init of all members */
|
||||
MessageMetadata() {}
|
||||
MessageMetadata(unsigned int length, OpCode opCode, bool compress)
|
||||
: length(length), opCode(opCode), compress(compress) {}
|
||||
};
|
||||
|
||||
public:
|
||||
/* Type queued up when publishing */
|
||||
struct TopicTreeMessage {
|
||||
std::string message;
|
||||
OpCode opCode;
|
||||
bool compress;
|
||||
};
|
||||
|
||||
/* All WebSocketContextData holds a list to all other WebSocketContextData in this app.
|
||||
* We cannot type it USERDATA since different WebSocketContextData can have different USERDATA. */
|
||||
std::vector<WebSocketContextData<SSL, int> *> adjacentWebSocketContextDatas;
|
||||
@@ -79,7 +76,7 @@ public:
|
||||
std::pair<unsigned short, unsigned short> idleTimeoutComponents;
|
||||
|
||||
/* Each websocket context has a topic tree for pub/sub */
|
||||
TopicTree topicTree;
|
||||
TopicTree<TopicTreeMessage> topicTree;
|
||||
|
||||
/* This is run once on start-up */
|
||||
void calculateIdleTimeoutCompnents(unsigned short idleTimeout) {
|
||||
@@ -100,120 +97,39 @@ public:
|
||||
Loop::get()->removePreHandler(this);
|
||||
}
|
||||
|
||||
WebSocketContextData() : topicTree([this](Subscriber *s, Intersection &intersection) -> int {
|
||||
WebSocketContextData() : topicTree([](Subscriber *s, TopicTreeMessage &message, auto flags) {
|
||||
/* Subscriber's user is the socket */
|
||||
auto *ws = (WebSocket<SSL, true, USERDATA> *) s->user;
|
||||
|
||||
/* We could potentially be called here even if we have nothing to send, since we can
|
||||
* be the sender of every single message in this intersection. Also "fin" of a segment is not
|
||||
* guaranteed to be set, in case remaining segments are all from us.
|
||||
* Essentially, we cannot make strict assumptions here. Also, we can even come here corked,
|
||||
* since publish can call drain! */
|
||||
|
||||
/* We rely on writing to regular asyncSockets */
|
||||
auto *asyncSocket = (AsyncSocket<SSL> *) s->user;
|
||||
|
||||
/* If we are corked, do not uncork - otherwise if we cork in here, uncork before leaving */
|
||||
bool wasCorked = asyncSocket->isCorked();
|
||||
|
||||
/* Do we even have room for potential data? */
|
||||
if (!maxBackpressure || asyncSocket->getBufferedAmount() < maxBackpressure) {
|
||||
|
||||
/* Roll over all our segments */
|
||||
intersection.forSubscriber(topicTree.getSenderFor(s), [asyncSocket, this](std::pair<std::string_view, std::string_view> data, bool fin) {
|
||||
|
||||
/* We have a segment that is not marked as last ("fin").
|
||||
* Cork if not already so (purely for performance reasons). Does not touch "wasCorked". */
|
||||
if (!fin && !asyncSocket->isCorked() && asyncSocket->canCork()) {
|
||||
asyncSocket->cork();
|
||||
}
|
||||
|
||||
/* Pick uncompressed data track */
|
||||
std::string_view selectedData = data.first;
|
||||
|
||||
/* Are we using compression? Fine, pick the compressed data track */
|
||||
WebSocketData *webSocketData = (WebSocketData *) asyncSocket->getAsyncSocketData();
|
||||
if (webSocketData->compressionStatus != WebSocketData::CompressionStatus::DISABLED) {
|
||||
|
||||
/* This is used for both shared and dedicated paths */
|
||||
selectedData = data.second;
|
||||
|
||||
/* However, dedicated compression has its own path */
|
||||
if (compression != SHARED_COMPRESSOR) {
|
||||
|
||||
WebSocket<SSL, true, int> *ws = (WebSocket<SSL, true, int> *) asyncSocket;
|
||||
|
||||
/* For performance reasons we always cork when in dedicated mode.
|
||||
* Is this really the best? We already kind of cork things in Zlib?
|
||||
* Right, formatting needs a cork buffer, right. Never mind. */
|
||||
if (!ws->isCorked() && ws->canCork()) {
|
||||
asyncSocket->cork();
|
||||
}
|
||||
|
||||
while (selectedData.length()) {
|
||||
/* Interpret the data like so, because this is how we shoved it in */
|
||||
MessageMetadata mm;
|
||||
memcpy((char *) &mm, selectedData.data(), sizeof(MessageMetadata));
|
||||
std::string_view unframedMessage(selectedData.data() + sizeof(MessageMetadata), mm.length);
|
||||
|
||||
/* Skip this message if our backpressure is too high */
|
||||
if (maxBackpressure && ws->getBufferedAmount() > maxBackpressure) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Here we perform the actual compression and framing */
|
||||
ws->send(unframedMessage, mm.opCode, mm.compress);
|
||||
|
||||
/* Advance until empty */
|
||||
selectedData.remove_prefix(sizeof(MessageMetadata) + mm.length);
|
||||
}
|
||||
|
||||
/* Continue to next segment without executing below path */
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Common path for SHARED and DISABLED. It is an invalid assumption that we always are
|
||||
* uncorked here, however the following (invalid) assumption is not critically wrong either way */
|
||||
|
||||
/* Note: this assumes we are not corked, as corking will swallow things and fail later on */
|
||||
auto [written, failed] = asyncSocket->write(selectedData.data(), (int) selectedData.length());
|
||||
/* If we want strict check for success, we can ignore this check if corked and repeat below
|
||||
* when uncorking - however this is too strict as we really care about PROGRESS rather than
|
||||
* ENTIRE SUCCESS - we need minor API changes to support correct checks */
|
||||
if (!failed) {
|
||||
if (this->resetIdleTimeoutOnSend) {
|
||||
auto *webSocketData = (WebSocketData *) us_socket_ext(SSL, (us_socket_t *) asyncSocket);
|
||||
webSocketData->hasTimedOut = false;
|
||||
asyncSocket->timeout(this->idleTimeoutComponents.first);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* We are done sending, for whatever reasons we ended up corked while not starting with "wasCorked",
|
||||
* we here need to uncork to restore the state we were called in */
|
||||
if (!wasCorked && asyncSocket->isCorked()) {
|
||||
/* Regarding timeout for writes; */
|
||||
auto [written, failed] = asyncSocket->uncork();
|
||||
/* Again, this check should be more like DID WE PROGRESS rather than DID WE SUCCEED ENTIRELY */
|
||||
if (!failed) {
|
||||
if (this->resetIdleTimeoutOnSend) {
|
||||
auto *webSocketData = (WebSocketData *) us_socket_ext(SSL, (us_socket_t *) asyncSocket);
|
||||
webSocketData->hasTimedOut = false;
|
||||
asyncSocket->timeout(this->idleTimeoutComponents.first);
|
||||
}
|
||||
/* If this is the first message we try and cork */
|
||||
bool needsUncork = false;
|
||||
if (flags & TopicTree<TopicTreeMessage>::IteratorFlags::FIRST) {
|
||||
if (ws->canCork() && !ws->isCorked()) {
|
||||
((AsyncSocket<SSL> *)ws)->cork();
|
||||
needsUncork = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Defer a close if we now have (or already had) too much backpressure, or simply skip */
|
||||
if (maxBackpressure && closeOnBackpressureLimit && asyncSocket->getBufferedAmount() > maxBackpressure) {
|
||||
/* We must not immediately close the socket, as that could result in stack overflow,
|
||||
* iterator invalidation and other TopicTree::drain bugs. We may shutdown the reading side of the socket,
|
||||
* causing next iteration to error-close the socket from that context instead, if we want to */
|
||||
us_socket_shutdown_read(SSL, (us_socket_t *) asyncSocket);
|
||||
/* If we ever overstep maxBackpresure, exit immediately */
|
||||
if (WebSocket<SSL, true, USERDATA>::SendStatus::DROPPED == ws->send(message.message, message.opCode, message.compress)) {
|
||||
|
||||
if (needsUncork) {
|
||||
((AsyncSocket<SSL> *)ws)->uncork();
|
||||
}
|
||||
/* Stop draining */
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Reserved, unused */
|
||||
return 0;
|
||||
/* If this is the last message we uncork if we are corked */
|
||||
if (flags & TopicTree<TopicTreeMessage>::IteratorFlags::LAST) {
|
||||
/* We should not uncork in all cases? */
|
||||
if (needsUncork) {
|
||||
((AsyncSocket<SSL> *)ws)->uncork();
|
||||
}
|
||||
}
|
||||
|
||||
/* Success */
|
||||
return false;
|
||||
}) {
|
||||
/* We empty for both pre and post just to make sure */
|
||||
Loop::get()->addPostHandler(this, [this](Loop */*loop*/) {
|
||||
@@ -226,71 +142,6 @@ public:
|
||||
topicTree.drain();
|
||||
});
|
||||
}
|
||||
|
||||
/* Helper for topictree publish, common path from app and ws */
|
||||
bool publish(std::string_view topic, std::string_view message, OpCode opCode, bool compress, Subscriber *sender = nullptr) {
|
||||
bool didMatch = false;
|
||||
|
||||
/* We frame the message right here and only pass raw bytes to the pub/subber */
|
||||
char *dst = (char *) malloc(protocol::messageFrameSize(message.size()));
|
||||
size_t dst_length = protocol::formatMessage<true>(dst, message.data(), message.length(), opCode, message.length(), false);
|
||||
|
||||
/* If compression is disabled */
|
||||
if (compression == DISABLED) {
|
||||
/* Leave second field empty as nobody will ever read it */
|
||||
didMatch |= topicTree.publish(topic, {std::string_view(dst, dst_length), {}}, sender);
|
||||
} else {
|
||||
/* DEDICATED_COMPRESSOR always takes the same path as must always have MessageMetadata as head */
|
||||
if (compress || compression != SHARED_COMPRESSOR) {
|
||||
/* Shared compression mode publishes compressed, framed data */
|
||||
if (compression == SHARED_COMPRESSOR) {
|
||||
/* Loop data holds shared compressor */
|
||||
LoopData *loopData = (LoopData *) us_loop_ext((us_loop_t *) Loop::get());
|
||||
|
||||
/* Compress it */
|
||||
std::string_view compressedMessage = loopData->deflationStream->deflate(loopData->zlibContext, message, true);
|
||||
|
||||
/* Frame it */
|
||||
char *dst_compressed = (char *) malloc(protocol::messageFrameSize(compressedMessage.size()));
|
||||
size_t dst_compressed_length = protocol::formatMessage<true>(dst_compressed, compressedMessage.data(), compressedMessage.length(), opCode, compressedMessage.length(), true);
|
||||
|
||||
/* Always publish the shortest one in any case */
|
||||
didMatch |= topicTree.publish(topic, {std::string_view(dst, dst_length), dst_compressed_length >= dst_length ? std::string_view(dst, dst_length) : std::string_view(dst_compressed, dst_compressed_length)}, sender);
|
||||
|
||||
/* We don't care for allocation here */
|
||||
::free(dst_compressed);
|
||||
} else {
|
||||
/* Dedicated compression mode publishes metadata + unframed uncompressed data */
|
||||
char *dst_compressed = (char *) malloc(message.length() + sizeof(MessageMetadata));
|
||||
|
||||
MessageMetadata mm(
|
||||
(unsigned int) message.length(),
|
||||
opCode,
|
||||
compress
|
||||
);
|
||||
|
||||
memcpy(dst_compressed, (char *) &mm, sizeof(MessageMetadata));
|
||||
memcpy(dst_compressed + sizeof(MessageMetadata), message.data(), message.length());
|
||||
|
||||
/* Interpretation of compressed data depends on what compressor we use */
|
||||
didMatch |= topicTree.publish(topic, {
|
||||
std::string_view(dst, dst_length),
|
||||
std::string_view(dst_compressed, message.length() + sizeof(MessageMetadata))
|
||||
}, sender);
|
||||
|
||||
::free(dst_compressed);
|
||||
}
|
||||
} else {
|
||||
/* If not compressing, put same message on both tracks (only valid for SHARED_COMPRESSOR).
|
||||
* DEDICATED_COMPRESSOR_xKB must never end up here as we don't put a proper head here. */
|
||||
didMatch |= topicTree.publish(topic, {std::string_view(dst, dst_length), std::string_view(dst, dst_length)}, sender);
|
||||
}
|
||||
}
|
||||
|
||||
::free(dst);
|
||||
|
||||
return didMatch;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user