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

ReBAC: graph strategy for checking relations #101

Merged
merged 8 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,4 @@ run: $(binary)
$(binary) -4 127.0.0.1

test: $(binary)
ctest --test-dir $(builddir) --output-on-failure
ctest --test-dir $(builddir) --output-on-failure $(filter-out $@,$(MAKECMDGOALS))
94 changes: 94 additions & 0 deletions bench/relations_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,100 @@ class bm_relations : public benchmark::Fixture {
void TearDown(benchmark::State &state) { db::testing::teardown(); }
};

// Benchmark check relations with graph strategy with different depth and breadth values.
BENCHMARK_DEFINE_F(bm_relations, check_graph)(benchmark::State &st) {
db::Tuple start({
.lEntityId = "bm_relations.check_set",
.lEntityType = "user",
.relation = "member",
.rEntityId = xid::next(),
.rEntityType = "bm_relations.check_graph",
});
start.store();

db::Tuple last({
.lEntityId = start.rEntityId(),
.lEntityType = start.rEntityType(),
.relation = "member",
.rEntityId = xid::next(),
.rEntityType = "bm_relations.check_graph",
.strand = start.relation(),
});
last.store();

for (auto n = st.range(0); n > 0; n--) {
// Add depth
db::Tuple tuple({
.lEntityId = last.rEntityId(),
.lEntityType = last.rEntityType(),
.relation = "member",
.rEntityId = xid::next(),
.rEntityType = "bm_relations.check_graph",
.strand = last.relation(),
});
tuple.store();

for (auto m = st.range(1); m > 0; m--) {
// Add breadth
db::Tuple tuple({
.lEntityId = last.rEntityId(),
.lEntityType = last.rEntityType(),
.relation = "reader",
.rEntityId = xid::next(),
.rEntityType = "bm_relations.check_graph",
.strand = last.relation(),
});
tuple.store();
}

last = tuple;
}

grpcxx::context ctx;
svc::Relations svc;

rpcCheck::request_type request;
request.set_strategy(static_cast<std::uint32_t>(svc::common::strategy_t::graph));
request.set_cost_limit(std::numeric_limits<std::uint16_t>::max());

auto *left = request.mutable_left_entity();
left->set_id(start.lEntityId());
left->set_type(start.lEntityType());

request.set_relation(last.relation());

auto *right = request.mutable_right_entity();
right->set_id(last.rEntityId());
right->set_type(last.rEntityType());

std::size_t ops = 0;
std::size_t cost = 0;

rpcCheck::result_type result;

for (auto _ : st) {
st.PauseTiming();
ops++;
st.ResumeTiming();

result = svc.call<rpcCheck>(ctx, request);
if (result.response->found() != true) {
st.SkipWithError("[error] Traversal failed!");
}

st.PauseTiming();
cost += result.response->cost();
st.ResumeTiming();
}

st.counters.insert({
{"ops", benchmark::Counter(ops, benchmark::Counter::kIsRate)},
{"vertices", benchmark::Counter(cost, benchmark::Counter::kIsRate)},
});
}
BENCHMARK_REGISTER_F(bm_relations, check_graph)
->ArgsProduct({{4, 8, 32}, benchmark::CreateRange(128, 2 << 10, 8)});

// Benchmark check relations with set strategy.
// Different numbers of tuple "sets" are generated and linked randomly. e.g.
// strand | l_entity_id | relation | r_entity_id
Expand Down
5 changes: 5 additions & 0 deletions proto/sentium/api/v1/relations.proto
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ message RelationsCheckRequest {
//
// Strategies:
// 2 (direct) - Only check if there's a direct relation exists between the entities.
// 4 (graph) - If a direct relation cannot be found between the entities, use a graph traversal
// algorithm to derive a relation.
// 8 (set) - Check if there's a direct relation exists between the entities and if not, use
// a set intersection algorithm to derive a relation between the entities.
optional uint32 strategy = 6;
Expand All @@ -104,6 +106,9 @@ message RelationsCheckResponse {
// Tuple containing relation data that matched the query. An empty tuple `id` indicates a computed
// tuple which isn't stored.
optional Tuple tuple = 3;

// Path that derived the relation between entities when using the `graph` lookup strategy.
repeated Tuple path = 4;
}

message RelationsCreateRequest {
Expand Down
151 changes: 144 additions & 7 deletions src/svc/relations.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "relations.h"

#include <unordered_set>

#include <google/protobuf/util/json_util.h>
#include <google/rpc/code.pb.h>

Expand All @@ -21,6 +23,9 @@
case common::strategy_t::direct:
strategy = common::strategy_t::direct;
break;
case common::strategy_t::graph:
strategy = common::strategy_t::graph;
break;
case common::strategy_t::set:
strategy = common::strategy_t::set;
break;
Expand Down Expand Up @@ -65,14 +70,43 @@
return {grpcxx::status::code_t::ok, response};
}

// Set strategy
if (cost < limit && common::strategy_t::set == strategy) {
auto r = spot(ctx.meta(common::space_id_v), left, req.relation(), right, limit);
if (cost < limit) {
switch (strategy) {

// Graph strategy
case common::strategy_t::graph: {
auto r = graph(ctx.meta(common::space_id_v), left, req.relation(), right, limit);

cost += r.cost;
if (!r.path.empty()) {
response.set_found(true);

auto *path = response.mutable_path();
path->Reserve(r.path.size());
while (!r.path.empty()) {
map(r.path.front(), path->Add());
r.path.pop();
}
}

cost += r.cost;
if (r.tuple) {
response.set_found(true);
map(*r.tuple, response.mutable_tuple());
break;
}

Check warning on line 93 in src/svc/relations.cpp

View check run for this annotation

Codecov / codecov/patch

src/svc/relations.cpp#L93

Added line #L93 was not covered by tests

// Set strategy
case common::strategy_t::set: {
auto r = spot(ctx.meta(common::space_id_v), left, req.relation(), right, limit);

cost += r.cost;
if (r.tuple) {
response.set_found(true);
map(*r.tuple, response.mutable_tuple());
}

break;
}

Check warning on line 106 in src/svc/relations.cpp

View check run for this annotation

Codecov / codecov/patch

src/svc/relations.cpp#L106

Added line #L106 was not covered by tests

default:
break;
}
}

Expand Down Expand Up @@ -322,6 +356,109 @@
return status;
}

Impl::graph_t Impl::graph(
std::string_view spaceId, db::Tuple::Entity left, std::string_view relation,
db::Tuple::Entity right, std::uint16_t limit) const {

class vertex_t {
public:
using path_t = std::queue<db::Tuple>;

struct hasher {
void combine(std::size_t &seed, const std::string &v) const noexcept {
seed ^= std::hash<std::string>()(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

std::size_t operator()(const vertex_t &v) const noexcept {
std::size_t seed = 0;
combine(seed, v.relation());
combine(seed, v.entityType());
combine(seed, v.entityId());

return seed;
}
};

vertex_t(vertex_t &&) = default;

// Copy constructor is _only_ used when keeping track of visited vertices. In order to
// _potentially_ save memory `_path` is ignored.
vertex_t(const vertex_t &v) noexcept :
_entityId(v._entityId), _entityType(v._entityType), _relation(v._relation){};

vertex_t(db::Tuple &&t) :
_entityId(t.rEntityId()), _entityType(t.rEntityType()), _relation(t.relation()) {
_path.push(std::move(t));
}

vertex_t(const vertex_t &v, db::Tuple &&t) :
_entityId(t.rEntityId()), _entityType(t.rEntityType()), _path(v._path),
_relation(t.relation()) {
_path.push(std::move(t));
}

bool operator==(const vertex_t &rhs) const noexcept {
return (
_entityId == rhs._entityId && _entityType == rhs._entityType &&
_relation == rhs._relation);
}

const std::string &entityId() const noexcept { return _entityId; }
const std::string &entityType() const noexcept { return _entityType; }
const std::string &relation() const noexcept { return _relation; }

path_t &path() noexcept { return _path; }

private:
std::string _entityId;
std::string _entityType;
path_t _path;
std::string _relation;
};

std::int32_t cost = 0;
std::queue<vertex_t> queue;

// Assume there's no direct relation between left and right entities to begin with
{
auto tuples = db::ListTuplesRight(spaceId, left, {}, {}, limit);
for (auto &t : tuples) {
queue.emplace(std::move(t));
}
}

// Keep track of visited vertices to avoid circular lookups
std::unordered_set<vertex_t, vertex_t::hasher> visited;

while (!queue.empty() && cost++ < limit) {
auto v = std::move(queue.front());
queue.pop();

if (visited.contains(v)) {
continue;
}

visited.insert(v);
for (auto &t :
db::ListTuplesRight(spaceId, {v.entityType(), v.entityId()}, {}, {}, limit)) {
if (v.relation() != t.strand()) {
continue;
}

if (t.relation() == relation && t.rEntityId() == right.id() &&
t.rEntityType() == right.type()) {
// Found
v.path().push(std::move(t));
return {cost, v.path()};
}

queue.emplace(v, std::move(t));
}
}

return {cost, {}};
}

db::Tuple Impl::map(
const grpcxx::context &ctx, const rpcCreate::request_type &from) const noexcept {
db::Tuple to({
Expand Down
14 changes: 14 additions & 0 deletions src/svc/relations.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#pragma once
#include <optional>
#include <queue>
#include <string_view>

#include <google/rpc/status.pb.h>

#include "db/tuples.h"
Expand Down Expand Up @@ -39,6 +43,11 @@ class Impl {
google::rpc::Status exception() noexcept;

private:
struct graph_t {
std::int32_t cost;
std::queue<db::Tuple> path;
};

struct spot_t {
std::int32_t cost;
std::optional<db::Tuple> tuple;
Expand All @@ -53,6 +62,11 @@ class Impl {
const db::Tuples &from,
google::protobuf::RepeatedPtrField<sentium::api::v1::Tuple> *to) const noexcept;

// Check for a relation between left and right entities using the `graph` algorithm.
graph_t graph(
std::string_view spaceId, db::Tuple::Entity left, std::string_view relation,
db::Tuple::Entity right, std::uint16_t limit) const;

// Check for a relation between left and right entities using the `spot` algorithm.
spot_t spot(
std::string_view spaceId, db::Tuple::Entity left, std::string_view relation,
Expand Down
Loading
Loading