mirror of https://github.com/processone/mqtree

commit
0c515cc1dd
8 changed files with 1127 additions and 0 deletions
@ -0,0 +1,70 @@
|
||||
REBAR=./rebar
|
||||
|
||||
all: src |
||||
|
||||
src: |
||||
$(REBAR) get-deps compile
|
||||
|
||||
clean: |
||||
$(REBAR) clean
|
||||
|
||||
distclean: clean |
||||
rm -f config.status
|
||||
rm -f config.log
|
||||
rm -rf autom4te.cache
|
||||
rm -rf deps
|
||||
rm -rf ebin
|
||||
rm -rf priv
|
||||
rm -f vars.config
|
||||
rm -f compile_commands.json
|
||||
rm -rf dialyzer
|
||||
|
||||
test: all |
||||
mkdir -p .eunit/priv/lib
|
||||
cp priv/lib/mqtree.* .eunit/priv/lib/
|
||||
$(REBAR) -v skip_deps=true eunit
|
||||
|
||||
xref: all |
||||
$(REBAR) skip_deps=true xref
|
||||
|
||||
deps := $(wildcard deps/*/ebin)
|
||||
|
||||
dialyzer/erlang.plt: |
||||
@mkdir -p dialyzer
|
||||
@dialyzer --build_plt --output_plt dialyzer/erlang.plt \
|
||||
-o dialyzer/erlang.log --apps kernel stdlib erts; \
|
||||
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
|
||||
|
||||
dialyzer/deps.plt: |
||||
@mkdir -p dialyzer
|
||||
@dialyzer --build_plt --output_plt dialyzer/deps.plt \
|
||||
-o dialyzer/deps.log $(deps); \
|
||||
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
|
||||
|
||||
dialyzer/mqtree.plt: |
||||
@mkdir -p dialyzer
|
||||
@dialyzer --build_plt --output_plt dialyzer/mqtree.plt \
|
||||
-o dialyzer/ejabberd.log ebin; \
|
||||
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
|
||||
|
||||
erlang_plt: dialyzer/erlang.plt |
||||
@dialyzer --plt dialyzer/erlang.plt --check_plt -o dialyzer/erlang.log; \
|
||||
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
|
||||
|
||||
deps_plt: dialyzer/deps.plt |
||||
@dialyzer --plt dialyzer/deps.plt --check_plt -o dialyzer/deps.log; \
|
||||
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
|
||||
|
||||
mqtree_plt: dialyzer/mqtree.plt |
||||
@dialyzer --plt dialyzer/mqtree.plt --check_plt -o dialyzer/ejabberd.log; \
|
||||
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
|
||||
|
||||
dialyzer: erlang_plt deps_plt mqtree_plt |
||||
@dialyzer --plts dialyzer/*.plt --no_check_plt \
|
||||
--get_warnings -o dialyzer/error.log ebin; \
|
||||
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
|
||||
|
||||
check-syntax: |
||||
gcc -o nul -S ${CHK_SOURCES}
|
||||
|
||||
.PHONY: clean src test all dialyzer erlang_plt deps_plt mqtree_plt |
@ -0,0 +1,95 @@
|
||||
mqtree: Index tree for MQTT topic filters |
||||
==================================================== |
||||
|
||||
mqtree is an Erlang NIF implementation of N-ary tree to keep MQTT |
||||
topic filters for efficient matching. |
||||
|
||||
# System requirements |
||||
|
||||
To compile mqtree you need: |
||||
|
||||
- GNU Make. |
||||
- GCC. |
||||
- Erlang/OTP 17.5 or higher. |
||||
|
||||
# Compiling |
||||
|
||||
``` |
||||
$ git clone https://github.com/processone/mqtree.git |
||||
$ cd mqtree |
||||
$ make |
||||
``` |
||||
|
||||
# API |
||||
|
||||
## new/0 |
||||
```erlang |
||||
-spec new() -> tree(). |
||||
``` |
||||
Creates new tree. Note that the tree is mutable just like ETS. |
||||
The created tree gets destroyed when it's garbage collected. |
||||
|
||||
## insert/2 |
||||
```erlang |
||||
-spec insert(Tree :: tree(), Filter :: iodata()) -> ok. |
||||
``` |
||||
Inserts `Filter` into `Tree` and increases its reference counter. |
||||
The reference counter is increased every time when the same |
||||
filter is inserted into the tree. The reference counter is decreased |
||||
when the filter is deleted, see [delete/2](#delete2). |
||||
|
||||
**NOTE**: no checks are performed on the filter being inserted: |
||||
it's up to the caller to check if the filter conforms to the MQTT |
||||
specification. |
||||
|
||||
## delete/2 |
||||
```erlang |
||||
-spec delete(Tree :: tree(), Filter :: iodata()) -> ok. |
||||
``` |
||||
Deletes `Filter` from `Tree` and decreases its reference counter. |
||||
Nothing is done if the filter is not found in the tree. |
||||
|
||||
**NOTE**: no checks are performed on the filter being deleted: |
||||
it's up to the caller to check if the filter conforms to the MQTT |
||||
specification. |
||||
|
||||
## match/2 |
||||
```erlang |
||||
-spec match(Tree :: tree(), Path :: iodata()) -> [binary()]. |
||||
``` |
||||
Finds filters in `Tree` matching `Path` according to the MQTT |
||||
specification. |
||||
|
||||
**NOTE**: no checks are performed on the path being matched: |
||||
it's up to the caller to check if the path conforms to the MQTT |
||||
specification. |
||||
|
||||
## refc/2 |
||||
```erlang |
||||
-spec refc(Tree :: tree(), Filter :: iodata()) -> non_neg_intger(). |
||||
``` |
||||
Returns the reference counter of `Filter` in `Tree`. In particular, |
||||
zero (0) is returned if the filter is not found in the tree. |
||||
|
||||
**NOTE**: no checks are performed on the filter being searched: |
||||
it's up to the caller to check if the filter conforms to the MQTT |
||||
specification. |
||||
|
||||
## clear/1 |
||||
```erlang |
||||
-spec clear(Tree :: tree()) -> ok. |
||||
``` |
||||
Deletes all filters from `Tree`. |
||||
|
||||
## size/1 |
||||
```erlang |
||||
-spec size(Tree :: tree()) -> ok. |
||||
``` |
||||
Returns the size of `Tree`. That is, the number of filters in the |
||||
tree (irrespective of their reference counters). |
||||
|
||||
## is_empty/1 |
||||
```erlang |
||||
-spec is_empty(Tree :: tree()) -> boolean(). |
||||
``` |
||||
Returns `true` if `Tree` holds no filters. Returns `false` otherwise. |
@ -0,0 +1,532 @@
|
||||
/*
|
||||
* ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne |
||||
* |
||||
* The ejabberd software is the exclusive property of the licensor |
||||
* ProcessOne. It is protected by the law on copyright and |
||||
* international conventions. As a result, the dealer |
||||
* recognizes that will make every effort to ensure the confidentiality |
||||
* on the software. It is recalled that a violation of the rights of |
||||
* authors of the software is an infringement and that any |
||||
* counterfeit is punishable in France by Article L339-2 of the Code of |
||||
* Intellectual property and punishable by three years imprisonment and |
||||
* 300000 euros. |
||||
* |
||||
* Any infringement liable to be so qualified and |
||||
* would be caused by third parties and whose dealer has knowledge |
||||
* should be terminated by the licensor that it will make its case |
||||
* personal conduct of the proceedings. Any allegation of infringement |
||||
* formed against the dealer because of the use of the Software will |
||||
* be brought to the knowledge of the licensor which will assist |
||||
* in defense of the dealer in the manner and form that |
||||
* see fit and fix alone. |
||||
* |
||||
*/ |
||||
|
||||
#include <erl_nif.h> |
||||
#include <stdio.h> |
||||
#include <errno.h> |
||||
#include "uthash.h" |
||||
|
||||
/****************************************************************
|
||||
* Structures/Globals definitions * |
||||
****************************************************************/ |
||||
typedef struct __tree_t { |
||||
char *key; |
||||
char *val; |
||||
int refc; |
||||
struct __tree_t *sub; |
||||
UT_hash_handle hh; |
||||
} tree_t; |
||||
|
||||
typedef struct { |
||||
tree_t *tree; |
||||
ErlNifRWLock *lock; |
||||
} state_t; |
||||
|
||||
static ErlNifResourceType *tree_state_t = NULL; |
||||
|
||||
/****************************************************************
|
||||
* MQTT Tree Manipulation * |
||||
****************************************************************/ |
||||
tree_t *tree_new(char *key, size_t len) { |
||||
tree_t *tree = malloc(sizeof(tree_t)); |
||||
if (tree) { |
||||
memset(tree, 0, sizeof(tree_t)); |
||||
if (key && len) { |
||||
tree->key = malloc(len); |
||||
if (tree->key) { |
||||
memcpy(tree->key, key, len); |
||||
} else { |
||||
free(tree); |
||||
tree = NULL; |
||||
} |
||||
} |
||||
} |
||||
return tree; |
||||
} |
||||
|
||||
void tree_free(tree_t *t) { |
||||
tree_t *found, *iter; |
||||
if (t) { |
||||
free(t->key); |
||||
free(t->val); |
||||
HASH_ITER(hh, t->sub, found, iter) { |
||||
HASH_DEL(t->sub, found); |
||||
tree_free(found); |
||||
} |
||||
memset(t, 0, sizeof(tree_t)); |
||||
free(t); |
||||
} |
||||
} |
||||
|
||||
void tree_clear(tree_t *root) { |
||||
tree_t *found, *iter; |
||||
HASH_ITER(hh, root->sub, found, iter) { |
||||
HASH_DEL(root->sub, found); |
||||
tree_free(found); |
||||
} |
||||
} |
||||
|
||||
int tree_add(tree_t *root, char *path, size_t size) { |
||||
int i = 0; |
||||
size_t len; |
||||
tree_t *t = root; |
||||
tree_t *found, *new; |
||||
|
||||
while (i<=size) { |
||||
len = strlen(path+i) + 1; |
||||
HASH_FIND_STR(t->sub, path+i, found); |
||||
if (found) { |
||||
i += len; |
||||
t = found; |
||||
} else { |
||||
new = tree_new(path+i, len); |
||||
if (new) { |
||||
HASH_ADD_STR(t->sub, key, new); |
||||
i += len; |
||||
t = new; |
||||
} else |
||||
return errno; |
||||
} |
||||
} |
||||
|
||||
if (t->val) { |
||||
free(path); |
||||
} else { |
||||
for (i=0; i<size; i++) { |
||||
if (!path[i]) |
||||
path[i] = '/'; |
||||
} |
||||
t->val = path; |
||||
} |
||||
t->refc++; |
||||
return 0; |
||||
} |
||||
|
||||
int tree_del(tree_t *root, char *path, size_t i, size_t size) { |
||||
tree_t *found; |
||||
|
||||
if (i<=size) { |
||||
HASH_FIND_STR(root->sub, path+i, found); |
||||
if (found) { |
||||
i += strlen(path+i) + 1; |
||||
int deleted = tree_del(found, path, i, size); |
||||
if (deleted) { |
||||
HASH_DEL(root->sub, found); |
||||
tree_free(found); |
||||
} |
||||
} |
||||
} else if (root->refc) { |
||||
root->refc--; |
||||
if (!root->refc) { |
||||
free(root->val); |
||||
root->val = NULL; |
||||
} |
||||
} |
||||
|
||||
return !root->refc && !root->sub; |
||||
} |
||||
|
||||
void tree_size(tree_t *tree, size_t *size) { |
||||
tree_t *found, *iter; |
||||
|
||||
HASH_ITER(hh, tree->sub, found, iter) { |
||||
if (found->refc) (*size)++; |
||||
tree_size(found, size); |
||||
} |
||||
} |
||||
|
||||
int tree_refc(tree_t *tree, char *path, size_t i, size_t size) { |
||||
tree_t *found; |
||||
|
||||
if (i<=size) { |
||||
HASH_FIND_STR(tree->sub, path+i, found); |
||||
if (found) { |
||||
i += strlen(path+i) + 1; |
||||
return tree_refc(found, path, i, size); |
||||
} else { |
||||
return 0; |
||||
} |
||||
} else |
||||
return tree->refc; |
||||
} |
||||
|
||||
/****************************************************************
|
||||
* NIF helpers * |
||||
****************************************************************/ |
||||
static ERL_NIF_TERM cons(ErlNifEnv *env, char *str, ERL_NIF_TERM tail) |
||||
{ |
||||
if (str) { |
||||
size_t len = strlen(str); |
||||
ERL_NIF_TERM head; |
||||
unsigned char *buf = enif_make_new_binary(env, len, &head); |
||||
if (buf) { |
||||
memcpy(buf, str, len); |
||||
return enif_make_list_cell(env, head, tail); |
||||
} |
||||
} |
||||
return tail; |
||||
} |
||||
|
||||
static void match(ErlNifEnv *env, tree_t *root, |
||||
char *path, size_t i, size_t size, ERL_NIF_TERM *acc) |
||||
{ |
||||
tree_t *found; |
||||
size_t len = 0; |
||||
|
||||
if (i<=size) { |
||||
HASH_FIND_STR(root->sub, path+i, found); |
||||
if (found) { |
||||
len = strlen(path+i) + 1; |
||||
match(env, found, path, i+len, size, acc); |
||||
}; |
||||
HASH_FIND_STR(root->sub, "+", found); |
||||
if (found) { |
||||
len = strlen(path+i) + 1; |
||||
match(env, found, path, i+len, size, acc); |
||||
} |
||||
HASH_FIND_STR(root->sub, "#", found); |
||||
if (found) { |
||||
*acc = cons(env, found->val, *acc); |
||||
} |
||||
} else { |
||||
*acc = cons(env, root->val, *acc); |
||||
HASH_FIND_STR(root->sub, "#", found); |
||||
if (found) |
||||
*acc = cons(env, found->val, *acc); |
||||
} |
||||
} |
||||
|
||||
static void to_list(ErlNifEnv *env, tree_t *root, ERL_NIF_TERM *acc) |
||||
{ |
||||
tree_t *found, *iter; |
||||
|
||||
HASH_ITER(hh, root->sub, found, iter) { |
||||
if (found->val) { |
||||
size_t len = strlen(found->val); |
||||
ERL_NIF_TERM refc = enif_make_int(env, found->refc); |
||||
ERL_NIF_TERM val; |
||||
unsigned char *buf = enif_make_new_binary(env, len, &val); |
||||
if (buf) { |
||||
memcpy(buf, found->val, len); |
||||
*acc = enif_make_list_cell(env, enif_make_tuple2(env, val, refc), *acc); |
||||
} |
||||
}; |
||||
to_list(env, found, acc); |
||||
} |
||||
} |
||||
|
||||
static ERL_NIF_TERM dump(ErlNifEnv *env, tree_t *tree) |
||||
{ |
||||
tree_t *found, *iter; |
||||
ERL_NIF_TERM tail, head; |
||||
|
||||
tail = enif_make_list(env, 0); |
||||
HASH_ITER(hh, tree->sub, found, iter) { |
||||
head = dump(env, found); |
||||
tail = enif_make_list_cell(env, head, tail); |
||||
} |
||||
if (tree->key) { |
||||
ERL_NIF_TERM part, path; |
||||
part = enif_make_string(env, tree->key, ERL_NIF_LATIN1); |
||||
if (tree->val) |
||||
path = enif_make_string(env, tree->val, ERL_NIF_LATIN1); |
||||
else |
||||
path = enif_make_atom(env, "none"); |
||||
return enif_make_tuple4(env, part, path, enif_make_int(env, tree->refc), tail); |
||||
} else |
||||
return tail; |
||||
} |
||||
|
||||
static ERL_NIF_TERM raise(ErlNifEnv *env, int err) |
||||
{ |
||||
switch (err) { |
||||
case ENOMEM: |
||||
return enif_raise_exception(env, enif_make_atom(env, "enomem")); |
||||
default: |
||||
return enif_make_badarg(env); |
||||
} |
||||
} |
||||
|
||||
static char *prep_path(ErlNifBinary *bin) { |
||||
int i; |
||||
unsigned char c; |
||||
char *buf = malloc(bin->size+1); |
||||
if (buf) { |
||||
buf[bin->size] = 0; |
||||
for (i=0; i<bin->size; i++) { |
||||
c = bin->data[i]; |
||||
if (c == '/') buf[i] = 0; |
||||
else buf[i] = c; |
||||
} |
||||
} |
||||
return buf; |
||||
} |
||||
|
||||
/****************************************************************
|
||||
* Constructors/Destructors * |
||||
****************************************************************/ |
||||
static state_t *init_tree_state(ErlNifEnv *env) { |
||||
state_t *state = enif_alloc_resource(tree_state_t, sizeof(state_t)); |
||||
if (state) { |
||||
memset(state, 0, sizeof(state_t)); |
||||
state->tree = tree_new(NULL, 0); |
||||
state->lock = enif_rwlock_create("mqtree_lock"); |
||||
if (state->tree && state->lock) |
||||
return state; |
||||
else |
||||
enif_release_resource(state); |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
static void destroy_tree_state(ErlNifEnv *env, void *data) { |
||||
state_t *state = (state_t *) data; |
||||
if (state) { |
||||
tree_free(state->tree); |
||||
if (state->lock) enif_rwlock_destroy(state->lock); |
||||
} |
||||
memset(state, 0, sizeof(state_t)); |
||||
} |
||||
|
||||
/****************************************************************
|
||||
* NIF definitions * |
||||
****************************************************************/ |
||||
static int load(ErlNifEnv* env, void** priv, ERL_NIF_TERM max) { |
||||
ErlNifResourceFlags flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER; |
||||
tree_state_t = enif_open_resource_type(env, NULL, "mqtree_state", |
||||
destroy_tree_state, |
||||
flags, NULL); |
||||
return 0; |
||||
} |
||||
|
||||
static void unload(ErlNifEnv* env, void* priv) {} |
||||
|
||||
static ERL_NIF_TERM new_0(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
ERL_NIF_TERM result; |
||||
state_t *state = init_tree_state(env); |
||||
if (state) { |
||||
result = enif_make_resource(env, state); |
||||
enif_release_resource(state); |
||||
} else |
||||
result = raise(env, ENOMEM); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static ERL_NIF_TERM insert_2(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
ErlNifBinary path_bin; |
||||
ERL_NIF_TERM result; |
||||
|
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) || |
||||
!enif_inspect_iolist_as_binary(env, argv[1], &path_bin)) |
||||
return raise(env, EINVAL); |
||||
|
||||
if (!path_bin.size) |
||||
return enif_make_atom(env, "ok"); |
||||
|
||||
char *path = prep_path(&path_bin); |
||||
if (path) { |
||||
enif_rwlock_rwlock(state->lock); |
||||
int ret = tree_add(state->tree, path, path_bin.size); |
||||
enif_rwlock_rwunlock(state->lock); |
||||
if (!ret) |
||||
result = enif_make_atom(env, "ok"); |
||||
else |
||||
result = raise(env, ENOMEM); |
||||
} else |
||||
result = raise(env, ENOMEM); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static ERL_NIF_TERM delete_2(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
ErlNifBinary path_bin; |
||||
ERL_NIF_TERM result; |
||||
|
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) || |
||||
!enif_inspect_iolist_as_binary(env, argv[1], &path_bin)) |
||||
return raise(env, EINVAL); |
||||
|
||||
if (!path_bin.size) |
||||
return enif_make_atom(env, "ok"); |
||||
|
||||
char *path = prep_path(&path_bin); |
||||
if (path) { |
||||
enif_rwlock_rwlock(state->lock); |
||||
tree_del(state->tree, path, 0, path_bin.size); |
||||
enif_rwlock_rwunlock(state->lock); |
||||
free(path); |
||||
result = enif_make_atom(env, "ok"); |
||||
} else |
||||
result = raise(env, ENOMEM); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static ERL_NIF_TERM match_2(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
ErlNifBinary path_bin; |
||||
ERL_NIF_TERM result = enif_make_list(env, 0); |
||||
|
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) || |
||||
!enif_inspect_iolist_as_binary(env, argv[1], &path_bin)) |
||||
return raise(env, EINVAL); |
||||
|
||||
if (!path_bin.size) |
||||
return result; |
||||
|
||||
char *path = prep_path(&path_bin); |
||||
if (path) { |
||||
enif_rwlock_rlock(state->lock); |
||||
match(env, state->tree, path, 0, path_bin.size, &result); |
||||
enif_rwlock_runlock(state->lock); |
||||
free(path); |
||||
} else |
||||
result = raise(env, ENOMEM); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static ERL_NIF_TERM refc_2(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
ErlNifBinary path_bin; |
||||
|
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) || |
||||
!enif_inspect_iolist_as_binary(env, argv[1], &path_bin)) |
||||
return raise(env, EINVAL); |
||||
|
||||
if (!path_bin.size) |
||||
return enif_make_int(env, 0); |
||||
|
||||
char *path = prep_path(&path_bin); |
||||
if (path) { |
||||
enif_rwlock_rlock(state->lock); |
||||
int refc = tree_refc(state->tree, path, 0, path_bin.size); |
||||
enif_rwlock_runlock(state->lock); |
||||
free(path); |
||||
return enif_make_int(env, refc); |
||||
} else |
||||
return raise(env, ENOMEM); |
||||
} |
||||
|
||||
static ERL_NIF_TERM clear_1(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state)) |
||||
return raise(env, EINVAL); |
||||
|
||||
enif_rwlock_rwlock(state->lock); |
||||
tree_clear(state->tree); |
||||
enif_rwlock_rwunlock(state->lock); |
||||
|
||||
return enif_make_atom(env, "ok"); |
||||
} |
||||
|
||||
static ERL_NIF_TERM size_1(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
size_t size = 0; |
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state)) |
||||
return raise(env, EINVAL); |
||||
|
||||
enif_rwlock_rlock(state->lock); |
||||
tree_size(state->tree, &size); |
||||
enif_rwlock_runlock(state->lock); |
||||
|
||||
return enif_make_uint64(env, (ErlNifUInt64) size); |
||||
} |
||||
|
||||
static ERL_NIF_TERM is_empty_1(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state)) |
||||
return raise(env, EINVAL); |
||||
|
||||
enif_rwlock_rlock(state->lock); |
||||
char *ret = state->tree->sub ? "false" : "true"; |
||||
enif_rwlock_runlock(state->lock); |
||||
|
||||
return enif_make_atom(env, ret); |
||||
} |
||||
|
||||
static ERL_NIF_TERM to_list_1(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
ERL_NIF_TERM result = enif_make_list(env, 0); |
||||
|
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state)) |
||||
return raise(env, EINVAL); |
||||
|
||||
enif_rwlock_rlock(state->lock); |
||||
to_list(env, state->tree, &result); |
||||
enif_rwlock_runlock(state->lock); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static ERL_NIF_TERM dump_1(ErlNifEnv* env, int argc, |
||||
const ERL_NIF_TERM argv[]) |
||||
{ |
||||
state_t *state; |
||||
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state)) |
||||
return raise(env, EINVAL); |
||||
|
||||
enif_rwlock_rlock(state->lock); |
||||
ERL_NIF_TERM result = dump(env, state->tree); |
||||
enif_rwlock_runlock(state->lock); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
static ErlNifFunc nif_funcs[] = |
||||
{ |
||||
{"new", 0, new_0}, |
||||
{"insert", 2, insert_2}, |
||||
{"delete", 2, delete_2}, |
||||
{"match", 2, match_2}, |
||||
{"refc", 2, refc_2}, |
||||
{"clear", 1, clear_1}, |
||||
{"size", 1, size_1}, |
||||
{"is_empty", 1, is_empty_1}, |
||||
{"to_list", 1, to_list_1}, |
||||
{"dump", 1, dump_1} |
||||
}; |
||||
|
||||
ERL_NIF_INIT(mqtree, nif_funcs, load, NULL, NULL, unload) |
Binary file not shown.
@ -0,0 +1,50 @@
|
||||
%%%------------------------------------------------------------------- |
||||
%%% |
||||
%%% ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne |
||||
%%% |
||||
%%% The ejabberd software is the exclusive property of the licensor |
||||
%%% ProcessOne. It is protected by the law on copyright and |
||||
%%% international conventions. As a result, the dealer |
||||
%%% recognizes that will make every effort to ensure the confidentiality |
||||
%%% on the software. It is recalled that a violation of the rights of |
||||
%%% authors of the software is an infringement and that any |
||||
%%% counterfeit is punishable in France by Article L339-2 of the Code of |
||||
%%% Intellectual property and punishable by three years imprisonment and |
||||
%%% 300000 euros. |
||||
%%% |
||||
%%% Any infringement liable to be so qualified and |
||||
%%% would be caused by third parties and whose dealer has knowledge |
||||
%%% should be terminated by the licensor that it will make its case |
||||
%%% personal conduct of the proceedings. Any allegation of infringement |
||||
%%% formed against the dealer because of the use of the Software will |
||||
%%% be brought to the knowledge of the licensor which will assist |
||||
%%% in defense of the dealer in the manner and form that |
||||
%%% see fit and fix alone. |
||||
%%% |
||||
%%%---------------------------------------------------------------------- |
||||
{erl_opts, [debug_info, {src_dirs, ["src"]}]}. |
||||
|
||||
{port_env, [{"CFLAGS", "$CFLAGS -g -O2 -Wall -I deps/uthash/src"}, |
||||
{"LDFLAGS", "$LDFLAGS -lpthread"}]}. |
||||
|
||||
{port_specs, [{"priv/lib/mqtree.so", ["c_src/mqtree.c"]}]}. |
||||
|
||||
{deps, [{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", |
||||
"6ff85e8"}}, |
||||
{uthash, ".*", {git, "https://github.com/troydhanson/uthash.git", |
||||
{tag, "v2.0.2"}}, [raw]}]}. |
||||
|
||||
{clean_files, ["c_src/mqtree.gcda", "c_src/mqtree.gcno"]}. |
||||
|
||||
{cover_enabled, true}. |
||||
{cover_export_enabled, true}. |
||||
|
||||
{xref_checks, [undefined_function_calls, undefined_functions, |
||||
deprecated_function_calls, deprecated_functions]}. |
||||
|
||||
{profiles, [{test, [{erl_opts, [{src_dirs, ["test"]}]}]}]}. |
||||
|
||||
%% Local Variables: |
||||
%% mode: erlang |
||||
%% End: |
||||
%% vim: set filetype=erlang tabstop=8: |
@ -0,0 +1,37 @@
|
||||
%%%------------------------------------------------------------------- |
||||
%%% |
||||
%%% ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne |
||||
%%% |
||||
%%% The ejabberd software is the exclusive property of the licensor |
||||
%%% ProcessOne. It is protected by the law on copyright and |
||||
%%% international conventions. As a result, the dealer |
||||
%%% recognizes that will make every effort to ensure the confidentiality |
||||
%%% on the software. It is recalled that a violation of the rights of |
||||
%%% authors of the software is an infringement and that any |
||||
%%% counterfeit is punishable in France by Article L339-2 of the Code of |
||||
%%% Intellectual property and punishable by three years imprisonment and |
||||
%%% 300000 euros. |
||||
%%% |
||||
%%% Any infringement liable to be so qualified and |
||||
%%% would be caused by third parties and whose dealer has knowledge |
||||
%%% should be terminated by the licensor that it will make its case |
||||
%%% personal conduct of the proceedings. Any allegation of infringement |
||||
%%% formed against the dealer because of the use of the Software will |
||||
%%% be brought to the knowledge of the licensor which will assist |
||||
%%% in defense of the dealer in the manner and form that |
||||
%%% see fit and fix alone. |
||||
%%% |
||||
%%%---------------------------------------------------------------------- |
||||
{application, mqtree, |
||||
[ |
||||
{description, "Index tree for MQTT topic filters"}, |
||||
{vsn, "1.0.0"}, |
||||
{registered, []}, |
||||
{applications, [kernel, stdlib]}, |
||||
{env, []} |
||||
]}. |
||||
|
||||
%% Local Variables: |
||||
%% mode: erlang |
||||
%% End: |
||||
%% vim: set filetype=erlang tabstop=8: |
@ -0,0 +1,99 @@
|
||||
%%%------------------------------------------------------------------- |
||||
%%% |
||||
%%% ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne |
||||
%%% |
||||
%%% The ejabberd software is the exclusive property of the licensor |
||||
%%% ProcessOne. It is protected by the law on copyright and |
||||
%%% international conventions. As a result, the dealer |
||||
%%% recognizes that will make every effort to ensure the confidentiality |
||||
%%% on the software. It is recalled that a violation of the rights of |
||||
%%% authors of the software is an infringement and that any |
||||
%%% counterfeit is punishable in France by Article L339-2 of the Code of |
||||
%%% Intellectual property and punishable by three years imprisonment and |
||||
%%% 300000 euros. |
||||
%%% |
||||
%%% Any infringement liable to be so qualified and |
||||
%%% would be caused by third parties and whose dealer has knowledge |
||||
%%% should be terminated by the licensor that it will make its case |
||||
%%% personal conduct of the proceedings. Any allegation of infringement |
||||
%%% formed against the dealer because of the use of the Software will |
||||
%%% be brought to the knowledge of the licensor which will assist |
||||
%%% in defense of the dealer in the manner and form that |
||||
%%% see fit and fix alone. |
||||
%%% |
||||
%%%---------------------------------------------------------------------- |
||||
-module(mqtree). |
||||
-on_load(load_nif/0). |
||||
|
||||
%% API |
||||
-export([new/0, insert/2, delete/2, match/2, refc/2, |
||||
clear/1, size/1, is_empty/1]). |
||||
%% For debugging |
||||
-export([dump/1, to_list/1]). |
||||
|
||||
-type path() :: iodata(). |
||||
-opaque tree() :: reference(). |
||||
-export_type([tree/0, path/0]). |
||||
|
||||
%%%=================================================================== |
||||
%%% API |
||||
%%%=================================================================== |
||||
-spec new() -> tree(). |
||||
new() -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
-spec insert(tree(), path()) -> ok. |
||||
insert(_Tree, _Path) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
-spec delete(tree(), path()) -> ok. |
||||
delete(_Tree, _Path) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
-spec match(tree(), path()) -> [binary()]. |
||||
match(_Tree, _Path) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
-spec refc(tree(), path()) -> non_neg_integer(). |
||||
refc(_Tree, _Path) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
-spec clear(tree()) -> ok. |
||||
clear(_Tree) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
-spec size(tree()) -> non_neg_integer(). |
||||
size(_Tree) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
-spec is_empty(tree()) -> boolean(). |
||||
is_empty(_Tree) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
%%%=================================================================== |
||||
%%% For testing/debugging |
||||
%%%=================================================================== |
||||
-type tree_node() :: {string(), string() | none, |
||||
non_neg_integer(), [tree_node()]}. |
||||
|
||||
-spec dump(tree()) -> [tree_node()]. |
||||
dump(_Tree) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
-spec to_list(tree()) -> [{binary(), non_neg_integer()}]. |
||||
to_list(_Tree) -> |
||||
erlang:nif_error({nif_not_loaded, ?MODULE}). |
||||
|
||||
%%%=================================================================== |
||||
%%% Internal functions |
||||
%%%=================================================================== |
||||
load_nif() -> |
||||
Path = p1_nif_utils:get_so_path(?MODULE, [?MODULE], atom_to_list(?MODULE)), |
||||
case erlang:load_nif(Path, 0) of |
||||
ok -> ok; |
||||
{error, {upgrade, _}} -> ok; |
||||
{error, {Reason, Text}} -> |
||||
error_logger:error_msg("Failed to load NIF ~s: ~s (~p)", |
||||
[Path, Text, Reason]), |
||||
erlang:nif_error(Reason) |
||||
end. |
@ -0,0 +1,244 @@
|
||||
%%%------------------------------------------------------------------- |
||||
%%% |
||||
%%% ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne |
||||
%%% |
||||
%%% The ejabberd software is the exclusive property of the licensor |
||||
%%% ProcessOne. It is protected by the law on copyright and |
||||
%%% international conventions. As a result, the dealer |
||||
%%% recognizes that will make every effort to ensure the confidentiality |
||||
%%% on the software. It is recalled that a violation of the rights of |
||||
%%% authors of the software is an infringement and that any |
||||
%%% counterfeit is punishable in France by Article L339-2 of the Code of |
||||
%%% Intellectual property and punishable by three years imprisonment and |
||||
%%% 300000 euros. |
||||
%%% |
||||
%%% Any infringement liable to be so qualified and |
||||
%%% would be caused by third parties and whose dealer has knowledge |
||||
%%% should be terminated by the licensor that it will make its case |
||||
%%% personal conduct of the proceedings. Any allegation of infringement |
||||
%%% formed against the dealer because of the use of the Software will |
||||
%%% be brought to the knowledge of the licensor which will assist |
||||
%%% in defense of the dealer in the manner and form that |
||||
%%% see fit and fix alone. |
||||
%%% |
||||
%%%---------------------------------------------------------------------- |
||||
-module(mqtree_test). |
||||
-include_lib("eunit/include/eunit.hrl"). |
||||
|
||||
-define(assertTree(L), |
||||
case L of |
||||
[] -> |
||||
?assertEqual([], mqtree:dump(T)), |
||||
?assertEqual([], mqtree:to_list(T)); |
||||
_ -> |
||||
?assertEqual(L, lists:sort(mqtree:to_list(T))) |
||||
end). |
||||
|
||||
-define(assertInsert(E), |
||||
?assertEqual(ok, mqtree:insert(T, E))). |
||||
|
||||
-define(assertDelete(E), |
||||
?assertEqual(ok, mqtree:delete(T, E))). |
||||
|
||||
%%%=================================================================== |
||||
%%% Tests |
||||
%%%=================================================================== |
||||
new_test() -> |
||||
T = mqtree:new(), |
||||
?assertTree([]). |
||||
|
||||
insert_test() -> |
||||
T = mqtree:new(), |
||||
Path = <<"/a/b/c">>, |
||||
?assertInsert(Path), |
||||
?assertTree([{Path, 1}]). |
||||
|
||||
is_empty_test() -> |
||||
T = mqtree:new(), |
||||
?assert(mqtree:is_empty(T)), |
||||
?assertInsert(<<"/">>), |
||||
?assert(not mqtree:is_empty(T)). |
||||
|
||||
insert_then_delete_test() -> |
||||
T = mqtree:new(), |
||||
Path = <<"a/b">>, |
||||
?assertInsert(Path), |
||||
?assertDelete(Path), |
||||
?assertTree([]). |
||||
|
||||
insert_empty_then_delete_empty_test() -> |
||||
T = mqtree:new(), |
||||
?assertInsert(<<>>), |
||||
?assertTree([]), |
||||
?assertDelete(<<>>), |
||||
?assertTree([]). |
||||
|
||||
insert_then_delete_empty_test() -> |
||||
T = mqtree:new(), |
||||
Path = <<"/a/b">>, |
||||
?assertInsert(Path), |
||||
?assertTree([{Path, 1}]), |
||||
?assertDelete(<<>>), |
||||
?assertTree([{Path, 1}]). |
||||
|
||||
insert_then_delete_shuffle_test() -> |
||||
T = mqtree:new(), |
||||
Check = lists:sort(rand_paths()), |
||||
lists:foldl( |
||||
fun(insert, Refc) -> |
||||
lists:foreach( |
||||
fun(Path) -> ?assertInsert(Path) end, |
||||
rand_paths()), |
||||
Refc1 = Refc+1, |
||||
?assertTree([{P, Refc1} || P <- Check]), |
||||
Refc1; |
||||
(delete, Refc) -> |
||||
lists:foreach( |
||||
fun(Path) -> ?assertDelete(Path) end, |
||||
rand_paths()), |
||||
Refc1 = Refc-1, |
||||
case Refc1 of |
||||
0 -> |
||||
?assertTree([]); |
||||
_ -> |
||||
?assertTree([{P, Refc1} || P <- Check]) |
||||
end, |
||||
Refc1 |
||||
end, 0, rand_funs()). |
||||
|
||||
refc_test() -> |
||||
T = mqtree:new(), |
||||
lists:foreach( |
||||
fun(Refc) -> |
||||
lists:foreach( |
||||
fun(P) -> |
||||
?assertEqual(Refc, mqtree:refc(T, P)), |
||||
?assertInsert(P) |
||||
end, rand_paths()) |
||||
end, lists:seq(0, 5)), |
||||
lists:foreach( |
||||
fun(Refc) -> |
||||
lists:foreach( |
||||
fun(P) -> |
||||
?assertDelete(P), |
||||
?assertEqual(Refc, mqtree:refc(T, P)) |
||||
end, rand_paths()) |
||||
end, lists:seq(5, 0, -1)). |
||||
|
||||
clear_test() -> |
||||
T = mqtree:new(), |
||||
lists:foreach( |
||||
fun(_) -> |
||||
lists:foreach(fun(P) -> ?assertInsert(P) end, rand_paths()), |
||||
?assertEqual(ok, mqtree:clear(T)), |
||||
?assertTree([]) |
||||
end, lists:seq(1, 10)). |
||||
|
||||
clear_empty_test() -> |
||||
T = mqtree:new(), |
||||
?assertEqual(ok, mqtree:clear(T)), |
||||
?assertTree([]). |
||||
|
||||
size_test() -> |
||||
T = mqtree:new(), |
||||
?assertEqual(0, mqtree:size(T)), |
||||
Paths = rand_paths(), |
||||
lists:foreach( |
||||
fun(_) -> |
||||
lists:foreach(fun(P) -> ?assertInsert(P) end, rand_paths()), |
||||
?assert(mqtree:size(T) == length(Paths)) |
||||
end, [1,2,3]), |
||||
?assertEqual(ok, mqtree:clear(T)), |
||||
?assertEqual(0, mqtree:size(T)). |
||||
|
||||
delete_non_existent_test() -> |
||||
T = mqtree:new(), |
||||
lists:foreach( |
||||
fun(_) -> |
||||
lists:foreach(fun(P) -> ?assertDelete(P) end, rand_paths()), |
||||
?assertTree([]) |
||||
end, lists:seq(1, 10)). |
||||
|
||||
insert_then_delete_non_existent_test() -> |
||||
T = mqtree:new(), |
||||
Inserts = rand_paths("@$%&*"), |
||||
Check = [{P, 1} || P <- lists:sort(Inserts)], |
||||
lists:foreach(fun(P) -> ?assertInsert(P) end, Inserts), |
||||
lists:foreach( |
||||
fun(_) -> |
||||
lists:foreach(fun(P) -> ?assertDelete(P) end, rand_paths()), |
||||
?assertTree(Check) |
||||
end, lists:seq(1, 10)). |
||||
|
||||
match_all_test() -> |
||||
T = mqtree:new(), |
||||
lists:foreach( |
||||
fun(_) -> |
||||
?assertInsert("#"), |
||||
lists:foreach( |
||||
fun(P) -> |
||||
?assertEqual([<<"#">>], mqtree:match(T, P)) |
||||
end, rand_paths()) |
||||
end, lists:seq(1, 10)). |
||||
|
||||
match_none_test() -> |
||||
T = mqtree:new(), |
||||
lists:foreach( |
||||
fun(P) -> |
||||
?assertEqual([], mqtree:match(T, P)) |
||||
end, rand_paths()). |
||||
|
||||
match_exact_test() -> |
||||
T = mqtree:new(), |
||||
lists:foreach(fun(P) -> ?assertInsert(P) end, rand_paths()), |
||||
lists:foreach( |
||||
fun(P) -> |
||||
?assertEqual([P], mqtree:match(T, P)) |
||||
end, rand_paths()). |
||||
|
||||
match_tail_test() -> |
||||
T = mqtree:new(), |
||||
Filter = <<"a/b/#">>, |
||||
?assertInsert(Filter), |
||||
?assertEqual([], mqtree:match(T, "a/bc")), |
||||
?assertEqual([Filter], mqtree:match(T, "a/b")), |
||||
?assertEqual([Filter], mqtree:match(T, "a/b/")), |
||||
?assertEqual([Filter], mqtree:match(T, "a/b/c")), |
||||
?assertEqual([Filter], mqtree:match(T, "a/b/c/d")). |
||||
|
||||
match_plus_test() -> |
||||
T = mqtree:new(), |
||||
Filter = lists:sort([<<A, $/, B>> || A<-"+a", B<-"+b"]), |
||||
lists:foreach(fun(P) -> ?assertInsert(P) end, Filter), |
||||
?assertEqual([<<"+/+">>], mqtree:match(T, "/")), |
||||
?assertEqual([<<"+/+">>], mqtree:match(T, "x/")), |
||||
?assertEqual([<<"+/+">>], mqtree:match(T, "/y")), |
||||
?assertEqual([<<"+/+">>], mqtree:match(T, "x/y")), |
||||
?assertEqual([<<"+/+">>, <<"a/+">>], mqtree:match(T, "a/")), |
||||
?assertEqual([<<"+/+">>, <<"a/+">>], mqtree:match(T, "a/y")), |
||||
?assertEqual([<<"+/+">>, <<"+/b">>], mqtree:match(T, "/b")), |
||||
?assertEqual([<<"+/+">>, <<"+/b">>], mqtree:match(T, "x/b")), |
||||
?assertEqual(Filter, lists:sort(mqtree:match(T, "a/b"))). |
||||
|
||||
%%%=================================================================== |
||||
%%% Internal functions |
||||
%%%=================================================================== |
||||
rand_paths() -> |
||||
rand_paths("/abcd"). |
||||
|
||||
rand_paths(Set) -> |
||||
L1 = [{p1_rand:uniform(), <<A>>} || A<-Set], |
||||
L2 = [{p1_rand:uniform(), <<A,B>>} || A<-Set, B<-Set], |
||||
L3 = [{p1_rand:uniform(), <<A,B,C>>} || A<-Set, B<-Set, C<-Set], |
||||
L4 = [{p1_rand:uniform(), <<A,B,C,D>>} || A<-Set, B<-Set, C<-Set, D<-Set], |
||||
L5 = [{p1_rand:uniform(), <<A,B,C,D,E>>} || A<-Set, B<-Set, C<-Set, D<-Set, E<-Set], |
||||
[Path || {_, Path} <- lists:keysort(1, L1++L2++L3++L4++L5)]. |
||||
|
||||
rand_funs() -> |
||||
lists:flatmap( |
||||
fun(_) -> |
||||
I = p1_rand:uniform(5), |
||||
Inserts = lists:duplicate(I, insert), |
||||
Deletes = lists:duplicate(I, delete), |
||||
Inserts ++ Deletes |
||||
end, [1,2,3,4,5]). |
Loading…
Reference in new issue