foundationdb/fdbserver/workloads/HTTPKeyValueStore.cpp

449 lines
14 KiB
C++

/*
* HTTPKeyValueStore.cpp
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2026 Apple Inc. and the FoundationDB project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "flow/Arena.h"
#include "flow/IRandom.h"
#include "flow/Trace.h"
#include "flow/serialize.h"
#include "fdbrpc/HTTP.h"
#include "fdbserver/tester/workloads.h"
/*
* Implements a basic put/get key-value store over HTTP to test the http client and simulated server code.
*/
#define DEBUG_HTTPKV false
static const int maxClients = 100;
struct SimHTTPKVStore : NonCopyable, ReferenceCounted<SimHTTPKVStore> {
std::unordered_map<std::string, std::string> data;
int64_t lastClientSeqnos[maxClients];
SimHTTPKVStore() {
for (int i = 0; i < maxClients; i++) {
lastClientSeqnos[i] = 0;
}
}
bool checkClientSeqno(int clientId, int64_t seqNo) {
ASSERT(clientId < maxClients);
if (seqNo < lastClientSeqnos[clientId]) {
if (DEBUG_HTTPKV) {
fmt::print("Client {0} SeqNo {1} < {2}\n", clientId, seqNo, lastClientSeqnos[clientId]);
}
// out of order retransmit, ignore
return false;
}
lastClientSeqnos[clientId] = seqNo;
return true;
}
};
void httpKVProcessPut(Reference<SimHTTPKVStore> kvStore,
std::string& key,
Reference<HTTP::IncomingRequest> req,
Reference<HTTP::OutgoingResponse> response) {
// content is value to put in kv store
kvStore->data[key] = req->data.content;
if (DEBUG_HTTPKV) {
fmt::print("KV:put {0} = {1}\n", key, req->data.content);
}
response->code = 200;
response->data.contentLen = 0;
}
// content is empty for request, content for response is the value for the key
void httpKVProcessGet(Reference<SimHTTPKVStore> kvStore,
std::string& key,
Reference<HTTP::IncomingRequest> req,
Reference<HTTP::OutgoingResponse> response) {
auto it = kvStore->data.find(key);
ASSERT(it != kvStore->data.end());
if (DEBUG_HTTPKV) {
fmt::print("KV:get {0} = {1}\n", key, it->second);
}
response->code = 200;
response->data.headers["Content-MD5"] = HTTP::computeMD5Sum(it->second);
PacketWriter pw(response->data.content->getWriteBuffer(it->second.size()), nullptr, Unversioned());
pw.serializeBytes(it->second);
response->data.contentLen = it->second.size();
}
// key header always exists for both put and get
Future<Void> httpKVRequestCallback(Reference<SimHTTPKVStore> kvStore,
Reference<HTTP::IncomingRequest> req,
Reference<HTTP::OutgoingResponse> response) {
co_await delay(0);
ASSERT(req->verb == HTTP::HTTP_VERB_PUT || req->verb == HTTP::HTTP_VERB_GET);
ASSERT_EQ(req->resource, "/kv");
// content-length and RequestID from http are already filled in
// ASSERT_EQ(req->data.headers.size(), 5);
ASSERT_EQ(req->data.headers.size(), 5);
ASSERT(req->data.headers.contains("Key"));
ASSERT(req->data.headers.contains("ClientID"));
ASSERT(req->data.headers.contains("UID"));
ASSERT(req->data.headers.contains("SeqNo"));
int clientId = atoi(req->data.headers["ClientID"].c_str());
int seqNo = atoi(req->data.headers["SeqNo"].c_str());
ASSERT(req->data.headers.contains("Content-Length"));
ASSERT_EQ(req->data.headers["Content-Length"], std::to_string(req->data.content.size()));
ASSERT_EQ(req->data.contentLen, req->data.content.size());
if (!kvStore->checkClientSeqno(clientId, seqNo)) {
CODE_PROBE(true, "kv store ignoring out of order retransmit");
throw http_request_failed();
}
std::string key = req->data.headers["ClientID"] + ":" + req->data.headers["Key"];
// echo headers back to the client for validation
response->data.headers["ClientID"] = req->data.headers["ClientID"];
response->data.headers["Key"] = req->data.headers["Key"];
response->data.headers["UID"] = req->data.headers["UID"];
// FIXME: need to echo http request id if present too
if (req->verb == HTTP::HTTP_VERB_PUT) {
httpKVProcessPut(kvStore, key, req, response);
} else {
httpKVProcessGet(kvStore, key, req, response);
}
}
static Reference<SimHTTPKVStore> globalKVStore = Reference<SimHTTPKVStore>();
struct KeyValueRequestHandler : HTTP::IRequestHandler, ReferenceCounted<KeyValueRequestHandler> {
// global kv store for all request handler instances during simulation
Reference<SimHTTPKVStore> myKVStore;
KeyValueRequestHandler() = default;
Future<Void> handleRequest(Reference<HTTP::IncomingRequest> req,
Reference<HTTP::OutgoingResponse> response) override {
return httpKVRequestCallback(myKVStore, req, response);
}
Future<Void> init() override {
if (!globalKVStore) {
globalKVStore = makeReference<SimHTTPKVStore>();
}
myKVStore = globalKVStore;
return Void();
}
Reference<HTTP::IRequestHandler> clone() override { return makeReference<KeyValueRequestHandler>(); }
void addref() override { ReferenceCounted<KeyValueRequestHandler>::addref(); }
void delref() override { ReferenceCounted<KeyValueRequestHandler>::delref(); }
};
struct HTTPKeyValueStoreWorkload : TestWorkload {
static constexpr auto NAME = "HTTPKeyValueStore";
double testDuration;
int nodeCount;
int opsPerSecond;
Future<Void> client;
int64_t nextSeqNo = 1;
bool manualResolve;
// handle race where test phase killed put in progress
Optional<std::pair<std::string, std::string>> activePut;
// client's view of the correct key-value state of the data
std::unordered_map<std::string, std::string> myData;
// client's connection it reuses between request attempts
Reference<IConnection> conn;
std::string hostname = "httpkvstore";
std::string service = "80";
PerfIntCounter getCount, putCount, connectCount, failedConnectCount;
explicit HTTPKeyValueStoreWorkload(WorkloadContext const& wcx)
: TestWorkload(wcx), putCount("PutCount"), getCount("GetCount"), connectCount("ConnectCount"),
failedConnectCount("FailedConnectCount") {
testDuration = getOption(options, "testDuration"_sr, 30.0);
nodeCount = getOption(options, "nodeCount"_sr, 100);
opsPerSecond = getOption(options, "nodeCount"_sr, 100);
manualResolve = getOption(options, "manualResolve"_sr, sharedRandomNumber % 2);
// it's important that we select this on a test-by-test basis and not a connection-by-connection basis as if one
// approach works but another doesn't, it would be hidden
sharedRandomNumber /= 2;
}
std::string getKey(int i) {
// FIXME: larger/more random keys?
return std::to_string(i);
}
std::string randomValue() {
// FIXME: larger/more random values?
return deterministicRandom()->randomUniqueID().toString();
}
// handles retrying on timeout and reinitializing connection like other users of HTTP (S3BlobStore, RestClient)
Future<Reference<HTTP::IncomingResponse>> doKVRequest(HTTPKeyValueStoreWorkload* self,
std::string key,
Optional<std::string> value) {
UnsentPacketQueue content;
int seqNo = self->nextSeqNo;
++self->nextSeqNo;
while (true) {
Error err;
try {
while (!self->conn) {
// sometimes do resolve and connect directly, other times simulate what rest kms connector does
// and resolve endpoints themself and then connect to one directly
if (self->manualResolve) {
std::vector<NetworkAddress> addrs =
co_await INetworkConnections::net()->resolveTCPEndpoint(self->hostname, self->service);
ASSERT(!addrs.empty());
int idx = deterministicRandom()->randomInt(0, addrs.size());
self->conn =
co_await timeoutError(INetworkConnections::net()->connect(
addrs[idx].ip.toString(), std::to_string(addrs[idx].port), false),
FLOW_KNOBS->CONNECTION_MONITOR_TIMEOUT);
} else {
self->conn = co_await timeoutError(
INetworkConnections::net()->connect(self->hostname, self->service, false),
FLOW_KNOBS->CONNECTION_MONITOR_TIMEOUT);
}
if (self->conn.isValid()) {
co_await self->conn->connectHandshake();
++self->connectCount;
} else {
co_await delay(0.1);
++self->failedConnectCount;
}
}
content.discardAll();
auto req = makeReference<HTTP::OutgoingRequest>();
UID requestID = deterministicRandom()->randomUniqueID();
req->data.content = &content;
req->data.contentLen = 0;
req->resource = "/kv";
req->data.headers["Key"] = key;
req->data.headers["ClientID"] = std::to_string(self->clientId);
req->data.headers["UID"] = requestID.toString();
req->data.headers["SeqNo"] = std::to_string(seqNo);
if (value.present()) {
// put key-value pair
req->verb = HTTP::HTTP_VERB_PUT;
PacketWriter pw(req->data.content->getWriteBuffer(value.get().size()), nullptr, Unversioned());
pw.serializeBytes(value.get());
req->data.contentLen = value.get().size();
} else {
// get key-value pair
req->verb = HTTP::HTTP_VERB_GET;
}
Reference<IRateControl> sendReceiveRate = makeReference<Unlimited>();
int64_t bytes_sent = 0;
Reference<HTTP::IncomingResponse> response = co_await timeoutError(
HTTP::doRequest(self->conn, req, sendReceiveRate, &bytes_sent, sendReceiveRate), 5.0);
// sometimes randomly close connection anyway
if (buggify(0.1)) {
ASSERT(self->conn.isValid());
self->conn->close();
self->conn.clear();
}
ASSERT_EQ(response->code, 200);
ASSERT(response->data.headers.contains("ClientID"));
ASSERT_EQ(response->data.headers["ClientID"], std::to_string(self->clientId));
ASSERT(response->data.headers.contains("Key"));
ASSERT_EQ(response->data.headers["Key"], key);
ASSERT(response->data.headers.contains("UID"));
ASSERT_EQ(response->data.headers["UID"], requestID.toString());
co_return response;
} catch (Error& e) {
err = e;
}
if (err.code() == error_code_operation_cancelled) {
throw err;
}
if (DEBUG_HTTPKV) {
fmt::print("REQ: ERROR: {0}\n", err.name());
}
if (self->conn) {
self->conn->close();
self->conn.clear();
}
if (err.code() != error_code_timed_out && err.code() != error_code_connection_failed &&
err.code() != error_code_lookup_failed) {
throw err;
}
// request got timed out or connection could not be established, close conn and try again
co_await delay(0.1);
}
}
Future<Void> put(HTTPKeyValueStoreWorkload* self, std::string key, std::string value) {
// TODO do http request
std::pair<std::string, std::string> active = { key, value };
self->activePut = active;
if (DEBUG_HTTPKV) {
fmt::print("CL:put {0}:{1} = {2}\n", self->clientId, key, value);
}
Reference<HTTP::IncomingResponse> response = co_await self->doKVRequest(self, key, value);
if (DEBUG_HTTPKV) {
fmt::print("CL:put {0}:{1} = {2} DONE\n", self->clientId, key, value);
}
// upon success, put in map
self->activePut.reset();
self->myData[key] = value;
++self->putCount;
}
Future<Void> get(HTTPKeyValueStoreWorkload* self, std::string key, bool checkActive) {
if (DEBUG_HTTPKV) {
fmt::print("CL:get {0}:{1}\n", self->clientId, key);
}
Reference<HTTP::IncomingResponse> response = co_await self->doKVRequest(self, key, {});
if (DEBUG_HTTPKV) {
fmt::print("CL:get {0}:{1} = {2} DONE\n", self->clientId, key, response->data.content);
}
if (!checkActive || !self->activePut.present()) {
ASSERT_EQ(response->data.content, self->myData[key]);
} else {
bool contentCorrect = response->data.content == self->myData[key];
bool inFlightCorrect =
self->activePut.get().first == key && self->activePut.get().second == response->data.content;
ASSERT(contentCorrect || inFlightCorrect);
}
++self->getCount;
}
Future<Void> setup(Database const& cx) override { return _setup(this); }
Future<Void> _setup(HTTPKeyValueStoreWorkload* self) {
ASSERT(g_network->isSimulated());
if (self->clientId == 0) {
TraceEvent("SimHTTPKeyValueStoreRegistering");
if (DEBUG_HTTPKV) {
fmt::print("Registering sim http kv server\n");
}
co_await g_simulator->registerSimHTTPServer(
self->hostname, self->service, makeReference<KeyValueRequestHandler>());
if (DEBUG_HTTPKV) {
fmt::print("Registered sim http kv server\n");
}
TraceEvent("SimHTTPKeyValueStoreRegistered");
}
TraceEvent("SimHTTPKeyValueStoreLoading");
for (int i = 0; i < self->nodeCount; i++) {
co_await self->put(self, self->getKey(i), self->randomValue());
}
TraceEvent("SimHTTPKeyValueStoreLoaded");
}
Future<Void> start(Database const& cx) override {
client = httpKeyValueClient(this);
return delay(testDuration);
}
Future<bool> check(Database const& cx) override {
// cancel workload client first
client = Future<Void>(Void());
return _check(this);
}
Future<bool> _check(HTTPKeyValueStoreWorkload* self) {
// reset conn since it could have been cancelled during part of initialization
if (self->conn) {
self->conn->close();
self->conn.clear();
}
TraceEvent("SimHTTPKeyValueStoreWorkloadChecking");
for (int i = 0; i < self->nodeCount; i++) {
co_await self->get(self, self->getKey(i), true);
}
TraceEvent("SimHTTPKeyValueStoreWorkloadChecked");
// tear down connection after test
if (self->conn) {
self->conn->close();
self->conn.clear();
}
co_return true;
}
void getMetrics(std::vector<PerfMetric>& m) override {
m.push_back(getCount.getMetric());
m.push_back(putCount.getMetric());
m.push_back(connectCount.getMetric());
m.push_back(failedConnectCount.getMetric());
}
Future<Void> httpKeyValueClient(HTTPKeyValueStoreWorkload* self) {
TraceEvent("SimHTTPKeyValueStoreWorkloadStarting");
double last = now();
while (true) {
Future<Void> waitNextOp = poisson(&last, 1.0 / self->opsPerSecond);
int key = deterministicRandom()->randomInt(0, self->nodeCount);
if (deterministicRandom()->coinflip()) {
co_await self->put(self, self->getKey(key), self->randomValue());
} else {
co_await self->get(self, self->getKey(key), false);
}
co_await waitNextOp;
}
}
};
WorkloadFactory<HTTPKeyValueStoreWorkload> HTTPKeyValueStoreWorkloadFactory;