Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update expensify_prod branch #1644

Merged
merged 22 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
103a86f
Start hacking everything up.
tylerkaraszewski Jan 31, 2024
55d9288
remove some cruft
tylerkaraszewski Jan 31, 2024
97aacfd
There's a lot of obsolete junk in here.
tylerkaraszewski Jan 31, 2024
f565a21
Add a default manager for deserialized https requests
tylerkaraszewski Jan 31, 2024
e213a25
We can deserialize, but not serialize.
tylerkaraszewski Jan 31, 2024
5d6831f
Add serialization.
tylerkaraszewski Jan 31, 2024
1a09647
Serialize when escalating to leader
tylerkaraszewski Jan 31, 2024
7f40da2
This might actually work.
tylerkaraszewski Jan 31, 2024
c358132
typo
tylerkaraszewski Jan 31, 2024
f11c74c
Better logline
tylerkaraszewski Jan 31, 2024
f8ecbfc
Testing changest to hopefully make auth changes small.
tylerkaraszewski Jan 31, 2024
42510fa
Remove noisy line
tylerkaraszewski Jan 31, 2024
a2f8d4d
Add base class serialization functions
tylerkaraszewski Feb 1, 2024
a4672b3
Remove extra logging
tylerkaraszewski Feb 1, 2024
f0dfa4c
Correctly set serialized data
tylerkaraszewski Feb 1, 2024
b9150b8
Remove test logging
tylerkaraszewski Feb 2, 2024
c6689c9
Fix up logic for when to serialize command data
tylerkaraszewski Feb 5, 2024
8b50e6c
Allow for a single commit count per cluster.
tylerkaraszewski Feb 6, 2024
4b4d996
Disable actually HTTPS on followers
tylerkaraszewski Feb 9, 2024
f3b44a1
Merge branch 'main' into tyler-nothing-ever-fails
tylerkaraszewski Feb 9, 2024
66e0c3d
Update sqlitecluster/SQLiteClusterMessenger.cpp
tylerkaraszewski Feb 12, 2024
77cd622
Merge pull request #1640 from Expensify/tyler-nothing-ever-fails
tylerkaraszewski Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions BedrockCommand.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#include <libstuff/libstuff.h>
#include <libstuff/SHTTPSManager.h>
#include "BedrockCommand.h"
#include "BedrockPlugin.h"

atomic<size_t> BedrockCommand::_commandCount(0);

SStandaloneHTTPSManager BedrockCommand::_noopHTTPSManager;

const string BedrockCommand::defaultPluginName("NO_PLUGIN");

BedrockCommand::BedrockCommand(SQLiteCommand&& baseCommand, BedrockPlugin* plugin, bool escalateImmediately_) :
Expand Down Expand Up @@ -303,3 +306,59 @@ void BedrockCommand::setTimeout(uint64_t timeoutDurationMS) {
bool BedrockCommand::shouldCommitEmptyTransactions() const {
return _commitEmptyTransactions;
}

void BedrockCommand::deserializeHTTPSRequests(const string& serializedHTTPSRequests) {
if (serializedHTTPSRequests.empty()) {
return;
}

list<string> requests = SParseJSONArray(serializedHTTPSRequests);
for (const string& requestStr : requests) {
STable requestMap = SParseJSONObject(requestStr);

SHTTPSManager::Transaction* httpsRequest = new SHTTPSManager::Transaction(_noopHTTPSManager, request["requestID"]);
httpsRequest->s = nullptr;
httpsRequest->created = SToUInt64(requestMap["created"]);
httpsRequest->finished = SToUInt64(requestMap["finished"]);
httpsRequest->timeoutAt = SToUInt64(requestMap["timeoutAt"]);
httpsRequest->sentTime = SToUInt64(requestMap["sentTime"]);
httpsRequest->response = SToInt(requestMap["response"]);
httpsRequest->fullRequest.deserialize(SDecodeBase64(requestMap["fullRequest"]));
httpsRequest->fullResponse.deserialize(SDecodeBase64(requestMap["fullResponse"]));

httpsRequests.push_back(httpsRequest);

// These should never be incomplete when passed with a serialized command.
if (!httpsRequest->response) {
SWARN("Received incomplete HTTPS request.");
}
}
}

string BedrockCommand::serializeHTTPSRequests() {
if (!httpsRequests.size()) {
return "";
}

list<string> requests;
for (const auto& httpsRequest : httpsRequests) {
STable data;
data["created"] = to_string(httpsRequest->created);
data["finished"] = to_string(httpsRequest->finished);
data["timeoutAt"] = to_string(httpsRequest->timeoutAt);
data["sentTime"] = to_string(httpsRequest->sentTime);
data["response"] = to_string(httpsRequest->response);
data["fullRequest"] = SEncodeBase64(httpsRequest->fullRequest.serialize());
data["fullResponse"] = SEncodeBase64(httpsRequest->fullResponse.serialize());
requests.push_back(SComposeJSONObject(data));
}

return SComposeJSONArray(requests);
}

string BedrockCommand::serializeData() const {
return "";
}

void BedrockCommand::deserializeData(const string& data) {
}
11 changes: 11 additions & 0 deletions BedrockCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ class BedrockCommand : public SQLiteCommand {
// Return the name of the plugin for this command.
const string& getName() const;

// Take all of the HTTPS requests attached to this object, and serialize them to a string.
string serializeHTTPSRequests();

// Take a serialized list of HTTPS requests as from `serializeHTTPSRequests` and deserialize them into the `httpsRequests` object.
void deserializeHTTPSRequests(const string& serializedHTTPSRequests);

// Bedrock will call this before each `processCommand` (note: not `peekCommand`) for each plugin to allow it to
// enable query rewriting. If a plugin would like to enable query rewriting, this should return true, and it should
// set the rewriteHandler it would like to use.
Expand Down Expand Up @@ -185,6 +191,9 @@ class BedrockCommand : public SQLiteCommand {
// Return the number of commands in existence.
static size_t getCommandCount() { return _commandCount.load(); }

virtual string serializeData() const;
virtual void deserializeData(const string& data);

// True if this command should be escalated immediately. This can be true for any command that does all of its work
// in `process` instead of peek, as it will always be escalated to leader
const bool escalateImmediately;
Expand Down Expand Up @@ -231,5 +240,7 @@ class BedrockCommand : public SQLiteCommand {

static atomic<size_t> _commandCount;

static SStandaloneHTTPSManager _noopHTTPSManager;

static const string defaultPluginName;
};
3 changes: 0 additions & 3 deletions BedrockPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ BedrockPlugin::BedrockPlugin(BedrockServer& s) : server(s) {
}

BedrockPlugin::~BedrockPlugin() {
for (auto httpsManager : httpsManagers) {
delete httpsManager;
}
}

bool BedrockPlugin::isValidDate(const string& date)
Expand Down
4 changes: 0 additions & 4 deletions BedrockPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ class BedrockPlugin {
// Called at some point during initiation to allow the plugin to verify/change the database schema.
virtual void upgradeDatabase(SQLite& db);

// A list of SHTTPSManagers that the plugin would like the server to watch for activity. It is only guaranteed to
// be safe to modify this list during `initialize`.
list<SHTTPSManager*> httpsManagers;

// The plugin can register any number of timers it wants. When any of them `ding`, then the `timerFired`
// function will be called, and passed the timer that is dinging.
set<SStopwatch*> timers;
Expand Down
30 changes: 17 additions & 13 deletions BedrockServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -944,19 +944,6 @@ void BedrockServer::runCommand(unique_ptr<BedrockCommand>&& _command, bool isBlo
// write it. We'll flag that here, to keep the node from falling out of LEADING/STANDINGDOWN
// until we're finished with this command.
if (command->httpsRequests.size()) {
// This *should* be impossible, but previous bugs have existed where it's feasible that we call
// `peekCommand` while leading, and by the time we're done, we're FOLLOWING, so we check just
// in case we ever introduce another similar bug.
if (state != SQLiteNodeState::LEADING && state != SQLiteNodeState::STANDINGDOWN) {
SALERT("Not leading or standing down (" << SQLiteNode::stateName(state)
<< ") but have outstanding HTTPS command: " << command->request.methodLine
<< ", returning 500.");
command->response.methodLine = "500 STANDDOWN TIMEOUT";
_reply(command);
core.rollback();
break;
}

if (command->repeek || !command->areHttpsRequestsComplete()) {
// Roll back the existing transaction, but only if we are inside an transaction
if (calledPeek) {
Expand Down Expand Up @@ -2170,8 +2157,25 @@ unique_ptr<BedrockCommand> BedrockServer::buildCommandFromRequest(SData&& reques
request["_source"] = ip;
}

// Pull any serialized https requests off the requests object to apply to the command.
string serializedHTTPSRequests = request["httpsRequests"];
string serializedData = request["serializedData"];
request.erase("httpsRequests");
request.erase("serializedData");

// Create a command.
unique_ptr<BedrockCommand> command = getCommandFromPlugins(move(request));

// Apply HTTPS requests. If we had any, pretend we peeked this command in our current (likely LEADING) state.
if (serializedHTTPSRequests.size()) {
command->deserializeHTTPSRequests(serializedHTTPSRequests);
command->lastPeekedOrProcessedInState = _syncNode->getState();
SINFO("Deserialized " << command->httpsRequests.size() << " HTTPS requests for command " << command->request.methodLine << ".");
}
if (serializedData.size()) {
command->deserializeData(serializedData);
}

SDEBUG("Deserialized command " << command->request.methodLine);
command->socket = fireAndForget ? nullptr : &socket;

Expand Down
9 changes: 4 additions & 5 deletions libstuff/SHTTPSManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@

SHTTPSManager::SHTTPSManager(BedrockPlugin& plugin_) : plugin(plugin_)
{
plugin.httpsManagers.push_back(this);
}

SHTTPSManager::SHTTPSManager(BedrockPlugin& plugin_, const string& pem, const string& srvCrt, const string& caCrt)
: SStandaloneHTTPSManager(pem, srvCrt, caCrt), plugin(plugin_)
{
plugin.httpsManagers.push_back(this);
}

void SHTTPSManager::validate() {
Expand Down Expand Up @@ -102,7 +100,7 @@ void SStandaloneHTTPSManager::postPoll(fd_map& fdm, SStandaloneHTTPSManager::Tra
// content. Why this is the what constitutes a valid response is lost to time. Any well-formed response should
// be valid here, and this should get cleaned up. However, this requires testing anything that might rely on
// the existing behavior, which is an exercise for later.
if (handleAllResponses() || SContains(transaction.fullResponse.methodLine, " 200 ") || transaction.fullResponse.content.size()) {
if (SContains(transaction.fullResponse.methodLine, " 200 ") || transaction.fullResponse.content.size()) {
// Pass the transaction down to the subclass.
_onRecv(&transaction);
} else {
Expand Down Expand Up @@ -133,16 +131,17 @@ void SStandaloneHTTPSManager::postPoll(fd_map& fdm, SStandaloneHTTPSManager::Tra
}
}

SStandaloneHTTPSManager::Transaction::Transaction(SStandaloneHTTPSManager& manager_) :
SStandaloneHTTPSManager::Transaction::Transaction(SStandaloneHTTPSManager& manager_, const string& requestID) :
s(nullptr),
created(STimeNow()),
finished(0),
timeoutAt(0),
response(0),
manager(manager_),
sentTime(0),
requestID(SThreadLogPrefix)
requestID(requestID.empty() ? SThreadLogPrefix : requestID)
{
// TODO: Remove this block to to enable HTTPS on followers. Also the `validate` method can be removed entirely.
manager.validate();
}

Expand Down
6 changes: 2 additions & 4 deletions libstuff/SHTTPSManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SStandaloneHTTPSManager : public STCPManager {
public:
struct Transaction {
// Constructor/Destructor
Transaction(SStandaloneHTTPSManager& manager_);
Transaction(SStandaloneHTTPSManager& manager_, const string& requestID = "");
~Transaction();

// Attributes
Expand Down Expand Up @@ -60,16 +60,14 @@ class SStandaloneHTTPSManager : public STCPManager {
Transaction* _httpsSend(const string& url, const SData& request);
Transaction* _createErrorTransaction();
virtual bool _onRecv(Transaction* transaction);

// Historically we only call _onRecv for `200 OK` responses. This allows manangers to handle all responses.
virtual bool handleAllResponses() { return false; }
};

class SHTTPSManager : public SStandaloneHTTPSManager {
public:
SHTTPSManager(BedrockPlugin& plugin_);
SHTTPSManager(BedrockPlugin& plugin_, const string& pem, const string& srvCrt, const string& caCrt);

// TODO: Remove this once Auth no longer checks for it.
class NotLeading : public exception {
const char* what() {
return "Can't create SHTTPSManager::Transaction when not leading";
Expand Down
1 change: 0 additions & 1 deletion libstuff/libstuff.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1460,7 +1460,6 @@ const char* _SParseJSONObject(const char* ptr, const char* end, STable& out, boo
_JSONASSERTPTR(); // Make sure no parse error.
if (populateOut) {
// Got one more
SDEBUG("Parsed: '" << name << "':'" << value << "'");
out[name] = value;
}
_JSONLOG();
Expand Down
11 changes: 11 additions & 0 deletions sqlitecluster/SQLiteClusterMessenger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ bool SQLiteClusterMessenger::_sendCommandOnSocket(SHTTPSManager::Socket& socket,

// This is what we need to send.
SData request = command.request;

// If the command has https requests, we serialize them to escalate. In this case we also check if the command has data
// that needs serialization, and if so, we serialize that as well.
if (command.httpsRequests.size()) {
request["httpsRequests"] = command.serializeHTTPSRequests();
string serializedData = command.serializeData();
if (serializedData.size()) {
request["serializedData"] = move(serializedData);
}
}

request.nameValueMap["ID"] = command.id;
SFastBuffer buf(request.serialize());

Expand Down
4 changes: 0 additions & 4 deletions test/clustertest/testplugin/TestPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,6 @@ bool TestPluginCommand::peek(SQLite& db) {
response["stored_not_special"] = plugin().arbitraryData["not_special"];
return true;
} else if (SStartsWith(request.methodLine, "sendrequest")) {
if (plugin().server.getState() != SQLiteNodeState::LEADING && plugin().server.getState() != SQLiteNodeState::STANDINGDOWN) {
// Only start HTTPS requests on leader, otherwise, we'll escalate.
return false;
}
int requestCount = 1;
if (request.isSet("httpsRequestCount")) {
requestCount = max(request.calc("httpsRequestCount"), 1);
Expand Down
8 changes: 4 additions & 4 deletions test/clustertest/tests/HTTPSTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct HTTPSTest : tpunit::TestFixture {
}

void testMultipleRequests() {
BedrockTester& brtester = tester->getTester(0);
BedrockTester& brtester = tester->getTester(1);
SData request("sendrequest");
request["httpsRequestCount"] = to_string(3);
auto result = brtester.executeWaitVerifyContent(request);
Expand All @@ -45,7 +45,7 @@ struct HTTPSTest : tpunit::TestFixture {

void test() {
// Send one request to verify that it works.
BedrockTester& brtester = tester->getTester(0);
BedrockTester& brtester = tester->getTester(1);

SData request("sendrequest a");
auto result = brtester.executeWaitVerifyContent(request);
Expand All @@ -56,7 +56,7 @@ struct HTTPSTest : tpunit::TestFixture {
// to cause a conflict.
mutex m;

// Every 10th request on leader is an HTTP request.
// Every 10th request is an HTTP request.
int nthHasRequest = 10;

// Let's spin up three threads, each spamming commands at one of our nodes.
Expand All @@ -66,7 +66,7 @@ struct HTTPSTest : tpunit::TestFixture {
threads.emplace_back([this, i, nthHasRequest, &responses, &m](){
vector<SData> requests;
for (int j = 0; j < 200; j++) {
if (i == 0 && (j % nthHasRequest == 0)) {
if (j % nthHasRequest == 0) {
// They should throw all sorts of errors if they repeat HTTPS requests.
SData request("sendrequest b");
request["writeConsistency"] = "ASYNC";
Expand Down