Preserve and retry telemetry match boundaries across restarts
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
#include "telemetry.hpp"
|
||||
#include <json.hpp>
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using utils::telemetry::outbox;
|
||||
using utils::telemetry::new_match_key;
|
||||
using utils::telemetry::parse_acknowledgement;
|
||||
|
||||
const std::string server = "t6_zombie_buried_1";
|
||||
const std::string endpoint = "https://example.test/api/telemetry";
|
||||
|
||||
std::string body(const std::string& match, int seq, const std::string& event)
|
||||
{
|
||||
nlohmann::json value = {{"server", server}, {"map", "zm_buried"}, {"match", match},
|
||||
{"seq", seq}, {"event", event}, {"players", nlohmann::json::array()}};
|
||||
if (event != "game_start") value["round"] = 12;
|
||||
return value.dump();
|
||||
}
|
||||
|
||||
template <typename F> void must_fail(F action)
|
||||
{
|
||||
bool failed = false;
|
||||
try { action(); } catch (const std::exception&) { failed = true; }
|
||||
assert(failed);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
assert(argc >= 2);
|
||||
const fs::path root(argv[1]);
|
||||
const outbox queue(root, server, endpoint);
|
||||
if (argc == 3)
|
||||
{
|
||||
const std::string mode(argv[2]);
|
||||
if (mode == "produce")
|
||||
{
|
||||
const auto match = new_match_key();
|
||||
queue.store(body(match, 1, "game_start"));
|
||||
queue.store(body(match, 14, "game_end"));
|
||||
}
|
||||
else if (mode == "recover")
|
||||
{
|
||||
const auto reports = queue.load();
|
||||
assert(reports.size() == 2);
|
||||
const auto next = new_match_key();
|
||||
assert(reports[0].match != next);
|
||||
queue.store(body(next, 1, "game_start"));
|
||||
const auto all = queue.load();
|
||||
assert(all.size() == 3 && all[0].event == "game_start" && all[1].event == "game_end");
|
||||
assert(all[0].match == all[1].match && all[2].match == next);
|
||||
for (const auto& value : all) queue.erase(value.match, value.seq);
|
||||
}
|
||||
else assert(mode == "empty" && queue.load().empty());
|
||||
return 0;
|
||||
}
|
||||
|
||||
fs::create_directories(root);
|
||||
assert(queue.load().empty());
|
||||
std::set<std::string> keys;
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
const auto key = new_match_key();
|
||||
assert(!key.empty() && key.size() <= 64 && keys.insert(key).second);
|
||||
}
|
||||
const auto match = new_match_key();
|
||||
const auto start = body(match, 1, "game_start");
|
||||
const auto end = body(match, 30, "game_end");
|
||||
queue.store(start);
|
||||
queue.store(end);
|
||||
queue.store(start); // A retry cannot add a duplicate or reorder the queue.
|
||||
const auto replay = outbox(root, server, endpoint).load();
|
||||
assert(replay.size() == 2 && replay[0].body == start && replay[1].body == end);
|
||||
assert(replay[0].seq == 1 && replay[1].seq == 30 && replay[1].round == 12);
|
||||
assert(outbox(root, "other-server", endpoint).load().empty());
|
||||
assert(outbox(root, server, "https://different.test/api").load().empty());
|
||||
auto changed = nlohmann::json::parse(start);
|
||||
changed["map"] = "another-map";
|
||||
must_fail([&] { queue.store(changed.dump()); });
|
||||
must_fail([&] { queue.store(body(match, 0, "game_start")); });
|
||||
must_fail([&] { queue.store(body(match, 2, "game_start")); });
|
||||
must_fail([&] { queue.store(body(match, 3, "heartbeat")); });
|
||||
must_fail([&] { queue.store("<html>error</html>"); });
|
||||
changed = nlohmann::json::parse(end);
|
||||
changed["server"] = "wrong-server";
|
||||
must_fail([&] { queue.store(changed.dump()); });
|
||||
|
||||
const auto file = fs::directory_iterator(root)->path();
|
||||
auto temporary = file;
|
||||
temporary += ".tmp";
|
||||
{ std::ofstream stream(temporary); stream << "interrupted write"; }
|
||||
assert(queue.load().size() == 2); // Incomplete temp file never replaces committed data.
|
||||
fs::remove(temporary);
|
||||
fs::create_directory(temporary); // Force replacement writes to fail even under root.
|
||||
must_fail([&] { queue.erase(match, 1); });
|
||||
must_fail([&] { queue.store(body(new_match_key(), 1, "game_start")); });
|
||||
assert(queue.load().size() == 2);
|
||||
fs::remove(temporary);
|
||||
queue.erase(match, 1);
|
||||
queue.erase(match, 1); // Cleanup is idempotent after an uncertain completion.
|
||||
assert(queue.load().size() == 1 && queue.load()[0].body == end);
|
||||
queue.erase(match, 30);
|
||||
assert(queue.load().empty());
|
||||
|
||||
{ std::ofstream stream(file); stream << "corrupt"; }
|
||||
must_fail([&] { queue.load(); });
|
||||
must_fail([&] { queue.store(start); });
|
||||
assert(fs::file_size(file) == 7); // Never silently overwrite corrupt retained data.
|
||||
fs::remove(file);
|
||||
|
||||
for (const std::string response : {"", "<html>bad gateway</html>", "{", "null", "[]", "true",
|
||||
"{}", "{\"ok\":false}", "{\"ok\":1}", "{\"ok\":\"true\"}", "{\"ok\":true}garbage"})
|
||||
assert(!parse_acknowledgement(response).accepted);
|
||||
const auto ack = parse_acknowledgement(R"({"ok":true,"telemetry":{"timing":{"matchId":921}}})");
|
||||
assert(ack.accepted && ack.match_id == "921");
|
||||
assert(parse_acknowledgement(R"({"ok":true})").accepted);
|
||||
const auto large_id = parse_acknowledgement(R"({"ok":true,"telemetry":{"timing":{"matchId":9223372036854775807}}})");
|
||||
assert(large_id.match_id == "9223372036854775807");
|
||||
assert(parse_acknowledgement(R"({"ok":true,"telemetry":null})").match_id.empty());
|
||||
assert(parse_acknowledgement(R"({"ok":true,"telemetry":{"timing":{"matchId":"wrong"}}})").match_id.empty());
|
||||
assert(!parse_acknowledgement("{\"ok\":true}" + std::string(0x5000, ' ')).accepted);
|
||||
std::cout << "PASS native telemetry fixtures\n";
|
||||
}
|
||||
Reference in New Issue
Block a user