-
Notifications
You must be signed in to change notification settings - Fork 968
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: preparation for basic http api (#2764)
* chore: preparation for basic http api The goal is to provide very basic support for simple commands, fancy stuff like pipelining, blocking commands won't work. 1. Added optional registration for /api handler. 2. Implemented parsing of post body. 3. Added basic formatting routine for the response. It does not cover all the commands but should suffice for basic usage. The API is a POST method and the body of the request should contain command arguments formatted as json array. For example, `'["set", "foo", "bar", "ex", "100"]'`. The response is a json object with either `result` field holding the response of the command or `error` field containing the error message sent by the server. See `test_http` test in tests/dragonfly/connection_test.py for more details. * chore: cover iouring with enable_direct_fd --------- Signed-off-by: Roman Gershman <[email protected]>
- Loading branch information
Showing
9 changed files
with
300 additions
and
34 deletions.
There are no files selected for viewing
Submodule helio
updated
4 files
+5 −1 | util/fibers/listener_interface.cc | |
+22 −11 | util/http/http_handler.cc | |
+22 −5 | util/http/http_handler.h | |
+27 −21 | util/http/http_main.cc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
// Copyright 2024, DragonflyDB authors. All rights reserved. | ||
// See LICENSE for licensing terms. | ||
// | ||
|
||
#include "server/http_api.h" | ||
|
||
#include "base/logging.h" | ||
#include "core/flatbuffers.h" | ||
#include "facade/conn_context.h" | ||
#include "facade/reply_builder.h" | ||
#include "server/main_service.h" | ||
#include "util/http/http_common.h" | ||
|
||
namespace dfly { | ||
using namespace util; | ||
using namespace std; | ||
namespace h2 = boost::beast::http; | ||
using facade::CapturingReplyBuilder; | ||
|
||
namespace { | ||
|
||
bool IsVectorOfStrings(flexbuffers::Reference req) { | ||
if (!req.IsVector()) { | ||
return false; | ||
} | ||
|
||
auto vec = req.AsVector(); | ||
if (vec.size() == 0) { | ||
return false; | ||
} | ||
|
||
for (size_t i = 0; i < vec.size(); ++i) { | ||
if (!vec[i].IsString()) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
// Escape a string so that it is legal to print it in JSON text. | ||
std::string JsonEscape(string_view input) { | ||
auto hex_digit = [](unsigned c) -> char { | ||
DCHECK_LT(c, 0xFu); | ||
return c < 10 ? c + '0' : c - 10 + 'a'; | ||
}; | ||
|
||
string out; | ||
out.reserve(input.size() + 2); | ||
out.push_back('\"'); | ||
|
||
auto p = input.begin(); | ||
auto e = input.end(); | ||
|
||
while (p < e) { | ||
uint8_t c = *p; | ||
if (c == '\\' || c == '\"') { | ||
out.push_back('\\'); | ||
out.push_back(*p++); | ||
} else if (c <= 0x1f) { | ||
switch (c) { | ||
case '\b': | ||
out.append("\\b"); | ||
p++; | ||
break; | ||
case '\f': | ||
out.append("\\f"); | ||
p++; | ||
break; | ||
case '\n': | ||
out.append("\\n"); | ||
p++; | ||
break; | ||
case '\r': | ||
out.append("\\r"); | ||
p++; | ||
break; | ||
case '\t': | ||
out.append("\\t"); | ||
p++; | ||
break; | ||
default: | ||
// this condition captures non readable chars with value < 32, | ||
// so size = 1 byte (e.g control chars). | ||
out.append("\\u00"); | ||
out.push_back(hex_digit((c & 0xf0) >> 4)); | ||
out.push_back(hex_digit(c & 0xf)); | ||
p++; | ||
} | ||
} else { | ||
out.push_back(*p++); | ||
} | ||
} | ||
|
||
out.push_back('\"'); | ||
return out; | ||
} | ||
|
||
struct CaptureVisitor { | ||
CaptureVisitor() { | ||
str = R"({"result":)"; | ||
} | ||
|
||
void operator()(monostate) { | ||
} | ||
|
||
void operator()(long v) { | ||
absl::StrAppend(&str, v); | ||
} | ||
|
||
void operator()(double v) { | ||
absl::StrAppend(&str, v); | ||
} | ||
|
||
void operator()(const CapturingReplyBuilder::SimpleString& ss) { | ||
absl::StrAppend(&str, "\"", ss, "\""); | ||
} | ||
|
||
void operator()(const CapturingReplyBuilder::BulkString& bs) { | ||
absl::StrAppend(&str, JsonEscape(bs)); | ||
} | ||
|
||
void operator()(CapturingReplyBuilder::Null) { | ||
absl::StrAppend(&str, "null"); | ||
} | ||
|
||
void operator()(CapturingReplyBuilder::Error err) { | ||
str = absl::StrCat(R"({"error": ")", err.first); | ||
} | ||
|
||
void operator()(facade::OpStatus status) { | ||
absl::StrAppend(&str, "\"", facade::StatusToMsg(status), "\""); | ||
} | ||
|
||
void operator()(const CapturingReplyBuilder::StrArrPayload& sa) { | ||
absl::StrAppend(&str, "not_implemented"); | ||
} | ||
|
||
void operator()(unique_ptr<CapturingReplyBuilder::CollectionPayload> cp) { | ||
if (!cp) { | ||
absl::StrAppend(&str, "null"); | ||
return; | ||
} | ||
if (cp->len == 0 && cp->type == facade::RedisReplyBuilder::ARRAY) { | ||
absl::StrAppend(&str, "[]"); | ||
return; | ||
} | ||
|
||
absl::StrAppend(&str, "["); | ||
for (auto& pl : cp->arr) { | ||
visit(*this, std::move(pl)); | ||
} | ||
} | ||
|
||
void operator()(facade::SinkReplyBuilder::MGetResponse resp) { | ||
absl::StrAppend(&str, "not_implemented"); | ||
} | ||
|
||
void operator()(const CapturingReplyBuilder::ScoredArray& sarr) { | ||
absl::StrAppend(&str, "["); | ||
for (const auto& [key, score] : sarr.arr) { | ||
absl::StrAppend(&str, "{", JsonEscape(key), ":", score, "},"); | ||
} | ||
if (sarr.arr.size() > 0) { | ||
str.pop_back(); | ||
} | ||
absl::StrAppend(&str, "]"); | ||
} | ||
|
||
string str; | ||
}; | ||
|
||
} // namespace | ||
|
||
void HttpAPI(const http::QueryArgs& args, HttpRequest&& req, Service* service, | ||
HttpContext* http_cntx) { | ||
auto& body = req.body(); | ||
|
||
flexbuffers::Builder fbb; | ||
flatbuffers::Parser parser; | ||
flexbuffers::Reference doc; | ||
bool success = parser.ParseFlexBuffer(body.c_str(), nullptr, &fbb); | ||
if (success) { | ||
fbb.Finish(); | ||
doc = flexbuffers::GetRoot(fbb.GetBuffer()); | ||
if (!IsVectorOfStrings(doc)) { | ||
success = false; | ||
} | ||
} | ||
|
||
if (!success) { | ||
auto response = http::MakeStringResponse(h2::status::bad_request); | ||
http::SetMime(http::kTextMime, &response); | ||
response.body() = "Failed to parse json\r\n"; | ||
http_cntx->Invoke(std::move(response)); | ||
return; | ||
} | ||
|
||
vector<string> cmd_args; | ||
flexbuffers::Vector vec = doc.AsVector(); | ||
for (size_t i = 0; i < vec.size(); ++i) { | ||
cmd_args.push_back(vec[i].AsString().c_str()); | ||
} | ||
vector<facade::MutableSlice> cmd_slices(cmd_args.size()); | ||
for (size_t i = 0; i < cmd_args.size(); ++i) { | ||
cmd_slices[i] = absl::MakeSpan(cmd_args[i]); | ||
} | ||
|
||
facade::ConnectionContext* context = (facade::ConnectionContext*)http_cntx->user_data(); | ||
DCHECK(context); | ||
|
||
facade::CapturingReplyBuilder reply_builder; | ||
auto* prev = context->Inject(&reply_builder); | ||
// TODO: to finish this. | ||
service->DispatchCommand(absl::MakeSpan(cmd_slices), context); | ||
facade::CapturingReplyBuilder::Payload payload = reply_builder.Take(); | ||
|
||
context->Inject(prev); | ||
auto response = http::MakeStringResponse(); | ||
http::SetMime(http::kJsonMime, &response); | ||
|
||
CaptureVisitor visitor; | ||
std::visit(visitor, std::move(payload)); | ||
visitor.str.append("}\r\n"); | ||
response.body() = visitor.str; | ||
http_cntx->Invoke(std::move(response)); | ||
} | ||
|
||
} // namespace dfly |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// Copyright 2024, DragonflyDB authors. All rights reserved. | ||
// See LICENSE for licensing terms. | ||
// | ||
|
||
#pragma once | ||
|
||
#include "util/http/http_handler.h" | ||
|
||
namespace dfly { | ||
class Service; | ||
using HttpRequest = util::HttpListenerBase::RequestType; | ||
|
||
/** | ||
* @brief The main handler function for dispatching commands via HTTP. | ||
* | ||
* @param args - query arguments. currently not used. | ||
* @param req - full http request including the body that should consist of a json array | ||
* representing a Dragonfly command. aka `["set", "foo", "bar"]` | ||
* @param service - a pointer to dfly::Service* object. | ||
* @param http_cntxt - a pointer to the http context object which provide dragonfly context | ||
* information via user_data() and allows to reply with HTTP responses. | ||
*/ | ||
void HttpAPI(const util::http::QueryArgs& args, HttpRequest&& req, Service* service, | ||
util::HttpContext* http_cntxt); | ||
|
||
} // namespace dfly |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.