Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
1 change: 1 addition & 0 deletions envoy_build_config/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ EXTENSIONS = {
"envoy.filters.http.router": "//source/extensions/filters/http/router:config",
"envoy.filters.network.http_connection_manager": "//source/extensions/filters/network/http_connection_manager:config",
"envoy.http.original_ip_detection.xff": "//source/extensions/http/original_ip_detection/xff:config",
"envoy.key_value.platform": "@envoy_mobile//library/common/extensions/key_value/platform:config",
"envoy.network.dns_resolver.apple": "//source/extensions/network/dns_resolver/apple:config",
"envoy.retry.options.network_configuration": "@envoy_mobile//library/common/extensions/retry/options/network_configuration:config",
"envoy.stat_sinks.metrics_service": "//source/extensions/stat_sinks/metrics_service:config",
Expand Down
31 changes: 31 additions & 0 deletions library/common/extensions/key_value/platform/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
load(
"@envoy//bazel:envoy_build_system.bzl",
"envoy_cc_extension",
"envoy_extension_package",
"envoy_proto_library",
)

licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_proto_library(
name = "platform",
srcs = ["platform.proto"],
)

envoy_cc_extension(
name = "config",
srcs = ["config.cc"],
hdrs = ["config.h"],
repository = "@envoy",
deps = [
":platform_cc_proto",
"@envoy//envoy/common:key_value_store_interface",
"@envoy//envoy/event:dispatcher_interface",
"@envoy//envoy/filesystem:filesystem_interface",
"@envoy//envoy/registry",
"@envoy//source/common/common:key_value_store_lib",
"@envoy_api//envoy/config/common/key_value/v3:pkg_cc_proto",
],
)
52 changes: 52 additions & 0 deletions library/common/extensions/key_value/platform/config.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#include "library/common/extensions/key_value/platform/config.h"

#include "envoy/config/common/key_value/v3/config.pb.h"
#include "envoy/config/common/key_value/v3/config.pb.validate.h"
#include "envoy/registry/registry.h"

namespace Envoy {
namespace Extensions {
namespace KeyValue {

PlatformKeyValueStore::PlatformKeyValueStore(Event::Dispatcher& dispatcher,
std::chrono::milliseconds save_interval,
PlatformInterface& platform_interface,
const std::string& key)
: KeyValueStoreBase(dispatcher, save_interval), platform_interface_(platform_interface),
key_(key) {
const std::string contents = platform_interface.read(key);
if (!parseContents(contents, store_)) {
ENVOY_LOG(warn, "Failed to parse key value store contents {}", key);
}
}

void PlatformKeyValueStore::flush() {
std::string output;
for (const auto& it : store_) {
absl::StrAppend(&output, it.first.length(), "\n", it.first, it.second.length(), "\n",
it.second);
}
platform_interface_.save(key_, output);
}

KeyValueStorePtr
PlatformKeyValueStoreFactory::createStore(const Protobuf::Message& config,
ProtobufMessage::ValidationVisitor& validation_visitor,
Event::Dispatcher& dispatcher, Filesystem::Instance&) {
const auto& typed_config = MessageUtil::downcastAndValidate<
const ::envoy::config::common::key_value::v3::KeyValueStoreConfig&>(config,
validation_visitor);
const auto file_config = MessageUtil::anyConvertAndValidate<
envoymobile::extensions::key_value::platform::PlatformKeyValueStoreConfig>(
typed_config.config().typed_config(), validation_visitor);
auto milliseconds =
std::chrono::milliseconds(DurationUtil::durationToMilliseconds(file_config.save_interval()));
return std::make_unique<PlatformKeyValueStore>(
dispatcher, milliseconds, platform_interface_.value().get(), file_config.key());
}

REGISTER_FACTORY(PlatformKeyValueStoreFactory, KeyValueStoreFactory);

} // namespace KeyValue
} // namespace Extensions
} // namespace Envoy
64 changes: 64 additions & 0 deletions library/common/extensions/key_value/platform/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "envoy/common/key_value_store.h"

#include "source/common/common/key_value_store_base.h"

#include "library/common/extensions/key_value/platform/platform.pb.h"
#include "library/common/extensions/key_value/platform/platform.pb.validate.h"

namespace Envoy {
namespace Extensions {
namespace KeyValue {

class PlatformInterface {
public:
virtual ~PlatformInterface() {}
// Save the contents to the key provided. This may be done asynchronously.
virtual void save(const std::string& key, const std::string& contents) PURE;
// Read the contents of the key provided.
virtual std::string read(const std::string& key) const PURE;
};

// A platform file based key value store, which reads from and saves from based on
// a key. An example implementation would be flushing to the android prefs file.
//
// All keys and values are flushed to a single entry as
// [length]\n[key][length]\n[value]
class PlatformKeyValueStore : public KeyValueStoreBase {
public:
PlatformKeyValueStore(Event::Dispatcher& dispatcher, std::chrono::milliseconds save_interval,
PlatformInterface& platform_interface, const std::string& key);
// KeyValueStore
void flush() override;

private:
PlatformInterface& platform_interface_;
const std::string key_;
};

class PlatformKeyValueStoreFactory : public KeyValueStoreFactory {
public:
PlatformKeyValueStoreFactory() {}
PlatformKeyValueStoreFactory(PlatformInterface& platform_interface)
: platform_interface_(platform_interface) {}

// KeyValueStoreFactory
KeyValueStorePtr createStore(const Protobuf::Message& config,
ProtobufMessage::ValidationVisitor& validation_visitor,
Event::Dispatcher& dispatcher,
Filesystem::Instance& file_system) override;

// TypedFactory
ProtobufTypes::MessagePtr createEmptyConfigProto() override {
return ProtobufTypes::MessagePtr{
new envoymobile::extensions::key_value::platform::PlatformKeyValueStoreConfig()};
}

std::string name() const override { return "envoy.key_value.platform"; }
// TODO(alyssawilk, goaway) the default PlatformInterface should do up calls through Java and this
// can be moved to a non-optional reference.
OptRef<PlatformInterface> platform_interface_;
};

} // namespace KeyValue
} // namespace Extensions
} // namespace Envoy
15 changes: 15 additions & 0 deletions library/common/extensions/key_value/platform/platform.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
syntax = "proto3";

package envoymobile.extensions.key_value.platform;

import "google/protobuf/duration.proto";

import "validate/validate.proto";

message PlatformKeyValueStoreConfig {
// The key to save the contents to.
string key = 1 [(validate.rules).string = {min_len: 1}];

// The interval at which the key value store should be saved.
google.protobuf.Duration save_interval = 2;
}
22 changes: 22 additions & 0 deletions test/common/extensions/key_value/platform/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load("@envoy//bazel:envoy_build_system.bzl", "envoy_package")
load(
"@envoy//test/extensions:extensions_build_system.bzl",
"envoy_extension_cc_test",
)

licenses(["notice"]) # Apache 2

envoy_package()

envoy_extension_cc_test(
name = "platform_store_test",
srcs = ["platform_store_test.cc"],
extension_names = ["envoy.key_value.platform"],
repository = "@envoy",
deps = [
"//library/common/extensions/key_value/platform:config",
"@envoy//source/common/common:key_value_store_lib",
"@envoy//test/mocks/event:event_mocks",
"@envoy//test/test_common:file_system_for_test_lib",
],
)
120 changes: 120 additions & 0 deletions test/common/extensions/key_value/platform/platform_store_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#include <memory>
#include <string>

#include "test/mocks/event/mocks.h"

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "library/common/extensions/key_value/platform/config.h"
#include "library/common/extensions/key_value/platform/platform.pb.h"

using testing::NiceMock;

namespace Envoy {
namespace Extensions {
namespace KeyValue {
namespace {

class TestPlatformInterface : public PlatformInterface {
virtual void save(const std::string& key, const std::string& contents) {
store_.erase(key);
store_.emplace(key, contents);
}
virtual std::string read(const std::string& key) const {
auto it = store_.find(key);
if (it == store_.end()) {
return "";
}
return it->second;
}

absl::flat_hash_map<std::string, std::string> store_;
};

class PlatformStoreTest : public testing::Test {
protected:
PlatformStoreTest() { createStore(); }

void createStore() {
flush_timer_ = new NiceMock<Event::MockTimer>(&dispatcher_);
store_ =
std::make_unique<PlatformKeyValueStore>(dispatcher_, save_interval_, mock_platform_, key_);
}
NiceMock<Event::MockDispatcher> dispatcher_;
std::string key_{"key"};
std::unique_ptr<PlatformKeyValueStore> store_{};
std::chrono::seconds save_interval_{5};
Event::MockTimer* flush_timer_ = nullptr;
TestPlatformInterface mock_platform_;
};

TEST_F(PlatformStoreTest, Basic) {
EXPECT_EQ(absl::nullopt, store_->get("foo"));
store_->addOrUpdate("foo", "bar");
EXPECT_EQ("bar", store_->get("foo").value());
store_->addOrUpdate("foo", "eep");
EXPECT_EQ("eep", store_->get("foo").value());
store_->remove("foo");
EXPECT_EQ(absl::nullopt, store_->get("foo"));
}

TEST_F(PlatformStoreTest, Persist) {
store_->addOrUpdate("foo", "bar");
store_->addOrUpdate("by\nz", "ee\np");
ASSERT_TRUE(flush_timer_->enabled_);
flush_timer_->invokeCallback(); // flush
EXPECT_TRUE(flush_timer_->enabled_);
// Not flushed as 5ms didn't pass.
store_->addOrUpdate("baz", "eep");

save_interval_ = std::chrono::seconds(0);
createStore();
KeyValueStore::ConstIterateCb validate = [](const std::string& key, const std::string&) {
EXPECT_TRUE(key == "foo" || key == "by\nz");
return KeyValueStore::Iterate::Continue;
};

EXPECT_EQ("bar", store_->get("foo").value());
EXPECT_EQ("ee\np", store_->get("by\nz").value());
EXPECT_FALSE(store_->get("baz").has_value());
store_->iterate(validate);

// This will flush due to 0ms flush interval
store_->addOrUpdate("baz", "eep");
createStore();
EXPECT_TRUE(store_->get("baz").has_value());

// This will flush due to 0ms flush interval
store_->remove("bar");
createStore();
EXPECT_FALSE(store_->get("bar").has_value());
}

TEST_F(PlatformStoreTest, Iterate) {
store_->addOrUpdate("foo", "bar");
store_->addOrUpdate("baz", "eep");

int full_counter = 0;
KeyValueStore::ConstIterateCb validate = [&full_counter](const std::string& key,
const std::string&) {
++full_counter;
EXPECT_TRUE(key == "foo" || key == "baz");
return KeyValueStore::Iterate::Continue;
};
store_->iterate(validate);
EXPECT_EQ(2, full_counter);

int stop_early_counter = 0;
KeyValueStore::ConstIterateCb stop_early = [&stop_early_counter](const std::string&,
const std::string&) {
++stop_early_counter;
return KeyValueStore::Iterate::Break;
};
store_->iterate(stop_early);
EXPECT_EQ(1, stop_early_counter);
}

} // namespace
} // namespace KeyValue
} // namespace Extensions
} // namespace Envoy