Skip to content

Commit

Permalink
Merge pull request #101
Browse files Browse the repository at this point in the history
ReBAC: `graph` strategy for checking relations
  • Loading branch information
uatuko authored May 18, 2024
2 parents 0eb3099 + 15904cf commit 323e266
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 8 deletions.
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 @@ rpcCheck::result_type Impl::call<rpcCheck>(
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 @@ rpcCheck::result_type Impl::call<rpcCheck>(
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;
}

// 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;
}

default:
break;
}
}

Expand Down Expand Up @@ -322,6 +356,109 @@ google::rpc::Status Impl::exception() noexcept {
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

0 comments on commit 323e266

Please sign in to comment.