/////////////////////////////////////////////////////////////////// // // THE ID ALLOCATOR // // This ID allocator's goal is to allocate IDs in such a way that the // synchronous model gets the same IDs as the master model. // // DESIGN PRINCIPLES // // There are two classes defined here: IdGlobalPool, and IdPlayerPool. // // Every logged-in player maintains an IdPlayerPool. That's basically a fifo // queue of ID batches. An ID batch is a contiguous range of IDs, containing // between 128 and 256 contiguous IDs. The IdPlayerPool is difference // transmitted, to ensure that the synchronous model has the same batches as the // master model. // // When a player creates a thread, that thread gets an ID batch from the // IdPlayerPool. To make this possible, the Lua runtime has been modified so // that a thread can contain an ID batch. When that thread allocates IDs, it // uses the batch it was allocated. In the unlikely event that a thread's batch // is used up, the thread falls back to using the IdGlobalPool. Such fallback // IDs are not likely to be predicted correctly. // // When a player creates a thread, he uses up one batch from his IdPlayerPool. // In the master model, the player pool is 'refilled' by asking the IdGlobalPool // to create a new batch. In the synchronous model, the IdPlayerPool is not // directly refilled, but the difference transmitter effectively refills it. It // is imperative that this difference transmission happen before the player's // asynchronous model runs out of batches, otherwise we'll get prediction // failures. // // It is common that a thread will only use 1 or 2 IDs. If a thread exits // without using up most of its IDs, then the batch it contains is still a // pretty usable batch. The batch gets returned to the IdGlobalPool, which will // eventually use that salvaged batch to satisfy an IdPlayerPool refill request. // // THE NUMERIC RANGES // // Any 53-bit number can be losslessly stored in a lua_Number. In other // words, the largest integer that can be stored losslessly is: // // * 0x001FFFFFFFFFFFFF // // As it turns out, that's just barely larger than 9 quadrillion. We are // going to use IDs that are between 0 and 9 quadrillion. We divide the // range of possible IDs into several subsections: // // 0 quadrillion + : manually created IDs. // 1 quadrillion + : used by master model's IdGlobalPool to create batches. // 5 quadrillion + : used by master model's IdGlobalPool to create individual IDs. // 8 quadrillion + : used by sync model's IdGlobalPool to create individual IDs. // // If you exhaust the Master Model's invididual pool at a rate of 10,000,000 IDs // per second, the individual pool will last for 12 years. // // BATCH REPRESENTATION // // A batch is represented as a 64-bit integer. The batch contains all IDs // starting with that integer, and incrementing, until the the two lowest digits // are 0x00. // // For example, consider the batch ID 0x11111111111111FC. That batch includes // these IDs: // // * 0x11111111111111FC // * 0x11111111111111FD // * 0x11111111111111FE // * 0x11111111111111FF // // But it does not include 0x1111111111111200. // // As a special case, the number 0 is used to indicate "invalid batch". // // SEQUENCE NUMBERS // // If you allocate IDs from a player pool rapidly, you will exhaust the fifo in // the player pool. As a result, ID allocation will fall back to the global // pool, which will cause prediction failures. // // Depending on your requirements, you may be able to use sequence numbers // instead of IDs. The player pool contains a monotonically increasing counter // for generating sequence numbers. These can be allocated extremely fast // and they'll never run out. These are highly predictable as long as the // only person fetching sequence numbers is the player himself. The downside // is that sequence numbers are not globally unique - they're only locally // unique for a single player. That may be enough for your requirements. // // Currently, sequence numbers are used by the random number generator. // /////////////////////////////////////////////////////////////////// #pragma once #include "wrap-map.hpp" #include "wrap-sstream.hpp" #include "wrap-deque.hpp" #include "luastack.hpp" #include "streambuffer.hpp" #include "debugcollector.hpp" #include #include class IdGlobalPool : public eng::nevernew { public: // Construct and destroy global pools. Note that after constructing // a global pool, it is generally also necessary to initialize it // for Master or Synchronous operation using init_master or init_synch. // Any attempt to use a pool that hasn't been init_master or init_synch // will fail. IdGlobalPool(); ~IdGlobalPool(); // Initialize the pool for use in a master model. void init_master(); // Initialize the pool for use in a synchronous model. void init_synch(); // Create a single unique ID. Ids allocated this way are // unlikely to be predicted correctly. int64_t get_one(); // Obtain a batch of IDs from the global pool. In a master // model, the batch is guaranteed to contain at least 128 IDs. // In a synchronous model, the batch is always zero (invalid). int64_t get_batch(); // Try to return the specified batch to the global pool. // The salvage operation quietly does nothing if the batch is // zero, or the batch contains fewer than 128 IDs. void salvage(int64_t batch); // Get a global sequence number. These are not the same as IDs. // Sequence numbers are unlikely to be predicted correctly. int64_t get_seqno() { return next_seqno_++; } // Serialize to or deserialize from a streambuffer. void serialize(StreamBuffer *sb) const; void deserialize(StreamBuffer *sb); // Generate a debug string. eng::string debug_string() const; private: eng::vector salvaged_; int64_t next_batch_; int64_t next_id_; int64_t next_seqno_; friend int unittests_idalloc(lua_State *L); }; class IdPlayerPool : public eng::nevernew { public: // Construct a player pool. // // The fifo is initially in the disabled state. In the disabled state, the // fifo is not kept filled. You can still use the allocator when the fifo is // disabled - doing so just fetches a batch from the global pool. // IdPlayerPool(IdGlobalPool *g); ~IdPlayerPool(); // Set the fifo capacity. Max=255. void set_fifo_capacity(int n); int get_fifo_capacity() const { return fifo_capacity_; } // Return all batches from the fifo to the global pool. void unqueue(); // Refill the fifo of batches from the global pool. void refill(); // Get a single ID from the fifo. Also refills the fifo. int64_t get_one(); // Get a player sequence number. This is not the same as an ID: player // sequence numbers are unique per player, but not globally. int64_t get_seqno() { return next_seqno_++; } // Return the size of the queue. int size() { return ranges_.size(); } // Return true if the two pools are identical. bool exactly_equal(const IdPlayerPool &other) const; // Check that the pool is valid. bool valid() const; // unit testing functions. // // These let you manipulate the deque explicitly. But that's // only useful for unit testing. // void test_push_back(int64_t range); void test_pop_front(); void test_clear_ranges(); // Serialize to or deserialize from a streambuffer. // Caution: the pointer to the global pool is not serialized or deserialized. void serialize(StreamBuffer *sb) const; void deserialize(StreamBuffer *sb); // Difference transmission // void diff(const IdPlayerPool &auth, StreamBuffer *sb) const; void patch(StreamBuffer *sb, DebugCollector *dbc); // Debug string. eng::string debug_string() const; private: IdGlobalPool *global_; int fifo_capacity_; int64_t next_seqno_; eng::deque ranges_; friend int lfn_unittests_idalloc(lua_State *L); };