OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / src / overlay_node.h

156 lines · c ref:
#ifndef HYPERSWARM_OVERLAY_NODE_H
#define HYPERSWARM_OVERLAY_NODE_H

// The Godot face of liboverlay: fetch content-addressed assets from peers, and
// serve the ones this instance holds.
//
// WHICH THREAD OWNS WHAT — see DESIGN-transfer-manager.md §2.1, which this file
// got wrong once and now follows.
//
//   UDX thread    owns the Protocol OUTRIGHT. Protomux dispatches inbound frames
//                 here, and udx/protomux handles belong to this loop — so both
//                 halves of the protocol must live here or they race. It is also
//                 what keeps step 6 honest: send() returns writability
//                 synchronously, and that answer only means anything on the
//                 thread that owns the socket.
//   main          Godot's. Issues commands (marshalled via run_on_loop) and
//                 drains an event queue. Signals are emitted ONLY from poll().
//
// There is no worker thread. An earlier version had one owning the Protocol,
// which was unsafe on both counts above. What that costs is stated rather than
// hidden: the two genuinely unbounded operations — building a chunk map for a
// file being served for the first time, and re-verifying a partial whose journal
// is missing — run on the UDX thread. Both are kept off the hot path instead of
// off the thread: serve() precomputes and caches the chunk map when the GAME
// registers an asset, not when a peer asks; and journaled resume makes
// re-verification O(1) in the normal case. Per-chunk work (one blake2b over a
// chunk, one positioned write) is bounded and stays inline.
//
// Moving those two onto a worker is the remaining refinement, and it needs the
// async chunk hand-off §2.1 describes. It is not needed to move bytes.

#include <condition_variable>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>

#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/variant/string.hpp>

#include "hyperswarm_peer.h"
#include "overlay/file_index.h"
#include "overlay/protocol.h"
#include "overlay_transport.h"
#include "udx_transport.h"

namespace godot {

class HyperOverlay : public Node {
	GDCLASS(HyperOverlay, Node)

public:
	HyperOverlay();
	~HyperOverlay();

	// Open the local index and start the worker. `storage_dir` holds the index
	// log, the journals and the content spool; it must survive restarts, because
	// that is what makes a resumed download cheap.
	bool open(const String &storage_dir);
	void close();
	bool is_open() const { return open_; }

	// Attach to a HyperswarmMultiplayerPeer's connections. From here on, every
	// peer that completes its Noise handshake gets an overlay channel and joins
	// the fetch pool; every peer that drops hands its outstanding chunks back.
	bool attach(Object *hyperswarm_peer);   // Object, not Node: the peer is RefCounted
	void detach();
	int peer_count() const;

	// The content hash of a file, computed the way the overlay computes it —
	// blake2b over the hypercore leaf construction, not a plain digest. A game
	// needs this to name what it serves and to ask for what it wants, and getting
	// it from anywhere else yields a hash the overlay will reject.
	String content_hash_of(const String &file_path) const;

	// Declare that this instance holds `content_hash` at `file_path`, and will
	// serve it to peers that ask. The file is NOT read here — the overlay reads
	// chunks at their offsets when they are requested, so registering a 4 GB asset
	// costs nothing.
	void serve(const String &content_hash, const String &file_path);

	// Ask connected peers for content, to be assembled at `dest_path`. Returns
	// immediately; watch the signals. A partial left by an earlier attempt is
	// resumed rather than restarted.
	void fetch(const String &content_hash, const String &dest_path);

	// Stop fetching. `discard` unlinks the partial; otherwise it is kept so a
	// later fetch of the same hash resumes it.
	void cancel(const String &content_hash, bool discard);

	// Drain completions onto the main thread and advance liboverlay's clock.
	// Called automatically from _process; exposed for headless use and tests,
	// where there is no scene tree ticking.
	void poll();

	void _process(double delta) override;

	// Diagnostics, because "it is downloading" is not an answer when it is not.
	int active_fetches() const;
	String describe() const;

protected:
	static void _bind_methods();

private:
	// Work handed to the worker, and results handed back. Deliberately plain
	// values: nothing that owns a file handle or a Godot object crosses a thread.
	struct Event {
		enum Kind { FETCHED, FAILED, PROGRESS } kind;
		std::string hash, detail;
		uint64_t received = 0, total = 0;
	};

	void push_event(const Event &e);
	// Run on the UDX thread, or inline when nothing is attached yet.
	void on_loop(std::function<void()> fn);
	// Everything below runs ON THE UDX THREAD.
	void udx_add_peer(int32_t id, Protomux *mux);
	void udx_remove_peer(int32_t id);

	std::unique_ptr<hyperoverlay::index::FileIndex> index_;
	std::unique_ptr<hyperoverlay::Protocol> proto_;

	bool open_ = false;
	std::string storage_dir_;

	// The peer is RefCounted and the transport is a MEMBER of it, so borrowing the
	// pointer without holding the object is borrowing memory the game may free at
	// any moment. It does: a room switch drops its reference before handing us the
	// replacement, and detach() then tore down a freed transport — corrupting the
	// heap, which the NEXT peer's uv_loop_init reported as its own abort. Holding
	// the peer here means the transport we point at cannot die before we let go of
	// it. See integration/layer14_room_switch.gd.
	Ref<HyperswarmMultiplayerPeer> peer_;
	UdxTransport *transport_ = nullptr;
	// One overlay channel per connection, created and destroyed on the UDX thread.
	std::map<int32_t, hyperswarm::OverlayTransport *> peers_;

	mutable std::mutex evt_mutex_;
	std::deque<Event> events_;
	// Bounded: a fetch that emits progress faster than the game polls must drop
	// updates, not accumulate them. Terminal events (FETCHED/FAILED) are never
	// dropped — losing one strands a caller waiting for a signal that never comes.
	static constexpr size_t MAX_EVENTS = 4096;
	size_t dropped_progress_ = 0;

	uint64_t started_ms_ = 0;
};

} // namespace godot

#endif // HYPERSWARM_OVERLAY_NODE_H