%PDF- %PDF-
| Direktori : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/src/dataqueue/ |
| Current File : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/src/dataqueue/queue.cc |
#include "queue.h"
#include <async_wrap-inl.h>
#include <base_object-inl.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <node.h>
#include <node_bob-inl.h>
#include <node_errors.h>
#include <node_external_reference.h>
#include <node_file-inl.h>
#include <stream_base-inl.h>
#include <util-inl.h>
#include <uv.h>
#include <v8.h>
#include <algorithm>
#include <deque>
#include <initializer_list>
#include <memory>
#include <vector>
namespace node {
using v8::ArrayBufferView;
using v8::BackingStore;
using v8::Local;
using v8::Object;
using v8::Value;
namespace {
// ============================================================================
class IdempotentDataQueueReader;
class NonIdempotentDataQueueReader;
class EntryImpl : public DataQueue::Entry {
public:
virtual std::shared_ptr<DataQueue::Reader> get_reader() = 0;
};
class DataQueueImpl final : public DataQueue,
public std::enable_shared_from_this<DataQueueImpl> {
public:
// Constructor for an idempotent, fixed sized DataQueue.
DataQueueImpl(std::vector<std::unique_ptr<Entry>>&& list, uint64_t size)
: entries_(std::move(list)),
idempotent_(true),
size_(size),
capped_size_(0) {}
// Constructor for a non-idempotent DataQueue. This kind of queue can have
// entries added to it over time. The size is set to 0 initially. The queue
// can be capped immediately on creation. Depending on the entries that are
// added, the size can be cleared if any of the entries are not capable of
// providing a size.
DataQueueImpl(std::optional<uint64_t> cap = std::nullopt)
: idempotent_(false), size_(0), capped_size_(cap) {}
// Disallow moving and copying.
DataQueueImpl(const DataQueueImpl&) = delete;
DataQueueImpl(DataQueueImpl&&) = delete;
DataQueueImpl& operator=(const DataQueueImpl&) = delete;
DataQueueImpl& operator=(DataQueueImpl&&) = delete;
std::shared_ptr<DataQueue> slice(
uint64_t start,
std::optional<uint64_t> maybeEnd = std::nullopt) override {
// If the data queue is not idempotent, or the size cannot be determined,
// we cannot reasonably create a slice. Therefore, return nothing.
if (!idempotent_ || !size_.has_value()) return nullptr;
uint64_t size = size_.value();
// start cannot be greater than the size.
start = std::min(start, size);
uint64_t end = std::max(start, std::min(maybeEnd.value_or(size), size));
DCHECK_LE(start, end);
uint64_t len = end - start;
uint64_t remaining = end - start;
std::vector<std::unique_ptr<Entry>> slices;
if (remaining > 0) {
for (const auto& entry : entries_) {
// The size of every entry should be known since this is an
// idempotent queue.
uint64_t entrySize = entry->size().value();
if (start > entrySize) {
start -= entrySize;
continue;
}
uint64_t chunkStart = start;
uint64_t len = std::min(remaining, entrySize - chunkStart);
slices.emplace_back(entry->slice(chunkStart, chunkStart + len));
remaining -= len;
start = 0;
if (remaining == 0) break;
}
}
return std::make_shared<DataQueueImpl>(std::move(slices), len);
}
std::optional<uint64_t> size() const override { return size_; }
bool is_idempotent() const override { return idempotent_; }
bool is_capped() const override { return capped_size_.has_value(); }
std::optional<bool> append(std::unique_ptr<Entry> entry) override {
if (idempotent_) return std::nullopt;
if (!entry) return false;
// If this entry successfully provides a size, we can add it to our size_
// if that has a value, otherwise, we keep uint64_t empty.
if (entry->size().has_value() && size_.has_value()) {
uint64_t entrySize = entry->size().value();
uint64_t size = size_.value();
// If capped_size_ is set, size + entrySize cannot exceed capped_size_
// or the entry cannot be added.
if (capped_size_.has_value() &&
(capped_size_.value() < entrySize + size)) {
return false;
}
size_ = size + entrySize;
} else {
// This entry cannot provide a size. We can still add it but we have to
// clear the known size.
size_ = std::nullopt;
}
entries_.push_back(std::move(entry));
return true;
}
void cap(uint64_t limit = 0) override {
if (is_idempotent()) return;
// If the data queue is already capped, it is possible to call
// cap again with a smaller size.
if (capped_size_.has_value()) {
capped_size_ = std::min(limit, capped_size_.value());
return;
}
// Otherwise just set the limit.
capped_size_ = limit;
}
std::optional<uint64_t> maybeCapRemaining() const override {
if (capped_size_.has_value() && size_.has_value()) {
uint64_t capped_size = capped_size_.value();
uint64_t size = size_.value();
return capped_size > size ? capped_size - size : 0UL;
}
return std::nullopt;
}
void MemoryInfo(node::MemoryTracker* tracker) const override {
tracker->TrackField(
"entries", entries_, "std::vector<std::unique_ptr<Entry>>");
}
void addBackpressureListener(BackpressureListener* listener) override {
if (idempotent_) return;
DCHECK_NOT_NULL(listener);
backpressure_listeners_.insert(listener);
}
void removeBackpressureListener(BackpressureListener* listener) override {
if (idempotent_) return;
DCHECK_NOT_NULL(listener);
backpressure_listeners_.erase(listener);
}
void NotifyBackpressure(size_t amount) {
if (idempotent_) return;
for (auto& listener : backpressure_listeners_) listener->EntryRead(amount);
}
bool HasBackpressureListeners() const noexcept {
return !backpressure_listeners_.empty();
}
std::shared_ptr<Reader> get_reader() override;
SET_MEMORY_INFO_NAME(DataQueue)
SET_SELF_SIZE(DataQueueImpl)
private:
std::vector<std::unique_ptr<Entry>> entries_;
bool idempotent_;
std::optional<uint64_t> size_ = std::nullopt;
std::optional<uint64_t> capped_size_ = std::nullopt;
bool locked_to_reader_ = false;
std::unordered_set<BackpressureListener*> backpressure_listeners_;
friend class DataQueue;
friend class IdempotentDataQueueReader;
friend class NonIdempotentDataQueueReader;
};
// An IdempotentDataQueueReader always reads the entire content of the
// DataQueue with which it is associated, and always from the beginning.
// Reads are non-destructive, meaning that the state of the DataQueue
// will not and cannot be changed.
class IdempotentDataQueueReader final
: public DataQueue::Reader,
public std::enable_shared_from_this<IdempotentDataQueueReader> {
public:
IdempotentDataQueueReader(std::shared_ptr<DataQueueImpl> data_queue)
: data_queue_(std::move(data_queue)) {
CHECK(data_queue_->is_idempotent());
}
// Disallow moving and copying.
IdempotentDataQueueReader(const IdempotentDataQueueReader&) = delete;
IdempotentDataQueueReader(IdempotentDataQueueReader&&) = delete;
IdempotentDataQueueReader& operator=(const IdempotentDataQueueReader&) =
delete;
IdempotentDataQueueReader& operator=(IdempotentDataQueueReader&&) = delete;
int Pull(Next next,
int options,
DataQueue::Vec* data,
size_t count,
size_t max_count_hint = bob::kMaxCountHint) override {
std::shared_ptr<DataQueue::Reader> self = shared_from_this();
// If ended is true, this reader has already reached the end and cannot
// provide any more data.
if (ended_) {
std::move(next)(bob::Status::STATUS_EOS, nullptr, 0, [](uint64_t) {});
return bob::Status::STATUS_EOS;
}
// If this is the first pull from this reader, we are first going to
// check to see if there is anything at all to actually do.
if (!current_index_.has_value()) {
// First, let's check the number of entries. If there are no entries,
// we've reached the end and have nothing to do.
// Because this is an idempotent dataqueue, we should always know the
// size...
if (data_queue_->entries_.empty()) {
ended_ = true;
std::move(next)(bob::Status::STATUS_EOS, nullptr, 0, [](uint64_t) {});
return bob::Status::STATUS_EOS;
}
current_index_ = 0;
}
// We have current_index_, awesome, we are going to keep reading from
// it until we receive and end.
auto current_reader = getCurrentReader();
if (current_reader == nullptr) {
// Getting the current reader for an entry could fail for several
// reasons. For an FdEntry, for instance, getting the reader may
// fail if the file has been modified since the FdEntry was created.
// We handle the case simply by erroring.
std::move(next)(UV_EINVAL, nullptr, 0, [](uint64_t) {});
return UV_EINVAL;
}
CHECK(!pull_pending_);
pull_pending_ = true;
int status = current_reader->Pull(
[this, next = std::move(next)](
int status, const DataQueue::Vec* vecs, uint64_t count, Done done) {
pull_pending_ = false;
// In each of these cases, we do not expect that the source will
// actually have provided any actual data.
CHECK_IMPLIES(status == bob::Status::STATUS_BLOCK ||
status == bob::Status::STATUS_WAIT ||
status == bob::Status::STATUS_EOS,
vecs == nullptr && count == 0);
if (status == bob::Status::STATUS_EOS) {
uint32_t current = current_index_.value() + 1;
current_reader_ = nullptr;
// We have reached the end of this entry. If this is the last entry,
// then we are done. Otherwise, we advance the current_index_, clear
// the current_reader_ and wait for the next read.
if (current == data_queue_->entries_.size()) {
// Yes, this was the final entry. We're all done.
ended_ = true;
} else {
// This was not the final entry, so we update the index and
// continue on by performing another read.
current_index_ = current;
status = bob::STATUS_CONTINUE;
}
std::move(next)(status, nullptr, 0, [](uint64_t) {});
return;
}
std::move(next)(status, vecs, count, std::move(done));
},
options,
data,
count,
max_count_hint);
// The pull was handled synchronously. If we're not ended, we want to
// make sure status returned is CONTINUE.
if (!pull_pending_) {
if (!ended_) return bob::Status::STATUS_CONTINUE;
// For all other status, we just fall through and return it straightaway.
}
// The other statuses that can be returned by the pull are:
// bob::Status::STATUS_CONTINUE - means that the entry has more data
// to pull.
// bob::Status::STATUS_BLOCK - means that the entry has more data to
// pull but it is not available yet. The
// caller should not keep calling pull for
// now but may check again later.
// bob::Status::STATUS_WAIT - means that the entry has more data to
// pull but it won't be provided
// synchronously, instead the next() callback
// will be called when the data is available.
//
// For any of these statuses, we want to keep the current index and
// current_reader_ set for the next pull.
return status;
}
DataQueue::Reader* getCurrentReader() {
CHECK(!ended_);
CHECK(current_index_.has_value());
if (current_reader_ == nullptr) {
auto& entry = data_queue_->entries_[current_index_.value()];
// Because this is an idempotent reader, let's just be sure to
// doublecheck that the entry itself is actually idempotent
DCHECK(entry->is_idempotent());
current_reader_ = static_cast<EntryImpl&>(*entry).get_reader();
}
return current_reader_.get();
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(IdempotentDataQueueReader)
SET_SELF_SIZE(IdempotentDataQueueReader)
private:
std::shared_ptr<DataQueueImpl> data_queue_;
std::optional<uint32_t> current_index_ = std::nullopt;
std::shared_ptr<DataQueue::Reader> current_reader_ = nullptr;
bool ended_ = false;
bool pull_pending_ = false;
};
// A NonIdempotentDataQueueReader reads entries from the DataEnqueue
// and removes those entries from the queue as they are fully consumed.
// This means that reads are destructive and the state of the DataQueue
// is mutated as the read proceeds.
class NonIdempotentDataQueueReader final
: public DataQueue::Reader,
public std::enable_shared_from_this<NonIdempotentDataQueueReader> {
public:
NonIdempotentDataQueueReader(std::shared_ptr<DataQueueImpl> data_queue)
: data_queue_(std::move(data_queue)) {
CHECK(!data_queue_->is_idempotent());
}
// Disallow moving and copying.
NonIdempotentDataQueueReader(const NonIdempotentDataQueueReader&) = delete;
NonIdempotentDataQueueReader(NonIdempotentDataQueueReader&&) = delete;
NonIdempotentDataQueueReader& operator=(const NonIdempotentDataQueueReader&) =
delete;
NonIdempotentDataQueueReader& operator=(NonIdempotentDataQueueReader&&) =
delete;
int Pull(Next next,
int options,
DataQueue::Vec* data,
size_t count,
size_t max_count_hint = bob::kMaxCountHint) override {
std::shared_ptr<DataQueue::Reader> self = shared_from_this();
// If ended is true, this reader has already reached the end and cannot
// provide any more data.
if (ended_) {
std::move(next)(bob::Status::STATUS_EOS, nullptr, 0, [](uint64_t) {});
return bob::Status::STATUS_EOS;
}
// If the collection of entries is empty, there's nothing currently left to
// read. How we respond depends on whether the data queue has been capped
// or not.
if (data_queue_->entries_.empty()) {
// If the data_queue_ is empty, and not capped, then we can reasonably
// expect more data to be provided later, but we don't know exactly when
// that'll happe, so the proper response here is to return a blocked
// status.
if (!data_queue_->is_capped()) {
std::move(next)(bob::Status::STATUS_BLOCK, nullptr, 0, [](uint64_t) {});
return bob::STATUS_BLOCK;
}
// However, if we are capped, the status will depend on whether the size
// of the data_queue_ is known or not.
if (data_queue_->size().has_value()) {
// If the size is known, and it is still less than the cap, then we
// still might get more data. We just don't know exactly when that'll
// come, so let's return a blocked status.
if (data_queue_->size().value() < data_queue_->capped_size_.value()) {
std::move(next)(
bob::Status::STATUS_BLOCK, nullptr, 0, [](uint64_t) {});
return bob::STATUS_BLOCK;
}
// Otherwise, if size is equal to or greater than capped, we are done.
// Fall through to allow the end handling to run.
}
// If the size is not known, and the data queue is capped, no additional
// entries are going to be added to the queue. Since we are all out of
// entries, we're done. There's nothing left to read.
current_reader_ = nullptr;
ended_ = true;
std::move(next)(bob::Status::STATUS_EOS, nullptr, 0, [](uint64_t) {});
return bob::STATUS_EOS;
}
auto current_reader = getCurrentReader();
if (current_reader == nullptr) {
std::move(next)(UV_EINVAL, nullptr, 0, [](uint64_t) {});
return UV_EINVAL;
}
// If we got here, we have an entry to read from.
CHECK(!pull_pending_);
pull_pending_ = true;
int status = current_reader->Pull(
[this, next = std::move(next)](
int status, const DataQueue::Vec* vecs, uint64_t count, Done done) {
pull_pending_ = false;
// In each of these cases, we do not expect that the source will
// actually have provided any actual data.
CHECK_IMPLIES(status == bob::Status::STATUS_BLOCK ||
status == bob::Status::STATUS_WAIT ||
status == bob::Status::STATUS_EOS,
vecs == nullptr && count == 0);
if (status == bob::Status::STATUS_EOS) {
data_queue_->entries_.erase(data_queue_->entries_.begin());
ended_ = data_queue_->entries_.empty();
current_reader_ = nullptr;
if (!ended_) status = bob::Status::STATUS_CONTINUE;
std::move(next)(status, nullptr, 0, [](uint64_t) {});
return;
}
// If there is a backpressure listener, lets report on how much data
// was actually read.
if (data_queue_->HasBackpressureListeners()) {
// How much did we actually read?
size_t read = 0;
for (uint64_t n = 0; n < count; n++) {
read += vecs[n].len;
}
data_queue_->NotifyBackpressure(read);
}
// Now that we have updated this readers state, we can forward
// everything on to the outer next.
std::move(next)(status, vecs, count, std::move(done));
},
options,
data,
count,
max_count_hint);
if (!pull_pending_) {
// The callback was resolved synchronously. Let's check our status.
if (!ended_) return bob::Status::STATUS_CONTINUE;
// For all other status, we just fall through and return it straightaway.
}
// The other statuses that can be returned by the pull are:
// bob::Status::STATUS_CONTINUE - means that the entry has more data
// to pull.
// bob::Status::STATUS_BLOCK - means that the entry has more data to
// pull but it is not available yet. The
// caller should not keep calling pull for
// now but may check again later.
// bob::Status::STATUS_WAIT - means that the entry has more data to
// pull but it won't be provided
// synchronously, instead the next() callback
// will be called when the data is available.
//
// For any of these statuses, we want to keep the current index and
// current_reader_ set for the next pull.
return status;
}
DataQueue::Reader* getCurrentReader() {
CHECK(!ended_);
CHECK(!data_queue_->entries_.empty());
if (current_reader_ == nullptr) {
auto& entry = data_queue_->entries_.front();
current_reader_ = static_cast<EntryImpl&>(*entry).get_reader();
}
return current_reader_.get();
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(NonIdempotentDataQueueReader)
SET_SELF_SIZE(NonIdempotentDataQueueReader)
private:
std::shared_ptr<DataQueueImpl> data_queue_;
std::shared_ptr<DataQueue::Reader> current_reader_ = nullptr;
bool ended_ = false;
bool pull_pending_ = false;
};
std::shared_ptr<DataQueue::Reader> DataQueueImpl::get_reader() {
if (is_idempotent()) {
return std::make_shared<IdempotentDataQueueReader>(shared_from_this());
}
if (locked_to_reader_) return nullptr;
locked_to_reader_ = true;
return std::make_shared<NonIdempotentDataQueueReader>(shared_from_this());
}
// ============================================================================
// An empty, always idempotent entry.
class EmptyEntry final : public EntryImpl {
public:
class EmptyReader final : public DataQueue::Reader,
public std::enable_shared_from_this<EmptyReader> {
public:
int Pull(Next next,
int options,
DataQueue::Vec* data,
size_t count,
size_t max_count_hint = bob::kMaxCountHint) override {
auto self = shared_from_this();
if (ended_) {
std::move(next)(bob::Status::STATUS_EOS, nullptr, 0, [](uint64_t) {});
return bob::Status::STATUS_EOS;
}
ended_ = true;
std::move(next)(
bob::Status::STATUS_CONTINUE, nullptr, 0, [](uint64_t) {});
return bob::Status::STATUS_CONTINUE;
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(EmptyReader)
SET_SELF_SIZE(EmptyReader)
private:
bool ended_ = false;
};
EmptyEntry() = default;
// Disallow moving and copying.
EmptyEntry(const EmptyEntry&) = delete;
EmptyEntry(EmptyEntry&&) = delete;
EmptyEntry& operator=(const EmptyEntry&) = delete;
EmptyEntry& operator=(EmptyEntry&&) = delete;
std::shared_ptr<DataQueue::Reader> get_reader() override {
return std::make_shared<EmptyReader>();
}
std::unique_ptr<Entry> slice(
uint64_t start,
std::optional<uint64_t> maybeEnd = std::nullopt) override {
if (start != 0) return nullptr;
return std::make_unique<EmptyEntry>();
}
std::optional<uint64_t> size() const override { return 0; }
bool is_idempotent() const override { return true; }
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(EmptyEntry)
SET_SELF_SIZE(EmptyEntry)
};
// ============================================================================
// An entry that consists of a single memory resident v8::BackingStore.
// These are always idempotent and always a fixed, known size.
class InMemoryEntry final : public EntryImpl {
public:
struct InMemoryFunctor final {
std::shared_ptr<BackingStore> backing_store;
void operator()(uint64_t) { backing_store = nullptr; }
};
class InMemoryReader final
: public DataQueue::Reader,
public std::enable_shared_from_this<InMemoryReader> {
public:
InMemoryReader(InMemoryEntry& entry) : entry_(entry) {}
int Pull(Next next,
int options,
DataQueue::Vec* data,
size_t count,
size_t max_count_hint = bob::kMaxCountHint) override {
auto self = shared_from_this();
if (ended_) {
std::move(next)(bob::Status::STATUS_EOS, nullptr, 0, [](uint64_t) {});
return bob::Status::STATUS_EOS;
}
ended_ = true;
DataQueue::Vec vec{
reinterpret_cast<uint8_t*>(entry_.backing_store_->Data()) +
entry_.offset_,
entry_.byte_length_,
};
std::move(next)(bob::Status::STATUS_CONTINUE,
&vec,
1,
InMemoryFunctor({entry_.backing_store_}));
return bob::Status::STATUS_CONTINUE;
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(InMemoryReader)
SET_SELF_SIZE(InMemoryReader)
private:
InMemoryEntry& entry_;
bool ended_ = false;
};
InMemoryEntry(std::shared_ptr<BackingStore> backing_store,
uint64_t offset,
uint64_t byte_length)
: backing_store_(std::move(backing_store)),
offset_(offset),
byte_length_(byte_length) {
// The offset_ + byte_length_ cannot extend beyond the size of the
// backing store, because that would just be silly.
CHECK_LE(offset_ + byte_length_, backing_store_->ByteLength());
}
// Disallow moving and copying.
InMemoryEntry(const InMemoryEntry&) = delete;
InMemoryEntry(InMemoryEntry&&) = delete;
InMemoryEntry& operator=(const InMemoryEntry&) = delete;
InMemoryEntry& operator=(InMemoryEntry&&) = delete;
std::shared_ptr<DataQueue::Reader> get_reader() override {
return std::make_shared<InMemoryReader>(*this);
}
std::unique_ptr<Entry> slice(
uint64_t start,
std::optional<uint64_t> maybeEnd = std::nullopt) override {
const auto makeEntry = [&](uint64_t start,
uint64_t len) -> std::unique_ptr<Entry> {
if (len == 0) {
return std::make_unique<EmptyEntry>();
}
return std::make_unique<InMemoryEntry>(backing_store_, start, len);
};
start += offset_;
// The start cannot extend beyond the maximum end point of this entry.
start = std::min(start, offset_ + byte_length_);
if (maybeEnd.has_value()) {
uint64_t end = maybeEnd.value();
// The end cannot extend beyond the maximum end point of this entry,
// and the end must be equal to or greater than the start.
end = std::max(start, std::min(offset_ + end, offset_ + byte_length_));
return makeEntry(start, end - start);
}
// If no end is given, then the new length is the current length
// minus the adjusted start.
return makeEntry(start, byte_length_ - start);
}
std::optional<uint64_t> size() const override { return byte_length_; }
bool is_idempotent() const override { return true; }
void MemoryInfo(node::MemoryTracker* tracker) const override {
tracker->TrackField(
"store", backing_store_, "std::shared_ptr<v8::BackingStore>");
}
SET_MEMORY_INFO_NAME(InMemoryEntry)
SET_SELF_SIZE(InMemoryEntry)
private:
std::shared_ptr<BackingStore> backing_store_;
uint64_t offset_;
uint64_t byte_length_;
friend class InMemoryReader;
};
// ============================================================================
// An entry that wraps a DataQueue. The entry takes on the characteristics
// of the wrapped dataqueue.
class DataQueueEntry : public EntryImpl {
public:
explicit DataQueueEntry(std::shared_ptr<DataQueue> data_queue)
: data_queue_(std::move(data_queue)) {
CHECK(data_queue_);
}
// Disallow moving and copying.
DataQueueEntry(const DataQueueEntry&) = delete;
DataQueueEntry(DataQueueEntry&&) = delete;
DataQueueEntry& operator=(const DataQueueEntry&) = delete;
DataQueueEntry& operator=(DataQueueEntry&&) = delete;
std::shared_ptr<DataQueue::Reader> get_reader() override {
return std::make_shared<ReaderImpl>(data_queue_->get_reader());
}
std::unique_ptr<Entry> slice(
uint64_t start, std::optional<uint64_t> end = std::nullopt) override {
std::shared_ptr<DataQueue> sliced = data_queue_->slice(start, end);
if (!sliced) return nullptr;
return std::make_unique<DataQueueEntry>(std::move(sliced));
}
// Returns the number of bytes represented by this Entry if it is
// known. Certain types of entries, such as those backed by streams
// might not know the size in advance and therefore cannot provide
// a value. In such cases, size() must return std::nullopt.
//
// If the entry is idempotent, a size should always be available.
std::optional<uint64_t> size() const override { return data_queue_->size(); }
// When true, multiple reads on the object must produce the exact
// same data or the reads will fail. Some sources of entry data,
// such as streams, may not be capable of preserving idempotency
// and therefore must not claim to be. If an entry claims to be
// idempotent and cannot preserve that quality, subsequent reads
// must fail with an error when a variance is detected.
bool is_idempotent() const override { return data_queue_->is_idempotent(); }
void MemoryInfo(node::MemoryTracker* tracker) const override {
tracker->TrackField(
"data_queue", data_queue_, "std::shared_ptr<DataQueue>");
}
DataQueue& getDataQueue() { return *data_queue_; }
SET_MEMORY_INFO_NAME(DataQueueEntry)
SET_SELF_SIZE(DataQueueEntry)
private:
std::shared_ptr<DataQueue> data_queue_;
class ReaderImpl : public DataQueue::Reader,
public std::enable_shared_from_this<ReaderImpl> {
public:
explicit ReaderImpl(std::shared_ptr<DataQueue::Reader> inner)
: inner_(std::move(inner)) {}
int Pull(DataQueue::Reader::Next next,
int options,
DataQueue::Vec* data,
size_t count,
size_t max_count_hint) override {
auto self = shared_from_this();
return inner_->Pull(
std::move(next), options, data, count, max_count_hint);
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(ReaderImpl)
SET_SELF_SIZE(ReaderImpl)
private:
std::shared_ptr<DataQueue::Reader> inner_;
};
};
// ============================================================================
// An FdEntry reads from a file descriptor. A check is made before each read
// to determine if the fd has changed on disc. This is a best-effort check
// that only looks at file size, creation, and modification times. The stat
// check is also async, so there's a natural race condition there where the
// file could be modified between the stat and actual read calls. That's
// a tolerable risk here. While FdEntry is considered idempotent, this race
// means that it is indeed possible for multiple reads to return different
// results if the file just happens to get modified.
class FdEntry final : public EntryImpl {
// TODO(@jasnell, @flakey5):
// * This should only allow reading from regular files. No directories, no
// pipes, etc.
// * The reader should support accepting the buffer(s) from the pull, if any.
// It should
// only allocate a managed buffer if the pull doesn't provide any.
// * We might want to consider making the stat on each read sync to eliminate
// the race
// condition described in the comment above.
public:
static std::unique_ptr<FdEntry> Create(Environment* env, Local<Value> path) {
// We're only going to create the FdEntry if the file exists.
uv_fs_t req = uv_fs_t();
auto cleanup = OnScopeLeave([&] { uv_fs_req_cleanup(&req); });
auto buf = std::make_shared<BufferValue>(env->isolate(), path);
if (uv_fs_stat(nullptr, &req, buf->out(), nullptr) < 0) return nullptr;
return std::make_unique<FdEntry>(
env, std::move(buf), req.statbuf, 0, req.statbuf.st_size);
}
FdEntry(Environment* env,
std::shared_ptr<BufferValue> path_,
uv_stat_t stat,
uint64_t start,
uint64_t end)
: env_(env),
path_(std::move(path_)),
stat_(stat),
start_(start),
end_(end) {}
std::shared_ptr<DataQueue::Reader> get_reader() override {
return ReaderImpl::Create(this);
}
std::unique_ptr<Entry> slice(
uint64_t start, std::optional<uint64_t> end = std::nullopt) override {
uint64_t new_start = start_ + start;
uint64_t new_end = end_;
if (end.has_value()) {
new_end = std::min(end.value(), end_);
}
CHECK(new_start >= start_);
CHECK(new_end <= end_);
return std::make_unique<FdEntry>(env_, path_, stat_, new_start, new_end);
}
std::optional<uint64_t> size() const override { return end_ - start_; }
bool is_idempotent() const override { return true; }
Environment* env() const { return env_; }
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(FdEntry)
SET_SELF_SIZE(FdEntry)
private:
Environment* env_;
std::shared_ptr<BufferValue> path_;
uv_stat_t stat_;
uint64_t start_ = 0;
uint64_t end_ = 0;
bool is_modified(const uv_stat_t& other) {
return other.st_size != stat_.st_size ||
other.st_mtim.tv_nsec != stat_.st_mtim.tv_nsec;
}
static bool CheckModified(FdEntry* entry, int fd) {
uv_fs_t req = uv_fs_t();
auto cleanup = OnScopeLeave([&] { uv_fs_req_cleanup(&req); });
// TODO(jasnell): Note the use of a sync fs call here is a bit unfortunate.
// Doing this asynchronously creates a bit of a race condition tho, a file
// could be unmodified when we call the operation but then by the time the
// async callback is triggered to give us that answer the file is modified.
// While such silliness is still possible here, the sync call at least makes
// it less likely to hit the race.
if (uv_fs_fstat(nullptr, &req, fd, nullptr) < 0) return true;
return entry->is_modified(req.statbuf);
}
class ReaderImpl final : public DataQueue::Reader,
public StreamListener,
public std::enable_shared_from_this<ReaderImpl> {
public:
static std::shared_ptr<ReaderImpl> Create(FdEntry* entry) {
uv_fs_t req;
auto cleanup = OnScopeLeave([&] { uv_fs_req_cleanup(&req); });
int file =
uv_fs_open(nullptr, &req, entry->path_->out(), O_RDONLY, 0, nullptr);
if (file < 0 || FdEntry::CheckModified(entry, file)) {
uv_fs_close(nullptr, &req, file, nullptr);
return nullptr;
}
Realm* realm = entry->env()->principal_realm();
return std::make_shared<ReaderImpl>(
BaseObjectPtr<fs::FileHandle>(
fs::FileHandle::New(realm->GetBindingData<fs::BindingData>(),
file,
Local<Object>(),
entry->start_,
entry->end_ - entry->start_)),
entry);
}
explicit ReaderImpl(BaseObjectPtr<fs::FileHandle> handle, FdEntry* entry)
: env_(handle->env()), handle_(std::move(handle)), entry_(entry) {
handle_->PushStreamListener(this);
handle_->env()->AddCleanupHook(cleanup, this);
}
~ReaderImpl() override {
handle_->env()->RemoveCleanupHook(cleanup, this);
DrainAndClose();
handle_->RemoveStreamListener(this);
}
uv_buf_t OnStreamAlloc(size_t suggested_size) override {
return env_->allocate_managed_buffer(suggested_size);
}
void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override {
std::shared_ptr<v8::BackingStore> store =
env_->release_managed_buffer(buf);
if (ended_) {
// If we got here and ended_ is true, it means we ended and drained
// while the read was pending. We're just going to do nothing.
CHECK(pending_pulls_.empty());
return;
}
CHECK(reading_);
auto pending = DequeuePendingPull();
if (CheckModified(entry_, handle_->GetFD())) {
DrainAndClose();
// The file was modified while the read was pending. We need to error.
std::move(pending.next)(UV_EINVAL, nullptr, 0, [](uint64_t) {});
return;
}
if (nread < 0) {
if (nread == UV_EOF) {
std::move(pending.next)(bob::STATUS_EOS, nullptr, 0, [](uint64_t) {});
} else {
std::move(pending.next)(nread, nullptr, 0, [](uint64_t) {});
}
return DrainAndClose();
}
DataQueue::Vec vec;
vec.base = static_cast<uint8_t*>(store->Data());
vec.len = static_cast<uint64_t>(nread);
std::move(pending.next)(
bob::STATUS_CONTINUE, &vec, 1, [store](uint64_t) {});
if (pending_pulls_.empty()) {
reading_ = false;
if (handle_->IsAlive()) handle_->ReadStop();
}
}
int Pull(Next next,
int options,
DataQueue::Vec* data,
size_t count,
size_t max_count_hint = bob::kMaxCountHint) override {
if (ended_ || !handle_->IsAlive()) {
std::move(next)(bob::STATUS_EOS, nullptr, 0, [](uint64_t) {});
return bob::STATUS_EOS;
}
if (FdEntry::CheckModified(entry_, handle_->GetFD())) {
DrainAndClose();
std::move(next)(UV_EINVAL, nullptr, 0, [](uint64_t) {});
return UV_EINVAL;
}
pending_pulls_.emplace_back(std::move(next), shared_from_this());
if (!reading_) {
reading_ = true;
handle_->ReadStart();
}
return bob::STATUS_WAIT;
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(FdEntry::Reader)
SET_SELF_SIZE(ReaderImpl)
private:
struct PendingPull {
Next next;
std::shared_ptr<ReaderImpl> self;
PendingPull(Next next, std::shared_ptr<ReaderImpl> self)
: next(std::move(next)), self(std::move(self)) {}
};
Environment* env_;
BaseObjectPtr<fs::FileHandle> handle_;
FdEntry* entry_;
std::deque<PendingPull> pending_pulls_;
bool reading_ = false;
bool ended_ = false;
static void cleanup(void* self) {
auto ptr = static_cast<ReaderImpl*>(self);
ptr->DrainAndClose();
}
void DrainAndClose() {
if (ended_) return;
ended_ = true;
while (!pending_pulls_.empty()) {
auto pending = DequeuePendingPull();
std::move(pending.next)(bob::STATUS_EOS, nullptr, 0, [](uint64_t) {});
}
handle_->ReadStop();
// We fallback to a sync close on the raw fd here because it is the
// easiest, simplest thing to do. All of FileHandle's close mechanisms
// assume async close and cleanup, while DrainAndClose might be running
// in the destructor during GC, for instance. As a todo, FileHandle could
// provide a sync mechanism for closing the FD but, for now, this
// approach works.
int fd = handle_->Release();
uv_fs_t req;
uv_fs_close(nullptr, &req, fd, nullptr);
uv_fs_req_cleanup(&req);
}
PendingPull DequeuePendingPull() {
CHECK(!pending_pulls_.empty());
auto pop = OnScopeLeave([this] { pending_pulls_.pop_front(); });
return std::move(pending_pulls_.front());
}
friend class FdEntry;
};
friend class ReaderImpl;
};
// ============================================================================
} // namespace
std::shared_ptr<DataQueue> DataQueue::CreateIdempotent(
std::vector<std::unique_ptr<Entry>> list) {
// Any entry is invalid for an idempotent DataQueue if any of the entries
// are nullptr or is not idempotent.
uint64_t size = 0;
const auto isInvalid = [&size](auto& item) {
if (item == nullptr || !item->is_idempotent()) {
return true; // true means the entry is not valid here.
}
// To keep from having to iterate over the entries
// again, we'll try calculating the size. If any
// of the entries are unable to provide a size, then
// we assume we cannot safely treat this entry as
// idempotent even if it claims to be.
if (item->size().has_value()) {
size += item->size().value();
} else {
return true; // true means the entry is not valid here.
}
return false;
};
if (std::any_of(list.begin(), list.end(), isInvalid)) {
return nullptr;
}
return std::make_shared<DataQueueImpl>(std::move(list), size);
}
std::shared_ptr<DataQueue> DataQueue::Create(std::optional<uint64_t> capped) {
return std::make_shared<DataQueueImpl>(capped);
}
std::unique_ptr<DataQueue::Entry> DataQueue::CreateInMemoryEntryFromView(
Local<ArrayBufferView> view) {
// If the view is not detachable, we do not want to create an InMemoryEntry
// from it. Why? Because if we're not able to detach the backing store from
// the underlying buffer, something else could modify the buffer while we're
// holding the reference, which means we cannot guarantee that reads will be
// idempotent.
if (!view->Buffer()->IsDetachable()) {
return nullptr;
}
auto store = view->Buffer()->GetBackingStore();
auto offset = view->ByteOffset();
auto length = view->ByteLength();
USE(view->Buffer()->Detach(Local<Value>()));
return CreateInMemoryEntryFromBackingStore(std::move(store), offset, length);
}
std::unique_ptr<DataQueue::Entry>
DataQueue::CreateInMemoryEntryFromBackingStore(
std::shared_ptr<BackingStore> store, uint64_t offset, uint64_t length) {
CHECK(store);
if (offset + length > store->ByteLength()) {
return nullptr;
}
return std::make_unique<InMemoryEntry>(std::move(store), offset, length);
}
std::unique_ptr<DataQueue::Entry> DataQueue::CreateDataQueueEntry(
std::shared_ptr<DataQueue> data_queue) {
return std::make_unique<DataQueueEntry>(std::move(data_queue));
}
std::unique_ptr<DataQueue::Entry> DataQueue::CreateFdEntry(Environment* env,
Local<Value> path) {
return FdEntry::Create(env, path);
}
void DataQueue::Initialize(Environment* env, v8::Local<v8::Object> target) {
// Nothing to do here currently.
}
void DataQueue::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
// Nothing to do here currently.
}
} // namespace node