Compare commits
27 Commits
fix-some-w
...
T610-FL-Ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83aa71a29f | ||
|
|
c2a1328dcc | ||
|
|
b63db202d6 | ||
|
|
1abe8f8bfc | ||
|
|
4366085d89 | ||
|
|
9369ae9085 | ||
|
|
86a15331d1 | ||
|
|
0c8b35b151 | ||
|
|
11d60c203e | ||
|
|
38c0a08342 | ||
|
|
7e1d39bf86 | ||
|
|
dd85b428bf | ||
|
|
2f9ed0146e | ||
|
|
3e0e17d469 | ||
|
|
b57f91fcfc | ||
|
|
066a96c0ae | ||
|
|
1ae6b71c5f | ||
|
|
cbe15e7f44 | ||
|
|
65a7ba01da | ||
|
|
7a2bbd4bb3 | ||
|
|
589e0e098b | ||
|
|
41d4185156 | ||
|
|
599c0a641f | ||
|
|
1fb49c4865 | ||
|
|
df1485aeec | ||
|
|
b2e1056389 | ||
|
|
e4c9411e63 |
@@ -61,7 +61,9 @@ Checks: '*,
|
||||
-readability-magic-numbers,
|
||||
-readability-named-parameter,
|
||||
-misc-no-recursion,
|
||||
-concurrency-mt-unsafe'
|
||||
-concurrency-mt-unsafe,
|
||||
-bugprone-easily-swappable-parameters'
|
||||
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: 'src/.*'
|
||||
AnalyzeTemporaryDtors: false
|
||||
|
||||
5
.github/workflows/diff.yaml
vendored
5
.github/workflows/diff.yaml
vendored
@@ -1,4 +1,7 @@
|
||||
name: Diff
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -112,7 +115,7 @@ jobs:
|
||||
source /opt/toolchain-v4/activate
|
||||
|
||||
# Restrict clang-tidy results only to the modified parts
|
||||
git diff -U0 master... -- src ':!*.hpp' | ./tools/github/clang-tidy/clang-tidy-diff.py -p 1 -j $THREADS -path build | tee ./build/clang_tidy_output.txt
|
||||
git diff -U0 master... -- src | ./tools/github/clang-tidy/clang-tidy-diff.py -p 1 -j $THREADS -path build | tee ./build/clang_tidy_output.txt
|
||||
|
||||
# Fail if any warning is reported
|
||||
! cat ./build/clang_tidy_output.txt | ./tools/github/clang-tidy/grep_error_lines.sh > /dev/null
|
||||
|
||||
76
.github/workflows/package_all.yaml
vendored
76
.github/workflows/package_all.yaml
vendored
@@ -6,11 +6,11 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
centos-7:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -22,29 +22,29 @@ jobs:
|
||||
name: centos-7
|
||||
path: build/output/centos-7/memgraph*.rpm
|
||||
|
||||
centos-8:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
centos-9:
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
run: |
|
||||
./release/package/run.sh package centos-8
|
||||
./release/package/run.sh package centos-9
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: centos-8
|
||||
path: build/output/centos-8/memgraph*.rpm
|
||||
name: centos-9
|
||||
path: build/output/centos-9/memgraph*.rpm
|
||||
|
||||
debian-10:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -57,11 +57,11 @@ jobs:
|
||||
path: build/output/debian-10/memgraph*.deb
|
||||
|
||||
debian-11:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -74,11 +74,11 @@ jobs:
|
||||
path: build/output/debian-11/memgraph*.deb
|
||||
|
||||
docker:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -93,11 +93,11 @@ jobs:
|
||||
path: build/output/docker/memgraph*.tar.gz
|
||||
|
||||
ubuntu-1804:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -110,11 +110,11 @@ jobs:
|
||||
path: build/output/ubuntu-18.04/memgraph*.deb
|
||||
|
||||
ubuntu-2004:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -126,12 +126,29 @@ jobs:
|
||||
name: ubuntu-2004
|
||||
path: build/output/ubuntu-20.04/memgraph*.deb
|
||||
|
||||
debian-11-platform:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
ubuntu-2204:
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
run: |
|
||||
./release/package/run.sh package ubuntu-22.04
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ubuntu-2204
|
||||
path: build/output/ubuntu-22.04/memgraph*.deb
|
||||
|
||||
debian-11-platform:
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -142,3 +159,20 @@ jobs:
|
||||
with:
|
||||
name: debian-11-platform
|
||||
path: build/output/debian-11/memgraph*.deb
|
||||
|
||||
debian-11-arm:
|
||||
runs-on: [self-hosted, DockerMgBuild, ARM64, strange]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
run: |
|
||||
./release/package/run.sh package debian-11-arm
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: debian-11
|
||||
path: build/output/debian-11/memgraph*.deb
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
*.swn
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
*~
|
||||
.DS_Store
|
||||
.gdb_history
|
||||
@@ -26,6 +27,7 @@ src/query/frontend/opencypher/generated/
|
||||
tags
|
||||
ve/
|
||||
ve3/
|
||||
.cache/
|
||||
perf.data*
|
||||
TAGS
|
||||
*.apollo_measurements
|
||||
|
||||
@@ -18,14 +18,16 @@ WIDTH = 80
|
||||
|
||||
def wrap_text(s, initial_indent="# "):
|
||||
return "\n#\n".join(
|
||||
map(lambda x: textwrap.fill(x, WIDTH, initial_indent=initial_indent,
|
||||
subsequent_indent="# "), s.split("\n")))
|
||||
map(
|
||||
lambda x: textwrap.fill(x, WIDTH, initial_indent=initial_indent, subsequent_indent="# "),
|
||||
s.split("\n"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def extract_flags(binary_path):
|
||||
ret = {}
|
||||
data = subprocess.run([binary_path, "--help-xml"],
|
||||
stdout=subprocess.PIPE).stdout.decode("utf-8")
|
||||
data = subprocess.run([binary_path, "--help-xml"], stdout=subprocess.PIPE).stdout.decode("utf-8")
|
||||
root = ET.fromstring(data)
|
||||
for child in root:
|
||||
if child.tag == "usage" and child.text.lower().count("warning"):
|
||||
@@ -46,8 +48,7 @@ def apply_config_to_flags(config, flags):
|
||||
for modification in config["modifications"]:
|
||||
name = modification["name"]
|
||||
if name not in flags:
|
||||
print("WARNING: Flag '" + name + "' missing from binary!",
|
||||
file=sys.stderr)
|
||||
print("WARNING: Flag '" + name + "' missing from binary!", file=sys.stderr)
|
||||
continue
|
||||
flags[name]["default"] = modification["value"]
|
||||
flags[name]["override"] = modification["override"]
|
||||
@@ -75,8 +76,9 @@ def extract_sections(flags):
|
||||
else:
|
||||
sections.append((current_section, current_flags))
|
||||
sections.append(("other", other))
|
||||
assert set(sum(map(lambda x: x[1], sections), [])) == set(flags.keys()), \
|
||||
"The section extraction algorithm lost some flags!"
|
||||
assert set(sum(map(lambda x: x[1], sections), [])) == set(
|
||||
flags.keys()
|
||||
), "The section extraction algorithm lost some flags!"
|
||||
return sections
|
||||
|
||||
|
||||
@@ -89,8 +91,7 @@ def generate_config_file(sections, flags):
|
||||
helpstr = flag["meaning"] + " [" + flag["type"] + "]"
|
||||
ret += wrap_text(helpstr) + "\n"
|
||||
prefix = "# " if not flag["override"] else ""
|
||||
ret += prefix + "--" + flag["name"].replace("_", "-") + \
|
||||
"=" + flag["default"] + "\n\n"
|
||||
ret += prefix + "--" + flag["name"].replace("_", "-") + "=" + flag["default"] + "\n\n"
|
||||
ret += "\n"
|
||||
ret += wrap_text(config["footer"])
|
||||
return ret.strip() + "\n"
|
||||
@@ -98,13 +99,16 @@ def generate_config_file(sections, flags):
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("memgraph_binary",
|
||||
help="path to Memgraph binary")
|
||||
parser.add_argument("output_file",
|
||||
help="path where to store the generated Memgraph "
|
||||
"configuration file")
|
||||
parser.add_argument("--config-file", default=CONFIG_FILE,
|
||||
help="path to generator configuration file")
|
||||
parser.add_argument("memgraph_binary", help="path to Memgraph binary")
|
||||
parser.add_argument(
|
||||
"output_file",
|
||||
help="path where to store the generated Memgraph " "configuration file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config-file",
|
||||
default=CONFIG_FILE,
|
||||
help="path to generator configuration file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
flags = extract_flags(args.memgraph_binary)
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils-common gcc gcc-c++ make # generic build tools
|
||||
wget # used for archive download
|
||||
gnupg2 # used for archive signature verification
|
||||
tar gzip bzip2 xz unzip # used for archive unpacking
|
||||
zlib-devel # zlib library used for all builds
|
||||
expat-devel libipt libipt-devel libbabeltrace-devel xz-devel python36-devel texinfo # for gdb
|
||||
libcurl-devel # for cmake
|
||||
curl # snappy
|
||||
readline-devel # for cmake and llvm
|
||||
libffi-devel libxml2-devel # for llvm
|
||||
libedit-devel pcre-devel automake bison # for swig
|
||||
file
|
||||
openssl-devel
|
||||
gmp-devel
|
||||
gperf
|
||||
patch
|
||||
)
|
||||
|
||||
TOOLCHAIN_RUN_DEPS=(
|
||||
make # generic build tools
|
||||
tar gzip bzip2 xz # used for archive unpacking
|
||||
zlib # zlib library used for all builds
|
||||
expat libipt libbabeltrace xz-libs python36 # for gdb
|
||||
readline # for cmake and llvm
|
||||
libffi libxml2 # for llvm
|
||||
openssl-devel
|
||||
)
|
||||
|
||||
MEMGRAPH_BUILD_DEPS=(
|
||||
git # source code control
|
||||
make pkgconf-pkg-config # build system
|
||||
curl wget # for downloading libs
|
||||
libuuid-devel java-11-openjdk # required by antlr
|
||||
readline-devel # for memgraph console
|
||||
python36-devel # for query modules
|
||||
openssl-devel
|
||||
libseccomp-devel
|
||||
python36 python3-virtualenv python3-pip nmap-ncat # for qa, macro_benchmark and stress tests
|
||||
#
|
||||
# IMPORTANT: python3-yaml does NOT exist on CentOS
|
||||
# Install it manually using `pip3 install PyYAML`
|
||||
#
|
||||
PyYAML # Package name here does not correspond to the yum package!
|
||||
libcurl-devel # mg-requests
|
||||
rpm-build rpmlint # for RPM package building
|
||||
doxygen graphviz # source documentation generators
|
||||
which mono-complete dotnet-sdk-3.1 nodejs golang zip unzip java-11-openjdk-devel # for driver tests
|
||||
sbcl # for custom Lisp C++ preprocessing
|
||||
autoconf # for jemalloc code generation
|
||||
libtool # for protobuf code generation
|
||||
)
|
||||
|
||||
list() {
|
||||
echo "$1"
|
||||
}
|
||||
|
||||
check() {
|
||||
local missing=""
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == "PyYAML" ]; then
|
||||
if ! python3 -c "import yaml" >/dev/null 2>/dev/null; then
|
||||
missing="$pkg $missing"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if ! yum list installed "$pkg" >/dev/null 2>/dev/null; then
|
||||
missing="$pkg $missing"
|
||||
fi
|
||||
done
|
||||
if [ "$missing" != "" ]; then
|
||||
echo "MISSING PACKAGES: $missing"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install() {
|
||||
cd "$DIR"
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root."
|
||||
exit 1
|
||||
fi
|
||||
# If GitHub Actions runner is installed, append LANG to the environment.
|
||||
# Python related tests doesn't work the LANG export.
|
||||
if [ -d "/home/gh/actions-runner" ]; then
|
||||
echo "LANG=en_US.utf8" >> /home/gh/actions-runner/.env
|
||||
else
|
||||
echo "NOTE: export LANG=en_US.utf8"
|
||||
fi
|
||||
dnf install -y epel-release
|
||||
dnf install -y 'dnf-command(config-manager)'
|
||||
dnf config-manager --set-enabled powertools # Required to install texinfo.
|
||||
dnf update -y
|
||||
dnf install -y wget git python36 python3-pip
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == libipt ]; then
|
||||
if ! dnf list installed libipt >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://repo.okay.com.mx/centos/8/x86_64/release/libipt-1.6.1-8.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == libipt-devel ]; then
|
||||
if ! yum list installed libipt-devel >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://repo.okay.com.mx/centos/8/x86_64/release/libipt-devel-1.6.1-8.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
# Install GDB dependencies not present in the standard repos.
|
||||
# https://bugs.centos.org/view.php?id=17068
|
||||
# https://centos.pkgs.org
|
||||
# Since 2020, there is Babeltrace2 (https://babeltrace.org). Not used
|
||||
# within GDB yet (an assumption).
|
||||
# http://mirror.centos.org/centos/8/PowerTools/x86_64/os/Packages/libbabeltrace-devel-1.5.4-3.el8.x86_64.rpm not working
|
||||
if [ "$pkg" == libbabeltrace-devel ]; then
|
||||
if ! dnf list installed libbabeltrace-devel >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://rpmfind.net/linux/centos/8-stream/PowerTools/x86_64/os/Packages/libbabeltrace-devel-1.5.4-3.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == sbcl ]; then
|
||||
if ! dnf list installed cl-asdf >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/cl-asdf-20101028-18.el8.noarch.rpm
|
||||
fi
|
||||
if ! dnf list installed common-lisp-controller >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/common-lisp-controller-7.4-20.el8.noarch.rpm
|
||||
fi
|
||||
if ! dnf list installed sbcl >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/sbcl-2.0.1-4.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == dotnet-sdk-3.1 ]; then
|
||||
if ! dnf list installed dotnet-sdk-3.1 >/dev/null 2>/dev/null; then
|
||||
wget -nv https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm -O packages-microsoft-prod.rpm
|
||||
rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm
|
||||
dnf update -y
|
||||
dnf install -y dotnet-sdk-3.1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == PyYAML ]; then
|
||||
if [ -z ${SUDO_USER+x} ]; then # Running as root (e.g. Docker).
|
||||
pip3 install --user PyYAML
|
||||
else # Running using sudo.
|
||||
sudo -H -u "$SUDO_USER" bash -c "pip3 install --user PyYAML"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
dnf install -y "$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
deps=$2"[*]"
|
||||
"$1" "${!deps}"
|
||||
@@ -6,14 +6,12 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc gcc-c++ make # generic build tools
|
||||
coreutils-common gcc gcc-c++ make # generic build tools
|
||||
wget # used for archive download
|
||||
gnupg2 # used for archive signature verification
|
||||
tar gzip bzip2 xz unzip # used for archive unpacking
|
||||
zlib-devel # zlib library used for all builds
|
||||
expat-devel xz-devel python3-devel texinfo # for gdb
|
||||
libcurl-devel # for cmake
|
||||
curl # snappy
|
||||
expat-devel xz-devel python3-devel texinfo libbabeltrace-devel # for gdb
|
||||
readline-devel # for cmake and llvm
|
||||
libffi-devel libxml2-devel # for llvm
|
||||
libedit-devel pcre-devel automake bison # for swig
|
||||
@@ -21,6 +19,9 @@ TOOLCHAIN_BUILD_DEPS=(
|
||||
openssl-devel
|
||||
gmp-devel
|
||||
gperf
|
||||
diffutils
|
||||
libipt libipt-devel # intel
|
||||
patch
|
||||
)
|
||||
|
||||
TOOLCHAIN_RUN_DEPS=(
|
||||
@@ -31,18 +32,19 @@ TOOLCHAIN_RUN_DEPS=(
|
||||
readline # for cmake and llvm
|
||||
libffi libxml2 # for llvm
|
||||
openssl-devel
|
||||
perl # for openssl
|
||||
)
|
||||
|
||||
MEMGRAPH_BUILD_DEPS=(
|
||||
git # source code control
|
||||
make pkgconf-pkg-config # build system
|
||||
curl wget # for downloading libs
|
||||
wget # for downloading libs
|
||||
libuuid-devel java-11-openjdk # required by antlr
|
||||
readline-devel # for memgraph console
|
||||
python3-devel # for query modules
|
||||
openssl-devel
|
||||
libseccomp-devel
|
||||
python3 python3-virtualenv python3-pip nmap-ncat # for qa, macro_benchmark and stress tests
|
||||
python3 python3-pip python3-virtualenv nmap-ncat # for qa, macro_benchmark and stress tests
|
||||
#
|
||||
# IMPORTANT: python3-yaml does NOT exist on CentOS
|
||||
# Install it manually using `pip3 install PyYAML`
|
||||
@@ -73,12 +75,6 @@ check() {
|
||||
if [ "$pkg" == "python3-virtualenv" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == sbcl ]; then
|
||||
if ! sbcl --version &> /dev/null; then
|
||||
missing="$pkg $missing"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if ! yum list installed "$pkg" >/dev/null 2>/dev/null; then
|
||||
missing="$pkg $missing"
|
||||
fi
|
||||
@@ -105,13 +101,37 @@ install() {
|
||||
yum update -y
|
||||
yum install -y wget git python3 python3-pip
|
||||
for pkg in $1; do
|
||||
# Since there is no support for libipt-devel for CentOS 9 we install
|
||||
# Fedoras version of same libs, they are the same version but released
|
||||
# for different OS
|
||||
# TODO Update when libipt-devel releases for CentOS 9
|
||||
if [ "$pkg" == libipt ]; then
|
||||
if ! dnf list installed libipt >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://repo.okay.com.mx/centos/8/x86_64/release/libipt-1.6.1-8.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == libipt-devel ]; then
|
||||
if ! dnf list installed libipt-devel >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://repo.okay.com.mx/centos/8/x86_64/release/libipt-devel-1.6.1-8.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == libbabeltrace-devel ]; then
|
||||
if ! dnf list installed libbabeltrace-devel >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://mirror.stream.centos.org/9-stream/CRB/x86_64/os/Packages/libbabeltrace-devel-1.5.8-10.el9.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == sbcl ]; then
|
||||
if ! sbcl --version &> /dev/null; then
|
||||
curl -s https://altushost-swe.dl.sourceforge.net/project/sbcl/sbcl/1.4.2/sbcl-1.4.2-arm64-linux-binary.tar.bz2 -o /tmp/sbcl-arm64.tar.bz2
|
||||
tar xvjf /tmp/sbcl-arm64.tar.bz2 -C /tmp
|
||||
pushd /tmp/sbcl-1.4.2-arm64-linux
|
||||
INSTALL_ROOT=/usr/local sh install.sh
|
||||
popd
|
||||
if ! dnf list installed cl-asdf >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/cl-asdf-20101028-18.el8.noarch.rpm
|
||||
fi
|
||||
if ! dnf list installed common-lisp-controller >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/common-lisp-controller-7.4-20.el8.noarch.rpm
|
||||
fi
|
||||
if ! dnf list installed sbcl >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/sbcl-2.0.1-4.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
@@ -125,9 +145,11 @@ install() {
|
||||
fi
|
||||
if [ "$pkg" == python3-virtualenv ]; then
|
||||
if [ -z ${SUDO_USER+x} ]; then # Running as root (e.g. Docker).
|
||||
pip3 install --user virtualenv
|
||||
pip3 install virtualenv
|
||||
pip3 install virtualenvwrapper
|
||||
else # Running using sudo.
|
||||
sudo -H -u "$SUDO_USER" bash -c "pip3 install --user virtualenv"
|
||||
sudo -H -u "$SUDO_USER" bash -c "pip3 install virtualenv"
|
||||
sudo -H -u "$SUDO_USER" bash -c "pip3 install virtualenvwrapper"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -87,15 +87,6 @@ EOF
|
||||
fi
|
||||
apt install -y wget
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == dotnet-sdk-3.1 ]; then
|
||||
if ! dpkg -s "$pkg" 2>/dev/null >/dev/null; then
|
||||
wget -nv https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
|
||||
dpkg -i packages-microsoft-prod.deb
|
||||
apt-get update
|
||||
apt-get install -y apt-transport-https dotnet-sdk-3.1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
apt install -y "$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ EOF
|
||||
fi
|
||||
apt install -y wget
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == dotnet-sdk-3.1 ]; then
|
||||
if [ "$pkg" == dotnet-sdk-3.1 ]; then
|
||||
if ! dpkg -s "$pkg" 2>/dev/null >/dev/null; then
|
||||
wget -nv https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
|
||||
dpkg -i packages-microsoft-prod.deb
|
||||
|
||||
93
environment/os/ubuntu-22.04.sh
Executable file
93
environment/os/ubuntu-22.04.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc g++ build-essential make # generic build tools
|
||||
wget # used for archive download
|
||||
gnupg # used for archive signature verification
|
||||
tar gzip bzip2 xz-utils unzip # used for archive unpacking
|
||||
zlib1g-dev # zlib library used for all builds
|
||||
libexpat1-dev libipt-dev libbabeltrace-dev liblzma-dev python3-dev texinfo # for gdb
|
||||
libcurl4-openssl-dev # for cmake
|
||||
libreadline-dev # for cmake and llvm
|
||||
libffi-dev libxml2-dev # for llvm
|
||||
curl # snappy
|
||||
file
|
||||
git # for thrift
|
||||
libgmp-dev # for gdb
|
||||
gperf # for proxygen
|
||||
libssl-dev
|
||||
libedit-dev libpcre3-dev automake bison # for swig
|
||||
)
|
||||
|
||||
TOOLCHAIN_RUN_DEPS=(
|
||||
make # generic build tools
|
||||
tar gzip bzip2 xz-utils # used for archive unpacking
|
||||
zlib1g # zlib library used for all builds
|
||||
libexpat1 libipt2 libbabeltrace1 liblzma5 python3 # for gdb
|
||||
libcurl4 # for cmake
|
||||
libreadline8 # for cmake and llvm
|
||||
libffi7 libxml2 # for llvm
|
||||
libssl-dev # for libevent
|
||||
)
|
||||
|
||||
MEMGRAPH_BUILD_DEPS=(
|
||||
git # source code control
|
||||
make pkg-config # build system
|
||||
curl wget # for downloading libs
|
||||
uuid-dev default-jre-headless # required by antlr
|
||||
libreadline-dev # for memgraph console
|
||||
libpython3-dev python3-dev # for query modules
|
||||
libssl-dev
|
||||
libseccomp-dev
|
||||
netcat # tests are using nc to wait for memgraph
|
||||
python3 python3-virtualenv python3-pip # for qa, macro_benchmark and stress tests
|
||||
python3-yaml # for the configuration generator
|
||||
libcurl4-openssl-dev # mg-requests
|
||||
sbcl # for custom Lisp C++ preprocessing
|
||||
doxygen graphviz # source documentation generators
|
||||
mono-runtime mono-mcs zip unzip default-jdk-headless # for driver tests
|
||||
dotnet-sdk-6.0 golang nodejs npm
|
||||
autoconf # for jemalloc code generation
|
||||
libtool # for protobuf code generation
|
||||
)
|
||||
|
||||
list() {
|
||||
echo "$1"
|
||||
}
|
||||
|
||||
check() {
|
||||
check_all_dpkg "$1"
|
||||
}
|
||||
|
||||
install() {
|
||||
cd "$DIR"
|
||||
apt update
|
||||
# If GitHub Actions runner is installed, append LANG to the environment.
|
||||
# Python related tests doesn't work the LANG export.
|
||||
if [ -d "/home/gh/actions-runner" ]; then
|
||||
echo "LANG=en_US.utf8" >> /home/gh/actions-runner/.env
|
||||
else
|
||||
echo "NOTE: export LANG=en_US.utf8"
|
||||
fi
|
||||
apt install -y wget
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == dotnet-sdk-6.0 ]; then
|
||||
if ! dpkg -s dotnet-sdk-6.0 2>/dev/null >/dev/null; then
|
||||
wget -nv https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
|
||||
dpkg -i packages-microsoft-prod.deb
|
||||
apt-get update
|
||||
apt-get install -y apt-transport-https dotnet-sdk-6.0
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
apt install -y "$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
deps=$2"[*]"
|
||||
"$1" "${!deps}"
|
||||
@@ -675,7 +675,7 @@ PROXYGEN_SHA256=5360a8ccdfb2f5a6c7b3eed331ec7ab0e2c792d579c6fff499c85c516c11fe14
|
||||
SNAPPY_SHA256=75c1fbb3d618dd3a0483bff0e26d0a92b495bbe5059c8b4f1c962b478b6e06e7
|
||||
SNAPPY_VERSION=1.1.9
|
||||
XZ_VERSION=5.2.5 # for LZMA
|
||||
ZLIB_VERSION=1.2.11
|
||||
ZLIB_VERSION=1.2.12
|
||||
ZSTD_VERSION=1.5.0
|
||||
WANGLE_SHA256=1002e9c32b6f4837f6a760016e3b3e22f3509880ef3eaad191c80dc92655f23f
|
||||
|
||||
@@ -1178,12 +1178,21 @@ popd
|
||||
|
||||
# create toolchain archive
|
||||
if [ ! -f $NAME-binaries-$DISTRO.tar.gz ]; then
|
||||
DISTRO_FULL_NAME=$DISTRO
|
||||
if [ "$for_arm" = true ]; then
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-aarch64"
|
||||
DISTRO_FULL_NAME=${DISTRO}
|
||||
if [[ "${DISTRO}" == centos* ]]; then
|
||||
if [[ "$for_arm" = "true" ]]; then
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-aarch64"
|
||||
else
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-x86_64"
|
||||
fi
|
||||
else
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-x86_64"
|
||||
if [[ "$for_arm" = "true" ]]; then
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-arm64"
|
||||
else
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-amd64"
|
||||
fi
|
||||
fi
|
||||
|
||||
tar --owner=root --group=root -cpvzf $NAME-binaries-$DISTRO_FULL_NAME.tar.gz -C /opt $NAME
|
||||
fi
|
||||
|
||||
|
||||
4
init
4
init
@@ -24,7 +24,7 @@ function setup_virtualenv () {
|
||||
fi
|
||||
|
||||
# create new virtualenv
|
||||
virtualenv -p python3 ve3 || exit 1
|
||||
python3 -m virtualenv -p python3 ve3 || exit 1
|
||||
source ve3/bin/activate
|
||||
pip --timeout 1000 install -r requirements.txt || exit 1
|
||||
deactivate
|
||||
@@ -66,7 +66,7 @@ fi
|
||||
|
||||
DISTRO=$(operating_system)
|
||||
ARCHITECTURE=$(architecture)
|
||||
if [ "${ARCHITECTURE}" = "arm64" ]; then
|
||||
if [ "${ARCHITECTURE}" = "arm64" ] || [ "${ARCHITECTURE}" = "aarch64" ]; then
|
||||
OS_SCRIPT=$DIR/environment/os/$DISTRO-arm.sh
|
||||
else
|
||||
OS_SCRIPT=$DIR/environment/os/$DISTRO.sh
|
||||
|
||||
1
libs/.gitignore
vendored
1
libs/.gitignore
vendored
@@ -4,5 +4,4 @@
|
||||
!cleanup.sh
|
||||
!CMakeLists.txt
|
||||
!__main.cpp
|
||||
!jemalloc.cmake
|
||||
!pulsar.patch
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
set(JEMALLOC_DIR "${LIB_DIR}/jemalloc")
|
||||
|
||||
set(JEMALLOC_SRCS
|
||||
${JEMALLOC_DIR}/src/arena.c
|
||||
${JEMALLOC_DIR}/src/background_thread.c
|
||||
${JEMALLOC_DIR}/src/base.c
|
||||
${JEMALLOC_DIR}/src/bin.c
|
||||
${JEMALLOC_DIR}/src/bitmap.c
|
||||
${JEMALLOC_DIR}/src/ckh.c
|
||||
${JEMALLOC_DIR}/src/ctl.c
|
||||
${JEMALLOC_DIR}/src/div.c
|
||||
${JEMALLOC_DIR}/src/extent.c
|
||||
${JEMALLOC_DIR}/src/extent_dss.c
|
||||
${JEMALLOC_DIR}/src/extent_mmap.c
|
||||
${JEMALLOC_DIR}/src/hash.c
|
||||
${JEMALLOC_DIR}/src/hook.c
|
||||
${JEMALLOC_DIR}/src/jemalloc.c
|
||||
${JEMALLOC_DIR}/src/large.c
|
||||
${JEMALLOC_DIR}/src/log.c
|
||||
${JEMALLOC_DIR}/src/malloc_io.c
|
||||
${JEMALLOC_DIR}/src/mutex.c
|
||||
${JEMALLOC_DIR}/src/mutex_pool.c
|
||||
${JEMALLOC_DIR}/src/nstime.c
|
||||
${JEMALLOC_DIR}/src/pages.c
|
||||
${JEMALLOC_DIR}/src/prng.c
|
||||
${JEMALLOC_DIR}/src/prof.c
|
||||
${JEMALLOC_DIR}/src/rtree.c
|
||||
${JEMALLOC_DIR}/src/sc.c
|
||||
${JEMALLOC_DIR}/src/stats.c
|
||||
${JEMALLOC_DIR}/src/sz.c
|
||||
${JEMALLOC_DIR}/src/tcache.c
|
||||
${JEMALLOC_DIR}/src/test_hooks.c
|
||||
${JEMALLOC_DIR}/src/ticker.c
|
||||
${JEMALLOC_DIR}/src/tsd.c
|
||||
${JEMALLOC_DIR}/src/witness.c
|
||||
${JEMALLOC_DIR}/src/safety_check.c
|
||||
)
|
||||
|
||||
add_library(jemalloc ${JEMALLOC_SRCS})
|
||||
target_include_directories(jemalloc PUBLIC "${JEMALLOC_DIR}/include")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(jemalloc PUBLIC Threads::Threads)
|
||||
|
||||
target_compile_definitions(jemalloc PRIVATE -DJEMALLOC_NO_PRIVATE_NAMESPACE)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "DEBUG")
|
||||
target_compile_definitions(jemalloc PRIVATE -DJEMALLOC_DEBUG=1 -DJEMALLOC_PROF=1)
|
||||
endif()
|
||||
|
||||
target_compile_options(jemalloc PRIVATE -Wno-redundant-decls)
|
||||
# for RTLD_NEXT
|
||||
target_compile_definitions(jemalloc PRIVATE _GNU_SOURCE)
|
||||
|
||||
set_property(TARGET jemalloc APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS USE_JEMALLOC=1)
|
||||
@@ -7,13 +7,11 @@ import copy
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def procedure(context: mgp.ProcCtx,
|
||||
required_arg: mgp.Nullable[mgp.Any],
|
||||
optional_arg: mgp.Nullable[mgp.Any] = None
|
||||
) -> mgp.Record(args=list,
|
||||
vertex_count=int,
|
||||
avg_degree=mgp.Number,
|
||||
props=mgp.Nullable[mgp.Map]):
|
||||
def procedure(
|
||||
context: mgp.ProcCtx,
|
||||
required_arg: mgp.Nullable[mgp.Any],
|
||||
optional_arg: mgp.Nullable[mgp.Any] = None,
|
||||
) -> mgp.Record(args=list, vertex_count=int, avg_degree=mgp.Number, props=mgp.Nullable[mgp.Map]):
|
||||
"""
|
||||
This example procedure returns 4 fields.
|
||||
|
||||
@@ -37,7 +35,7 @@ def procedure(context: mgp.ProcCtx,
|
||||
if isinstance(required_arg, (mgp.Edge, mgp.Vertex)):
|
||||
props = dict(required_arg.properties.items())
|
||||
elif isinstance(required_arg, mgp.Path):
|
||||
start_vertex, = required_arg.vertices
|
||||
(start_vertex,) = required_arg.vertices
|
||||
props = dict(start_vertex.properties.items())
|
||||
# Count the vertices and edges in the database; this may take a while.
|
||||
vertex_count = 0
|
||||
@@ -51,15 +49,13 @@ def procedure(context: mgp.ProcCtx,
|
||||
# Copy the received arguments to make it equivalent to the C example.
|
||||
args_copy = [copy.deepcopy(required_arg), copy.deepcopy(optional_arg)]
|
||||
# Multiple rows can be produced by returning an iterable of mgp.Record.
|
||||
return mgp.Record(args=args_copy, vertex_count=vertex_count,
|
||||
avg_degree=avg_degree, props=props)
|
||||
return mgp.Record(args=args_copy, vertex_count=vertex_count, avg_degree=avg_degree, props=props)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def write_procedure(context: mgp.ProcCtx,
|
||||
property_name: str,
|
||||
property_value: mgp.Nullable[mgp.Any]
|
||||
) -> mgp.Record(created_vertex=mgp.Vertex):
|
||||
def write_procedure(
|
||||
context: mgp.ProcCtx, property_name: str, property_value: mgp.Nullable[mgp.Any]
|
||||
) -> mgp.Record(created_vertex=mgp.Vertex):
|
||||
"""
|
||||
This example procedure creates a new vertex with the specified property
|
||||
and connects it to all existing vertex which has the same property with
|
||||
|
||||
@@ -4,15 +4,17 @@ from collections import OrderedDict
|
||||
from itertools import chain, repeat
|
||||
from inspect import cleandoc
|
||||
from typing import List, Tuple
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write((
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use graph_analyzer '
|
||||
'module. Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n'))
|
||||
sys.stderr.write(
|
||||
(
|
||||
"\n"
|
||||
"NOTE: Please install networkx to be able to use graph_analyzer "
|
||||
"module. Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
)
|
||||
raise import_error
|
||||
# Imported last because it also depends on networkx.
|
||||
from mgp_networkx import MemgraphMultiDiGraph # noqa E402
|
||||
@@ -23,16 +25,14 @@ _MAX_LIST_SIZE = 10
|
||||
|
||||
@mgp.read_proc
|
||||
def help() -> mgp.Record(name=str, value=str):
|
||||
'''Shows manual page for graph_analyzer.'''
|
||||
"""Shows manual page for graph_analyzer."""
|
||||
records = []
|
||||
|
||||
def make_records(name, doc):
|
||||
return (mgp.Record(name=n, value=v) for n, v in
|
||||
zip(chain([name], repeat('')), cleandoc(doc).splitlines()))
|
||||
return (mgp.Record(name=n, value=v) for n, v in zip(chain([name], repeat("")), cleandoc(doc).splitlines()))
|
||||
|
||||
for func in (help, analyze, analyze_subgraph):
|
||||
records.extend(make_records("Procedure '{}'".format(func.__name__),
|
||||
func.__doc__))
|
||||
records.extend(make_records("Procedure '{}'".format(func.__name__), func.__doc__))
|
||||
|
||||
for m, v in _get_analysis_mapping().items():
|
||||
records.extend(make_records("Analysis '{}'".format(m), v.__doc__))
|
||||
@@ -41,10 +41,8 @@ def help() -> mgp.Record(name=str, value=str):
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def analyze(context: mgp.ProcCtx,
|
||||
analyses: mgp.Nullable[List[str]] = None
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
'''
|
||||
def analyze(context: mgp.ProcCtx, analyses: mgp.Nullable[List[str]] = None) -> mgp.Record(name=str, value=str):
|
||||
"""
|
||||
Shows graph information.
|
||||
|
||||
In case of multiple results, only the first 10 will be shown.
|
||||
@@ -57,19 +55,20 @@ def analyze(context: mgp.ProcCtx,
|
||||
|
||||
Example call (with parameter):
|
||||
CALL graph_analyzer.analyze(['nodes', 'edges']) YIELD *;
|
||||
'''
|
||||
"""
|
||||
g = MemgraphMultiDiGraph(ctx=context)
|
||||
recs = _analyze_graph(context, g, analyses)
|
||||
return [mgp.Record(name=name, value=value) for name, value in recs]
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def analyze_subgraph(context: mgp.ProcCtx,
|
||||
vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge],
|
||||
analyses: mgp.Nullable[List[str]] = None
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
'''
|
||||
def analyze_subgraph(
|
||||
context: mgp.ProcCtx,
|
||||
vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge],
|
||||
analyses: mgp.Nullable[List[str]] = None,
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
"""
|
||||
Shows subgraph information.
|
||||
|
||||
In case of multiple results, only the first 10 will be shown.
|
||||
@@ -91,36 +90,40 @@ def analyze_subgraph(context: mgp.ProcCtx,
|
||||
CALL graph_analyzer.analyze_subgraph(nodes, edges, ['nodes', 'edges'])
|
||||
YIELD *
|
||||
RETURN name, value;
|
||||
'''
|
||||
"""
|
||||
vertices, edges = map(set, [vertices, edges])
|
||||
g = nx.subgraph_view(
|
||||
MemgraphMultiDiGraph(ctx=context),
|
||||
lambda n: n in vertices,
|
||||
lambda n1, n2, e: e in edges)
|
||||
lambda n1, n2, e: e in edges,
|
||||
)
|
||||
recs = _analyze_graph(context, g, analyses)
|
||||
return [mgp.Record(name=name, value=value) for name, value in recs]
|
||||
|
||||
|
||||
def _get_analysis_mapping():
|
||||
return OrderedDict([
|
||||
('nodes', _number_of_nodes),
|
||||
('edges', _number_of_edges),
|
||||
('bridges', _bridges),
|
||||
('articulation_points', _articulation_points),
|
||||
('avg_degree', _avg_degree),
|
||||
('sorted_nodes_degree', _sorted_nodes_degree),
|
||||
('self_loops', _self_loops),
|
||||
('is_bipartite', _is_bipartite),
|
||||
('is_planar', _is_planar),
|
||||
('is_biconnected: ', _is_biconnected),
|
||||
('is_weakly_connected', _is_weakly_connected),
|
||||
('number_of_weakly_components', _weakly_components),
|
||||
('is_strongly_connected', _is_strongly_connected),
|
||||
('strongly_components', _strongly_components),
|
||||
('is_dag', _is_dag),
|
||||
('is_eulerian', _is_eulerian),
|
||||
('is_forest', _is_forest),
|
||||
('is_tree', _is_tree)])
|
||||
return OrderedDict(
|
||||
[
|
||||
("nodes", _number_of_nodes),
|
||||
("edges", _number_of_edges),
|
||||
("bridges", _bridges),
|
||||
("articulation_points", _articulation_points),
|
||||
("avg_degree", _avg_degree),
|
||||
("sorted_nodes_degree", _sorted_nodes_degree),
|
||||
("self_loops", _self_loops),
|
||||
("is_bipartite", _is_bipartite),
|
||||
("is_planar", _is_planar),
|
||||
("is_biconnected: ", _is_biconnected),
|
||||
("is_weakly_connected", _is_weakly_connected),
|
||||
("number_of_weakly_components", _weakly_components),
|
||||
("is_strongly_connected", _is_strongly_connected),
|
||||
("strongly_components", _strongly_components),
|
||||
("is_dag", _is_dag),
|
||||
("is_eulerian", _is_eulerian),
|
||||
("is_forest", _is_forest),
|
||||
("is_tree", _is_tree),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _get_analysis_func(name: str):
|
||||
@@ -132,20 +135,15 @@ def _get_analysis_funcs():
|
||||
return _get_analysis_mapping().values()
|
||||
|
||||
|
||||
def _analyze_graph(context: mgp.ProcCtx,
|
||||
g: nx.MultiDiGraph,
|
||||
analyses: List[str]
|
||||
) -> List[Tuple[str, str]]:
|
||||
def _analyze_graph(context: mgp.ProcCtx, g: nx.MultiDiGraph, analyses: List[str]) -> List[Tuple[str, str]]:
|
||||
|
||||
functions = (_get_analysis_funcs() if analyses is None
|
||||
else [_get_analysis_func(name) for name in analyses])
|
||||
functions = _get_analysis_funcs() if analyses is None else [_get_analysis_func(name) for name in analyses]
|
||||
|
||||
records = []
|
||||
for index, f in enumerate(functions):
|
||||
context.check_must_abort()
|
||||
if f is None:
|
||||
raise KeyError('Graph analysis is not supported: ' +
|
||||
analyses[index])
|
||||
raise KeyError("Graph analysis is not supported: " + analyses[index])
|
||||
name, value = f(g)
|
||||
if isinstance(value, (list, set, tuple)):
|
||||
value = list(value)[:_MAX_LIST_SIZE]
|
||||
@@ -155,126 +153,120 @@ def _analyze_graph(context: mgp.ProcCtx,
|
||||
|
||||
|
||||
def _number_of_nodes(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
'''Returns number of nodes.'''
|
||||
return 'Number of nodes', nx.number_of_nodes(g)
|
||||
"""Returns number of nodes."""
|
||||
return "Number of nodes", nx.number_of_nodes(g)
|
||||
|
||||
|
||||
def _number_of_edges(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
'''Returns number of edges.'''
|
||||
return 'Number of edges', nx.number_of_edges(g)
|
||||
"""Returns number of edges."""
|
||||
return "Number of edges", nx.number_of_edges(g)
|
||||
|
||||
|
||||
def _avg_degree(g: nx.MultiDiGraph) -> Tuple[str, float]:
|
||||
'''Returns average degree.'''
|
||||
"""Returns average degree."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
_, number_of_edges = _number_of_edges(g)
|
||||
avg_degree = (0 if number_of_nodes == 0
|
||||
else number_of_edges / number_of_nodes)
|
||||
return 'Average degree', avg_degree
|
||||
avg_degree = 0 if number_of_nodes == 0 else number_of_edges / number_of_nodes
|
||||
return "Average degree", avg_degree
|
||||
|
||||
|
||||
def _sorted_nodes_degree(g: nx.MultiDiGraph) -> Tuple[str, List[int]]:
|
||||
'''Returns list of sorted nodes degree. [(node_id, degree), ...]'''
|
||||
"""Returns list of sorted nodes degree. [(node_id, degree), ...]"""
|
||||
nodes_degree = [(n, g.degree(n)) for n in g.nodes()]
|
||||
nodes_degree.sort(key=lambda x: x[1], reverse=True)
|
||||
return 'Sorted nodes degree', nodes_degree
|
||||
return "Sorted nodes degree", nodes_degree
|
||||
|
||||
|
||||
def _self_loops(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
'''Returns number of self loops.'''
|
||||
return 'Self loops', sum((1 if e[0] == e[1] else 0 for e in g.edges()))
|
||||
"""Returns number of self loops."""
|
||||
return "Self loops", sum((1 if e[0] == e[1] else 0 for e in g.edges()))
|
||||
|
||||
|
||||
def _is_bipartite(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Checks if graph is bipartite.'''
|
||||
"""Checks if graph is bipartite."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.bipartite.basic.is_bipartite(g))
|
||||
return 'Is bipartite', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.bipartite.basic.is_bipartite(g)
|
||||
return "Is bipartite", ret
|
||||
|
||||
|
||||
def _is_planar(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Checks if graph is planar.'''
|
||||
"""Checks if graph is planar."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.planarity.check_planarity(g)[0])
|
||||
return 'Is planar', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.planarity.check_planarity(g)[0]
|
||||
return "Is planar", ret
|
||||
|
||||
|
||||
def _is_biconnected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Check if graph is biconnected.'''
|
||||
"""Check if graph is biconnected."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.is_biconnected(nx.MultiDiGraph.to_undirected(g)))
|
||||
return 'Is biconnected', ret
|
||||
ret = False if number_of_nodes == 0 else nx.is_biconnected(nx.MultiDiGraph.to_undirected(g))
|
||||
return "Is biconnected", ret
|
||||
|
||||
|
||||
def _is_weakly_connected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Check if graph is weakly connected.'''
|
||||
"""Check if graph is weakly connected."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.is_weakly_connected(g)
|
||||
return 'Is weakly connected', ret
|
||||
return "Is weakly connected", ret
|
||||
|
||||
|
||||
def _is_strongly_connected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Checks if graph is strongly connected.'''
|
||||
"""Checks if graph is strongly connected."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.is_strongly_connected(g)
|
||||
return 'Is strongly connected', ret
|
||||
return "Is strongly connected", ret
|
||||
|
||||
|
||||
def _is_dag(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Check if graph is directed acyclic graph (DAG)'''
|
||||
"""Check if graph is directed acyclic graph (DAG)"""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.dag.is_directed_acyclic_graph(g))
|
||||
return 'Is DAG', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.dag.is_directed_acyclic_graph(g)
|
||||
return "Is DAG", ret
|
||||
|
||||
|
||||
def _is_eulerian(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Checks if graph is Eulerian.'''
|
||||
"""Checks if graph is Eulerian."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.euler.is_eulerian(g))
|
||||
return 'Is eulerian', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.euler.is_eulerian(g)
|
||||
return "Is eulerian", ret
|
||||
|
||||
|
||||
def _is_forest(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Checks if graph is forest, all components must be trees.'''
|
||||
"""Checks if graph is forest, all components must be trees."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.tree.recognition.is_forest(g))
|
||||
return 'Is forest', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.tree.recognition.is_forest(g)
|
||||
return "Is forest", ret
|
||||
|
||||
|
||||
def _is_tree(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
'''Checks if graph is tree.'''
|
||||
"""Checks if graph is tree."""
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.tree.recognition.is_tree(g))
|
||||
return 'Is tree', ret
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.tree.recognition.is_tree(g)
|
||||
return "Is tree", ret
|
||||
|
||||
|
||||
def _bridges(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
'''Returns number of bridges, multiple edges between same nodes are
|
||||
mapped to one edge.'''
|
||||
return 'Number of bridges', sum(1 for _ in nx.bridges(nx.Graph(g)))
|
||||
"""Returns number of bridges, multiple edges between same nodes are
|
||||
mapped to one edge."""
|
||||
return "Number of bridges", sum(1 for _ in nx.bridges(nx.Graph(g)))
|
||||
|
||||
|
||||
def _articulation_points(g: nx.MultiDiGraph):
|
||||
'''Returns number of articulation points.'''
|
||||
"""Returns number of articulation points."""
|
||||
undirected = nx.MultiDiGraph.to_undirected(g)
|
||||
return ('Number of articulation points',
|
||||
sum(1 for _ in nx.articulation_points(undirected)))
|
||||
return (
|
||||
"Number of articulation points",
|
||||
sum(1 for _ in nx.articulation_points(undirected)),
|
||||
)
|
||||
|
||||
|
||||
def _weakly_components(g: nx.MultiDiGraph):
|
||||
'''Returns number of weakly components.'''
|
||||
"""Returns number of weakly components."""
|
||||
comps = nx.algorithms.components.number_weakly_connected_components(g)
|
||||
return 'Number of weakly connected components', comps
|
||||
return "Number of weakly connected components", comps
|
||||
|
||||
|
||||
def _strongly_components(g: nx.MultiDiGraph):
|
||||
'''Returns number of strongly connected components.'''
|
||||
"""Returns number of strongly connected components."""
|
||||
comps = nx.algorithms.components.number_strongly_connected_components(g)
|
||||
return 'Number of strongly connected components', comps
|
||||
return "Number of strongly connected components", comps
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import sys
|
||||
import mgp
|
||||
import collections
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write((
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use Memgraph NetworkX '
|
||||
'wrappers. Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n'))
|
||||
sys.stderr.write(
|
||||
(
|
||||
"\n"
|
||||
"NOTE: Please install networkx to be able to use Memgraph NetworkX "
|
||||
"wrappers. Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
)
|
||||
raise import_error
|
||||
|
||||
|
||||
class MemgraphAdjlistOuterDict(collections.abc.Mapping):
|
||||
__slots__ = ('_ctx', '_succ', '_multi')
|
||||
__slots__ = ("_ctx", "_succ", "_multi")
|
||||
|
||||
def __init__(self, ctx, succ=True, multi=True):
|
||||
self._ctx = ctx
|
||||
@@ -24,8 +26,7 @@ class MemgraphAdjlistOuterDict(collections.abc.Mapping):
|
||||
def __getitem__(self, key):
|
||||
if key not in self:
|
||||
raise KeyError
|
||||
return MemgraphAdjlistInnerDict(key, succ=self._succ,
|
||||
multi=self._multi)
|
||||
return MemgraphAdjlistInnerDict(key, succ=self._succ, multi=self._multi)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._ctx.graph.vertices)
|
||||
@@ -40,7 +41,7 @@ class MemgraphAdjlistOuterDict(collections.abc.Mapping):
|
||||
|
||||
|
||||
class MemgraphAdjlistInnerDict(collections.abc.Mapping):
|
||||
__slots__ = ('_node', '_succ', '_multi', '_neighbors')
|
||||
__slots__ = ("_node", "_succ", "_multi", "_neighbors")
|
||||
|
||||
def __init__(self, node, succ=True, multi=True):
|
||||
self._node = node
|
||||
@@ -71,31 +72,26 @@ class MemgraphAdjlistInnerDict(collections.abc.Mapping):
|
||||
def _get_neighbors(self):
|
||||
if not self._neighbors:
|
||||
if self._succ:
|
||||
self._neighbors = set(
|
||||
e.to_vertex for e in self._node.out_edges)
|
||||
self._neighbors = set(e.to_vertex for e in self._node.out_edges)
|
||||
else:
|
||||
self._neighbors = set(
|
||||
e.from_vertex for e in self._node.in_edges)
|
||||
self._neighbors = set(e.from_vertex for e in self._node.in_edges)
|
||||
return self._neighbors
|
||||
|
||||
def _get_edge(self, neighbor):
|
||||
if self._succ:
|
||||
edge = list(filter(lambda e: e.to_vertex == neighbor,
|
||||
self._node.out_edges))
|
||||
edge = list(filter(lambda e: e.to_vertex == neighbor, self._node.out_edges))
|
||||
else:
|
||||
edge = list(filter(lambda e: e.from_vertex == neighbor,
|
||||
self._node.in_edges))
|
||||
edge = list(filter(lambda e: e.from_vertex == neighbor, self._node.in_edges))
|
||||
|
||||
assert len(edge) >= 1
|
||||
if len(edge) > 1:
|
||||
raise RuntimeError('Graph contains multiedges but '
|
||||
'is of non-multigraph type: {}'.format(edge))
|
||||
raise RuntimeError("Graph contains multiedges but " "is of non-multigraph type: {}".format(edge))
|
||||
|
||||
return edge[0]
|
||||
|
||||
|
||||
class MemgraphEdgeKeyDict(collections.abc.Mapping):
|
||||
__slots__ = ('_node', '_neighbor', '_succ', '_edges')
|
||||
__slots__ = ("_node", "_neighbor", "_succ", "_edges")
|
||||
|
||||
def __init__(self, node, neighbor, succ=True):
|
||||
self._node = node
|
||||
@@ -122,18 +118,14 @@ class MemgraphEdgeKeyDict(collections.abc.Mapping):
|
||||
def _get_edges(self):
|
||||
if not self._edges:
|
||||
if self._succ:
|
||||
self._edges = list(filter(
|
||||
lambda e: e.to_vertex == self._neighbor,
|
||||
self._node.out_edges))
|
||||
self._edges = list(filter(lambda e: e.to_vertex == self._neighbor, self._node.out_edges))
|
||||
else:
|
||||
self._edges = list(filter(
|
||||
lambda e: e.from_vertex == self._neighbor,
|
||||
self._node.in_edges))
|
||||
self._edges = list(filter(lambda e: e.from_vertex == self._neighbor, self._node.in_edges))
|
||||
return self._edges
|
||||
|
||||
|
||||
class UnhashableProperties(collections.abc.Mapping):
|
||||
__slots__ = ('_properties')
|
||||
__slots__ = "_properties"
|
||||
|
||||
def __init__(self, properties):
|
||||
self._properties = properties
|
||||
@@ -155,7 +147,7 @@ class UnhashableProperties(collections.abc.Mapping):
|
||||
|
||||
|
||||
class MemgraphNodeDict(collections.abc.Mapping):
|
||||
__slots__ = ('_ctx',)
|
||||
__slots__ = ("_ctx",)
|
||||
|
||||
def __init__(self, ctx):
|
||||
self._ctx = ctx
|
||||
@@ -187,8 +179,7 @@ class MemgraphNodeDict(collections.abc.Mapping):
|
||||
|
||||
|
||||
class MemgraphDiGraphBase:
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, multi=True,
|
||||
**kwargs):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, multi=True, **kwargs):
|
||||
# NOTE: We assume that our graph will never be given any initial data
|
||||
# because we already pull our data from the Memgraph database. This
|
||||
# assert is triggered by certain NetworkX procedures because they
|
||||
@@ -201,23 +192,30 @@ class MemgraphDiGraphBase:
|
||||
# modify the graph's internal attributes and don't try to populate it
|
||||
# with initial data or modify it.
|
||||
|
||||
self.node_dict_factory = lambda: MemgraphNodeDict(ctx) \
|
||||
if ctx else self._error
|
||||
self.node_dict_factory = lambda: MemgraphNodeDict(ctx) if ctx else self._error
|
||||
self.node_attr_dict_factory = self._error
|
||||
|
||||
self.adjlist_outer_dict_factory = \
|
||||
lambda: MemgraphAdjlistOuterDict(ctx, multi=multi) \
|
||||
if ctx else self._error
|
||||
self.adjlist_outer_dict_factory = lambda: MemgraphAdjlistOuterDict(ctx, multi=multi) if ctx else self._error
|
||||
self.adjlist_inner_dict_factory = self._error
|
||||
self.edge_key_dict_factory = self._error
|
||||
self.edge_attr_dict_factory = self._error
|
||||
|
||||
# NOTE: We forbid any mutating operations because our graph is
|
||||
# immutable and pulls its data from the Memgraph database.
|
||||
for f in ['add_node', 'add_nodes_from', 'remove_node',
|
||||
'remove_nodes_from', 'add_edge', 'add_edges_from',
|
||||
'add_weighted_edges_from', 'new_edge_key', 'remove_edge',
|
||||
'remove_edges_from', 'update', 'clear']:
|
||||
for f in [
|
||||
"add_node",
|
||||
"add_nodes_from",
|
||||
"remove_node",
|
||||
"remove_nodes_from",
|
||||
"add_edge",
|
||||
"add_edges_from",
|
||||
"add_weighted_edges_from",
|
||||
"new_edge_key",
|
||||
"remove_edge",
|
||||
"remove_edges_from",
|
||||
"update",
|
||||
"clear",
|
||||
]:
|
||||
setattr(self, f, lambda *args, **kwargs: self._error())
|
||||
|
||||
super().__init__(None, **kwargs)
|
||||
@@ -231,33 +229,29 @@ class MemgraphDiGraphBase:
|
||||
self._pred = MemgraphAdjlistOuterDict(ctx, succ=False, multi=multi)
|
||||
|
||||
def _error(self):
|
||||
raise RuntimeError('Modification operations are not supported')
|
||||
raise RuntimeError("Modification operations are not supported")
|
||||
|
||||
|
||||
class MemgraphMultiDiGraph(MemgraphDiGraphBase, nx.MultiDiGraph):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, **kwargs):
|
||||
super().__init__(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, multi=True, **kwargs)
|
||||
super().__init__(incoming_graph_data=incoming_graph_data, ctx=ctx, multi=True, **kwargs)
|
||||
|
||||
|
||||
def MemgraphMultiGraph(incoming_graph_data=None, ctx=None, **kwargs):
|
||||
return MemgraphMultiDiGraph(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
return MemgraphMultiDiGraph(incoming_graph_data=incoming_graph_data, ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
|
||||
|
||||
class MemgraphDiGraph(MemgraphDiGraphBase, nx.DiGraph):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, **kwargs):
|
||||
super().__init__(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, multi=False, **kwargs)
|
||||
super().__init__(incoming_graph_data=incoming_graph_data, ctx=ctx, multi=False, **kwargs)
|
||||
|
||||
|
||||
def MemgraphGraph(incoming_graph_data=None, ctx=None, **kwargs):
|
||||
return MemgraphDiGraph(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
return MemgraphDiGraph(incoming_graph_data=incoming_graph_data, ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
|
||||
|
||||
class PropertiesDictionary(collections.abc.Mapping):
|
||||
__slots__ = ('_ctx', '_prop', '_len')
|
||||
__slots__ = ("_ctx", "_prop", "_len")
|
||||
|
||||
def __init__(self, ctx, prop):
|
||||
self._ctx = ctx
|
||||
@@ -270,8 +264,7 @@ class PropertiesDictionary(collections.abc.Mapping):
|
||||
try:
|
||||
return vertex.properties[self._prop]
|
||||
except KeyError:
|
||||
raise KeyError(("{} doesn\t have the required " +
|
||||
"property '{}'").format(vertex, self._prop))
|
||||
raise KeyError(("{} doesn\t have the required " + "property '{}'").format(vertex, self._prop))
|
||||
|
||||
def __iter__(self):
|
||||
for v in self._ctx.graph.vertices:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,20 @@
|
||||
import sys
|
||||
import mgp
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write(
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use wcc module.\n'
|
||||
'Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n')
|
||||
"\n" "NOTE: Please install networkx to be able to use wcc module.\n" "Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
raise import_error
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def get_components(vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge]
|
||||
) -> mgp.Record(n_components=int,
|
||||
components=mgp.List[mgp.List[mgp.Vertex]]):
|
||||
'''
|
||||
def get_components(
|
||||
vertices: mgp.List[mgp.Vertex], edges: mgp.List[mgp.Edge]
|
||||
) -> mgp.Record(n_components=int, components=mgp.List[mgp.List[mgp.Vertex]]):
|
||||
"""
|
||||
This procedure finds weakly connected components of a given subgraph of a
|
||||
directed graph.
|
||||
|
||||
@@ -41,7 +38,7 @@ def get_components(vertices: mgp.List[mgp.Vertex],
|
||||
WITH collect(n) AS nodes, collect(e) AS edges
|
||||
CALL wcc.get_components(nodes, edges) YIELD *
|
||||
RETURN n_components, components;
|
||||
'''
|
||||
"""
|
||||
g = nx.DiGraph()
|
||||
g.add_nodes_from(vertices)
|
||||
g.add_edges_from([(edge.from_vertex, edge.to_vertex) for edge in edges])
|
||||
|
||||
@@ -67,7 +67,7 @@ It aims to deliver developers the speed, simplicity and scale required to build
|
||||
the next generation of applications driver by real-time connected data.")
|
||||
# Add `openssl` package to dependencies list. Used to generate SSL certificates.
|
||||
# We also depend on `python3` because we embed it in Memgraph.
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.0.0, curl >= 7.29.0, python3 >= 3.5.0, libstdc >= 6")
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.0.0, curl >= 7.29.0, python3 >= 3.5.0, libstdc >= 6, logrotate")
|
||||
|
||||
# All variables must be set before including.
|
||||
include(CPack)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
FROM dokken/centos-stream-9
|
||||
# NOTE: If you change the base distro update release/package as well.
|
||||
|
||||
ARG release
|
||||
|
||||
RUN yum update && yum install -y \
|
||||
openssl libcurl libseccomp python3 python3-pip \
|
||||
--nobest --allowerasing \
|
||||
&& rm -rf /tmp/* \
|
||||
&& yum clean all
|
||||
|
||||
RUN pip3 install networkx==2.4 numpy==1.21.4 scipy==1.7.3
|
||||
|
||||
COPY ${release} /
|
||||
|
||||
# Install memgraph package
|
||||
RUN rpm -i ${release}
|
||||
|
||||
# Memgraph listens for Bolt Protocol on this port by default.
|
||||
EXPOSE 7687
|
||||
# Snapshots and logging volumes
|
||||
VOLUME /var/log/memgraph
|
||||
VOLUME /var/lib/memgraph
|
||||
# Configuration volume
|
||||
VOLUME /etc/memgraph
|
||||
|
||||
USER memgraph
|
||||
WORKDIR /usr/lib/memgraph
|
||||
|
||||
ENTRYPOINT ["/usr/lib/memgraph/memgraph"]
|
||||
CMD [""]
|
||||
@@ -104,7 +104,9 @@ def retry(retry_limit, timeout=100):
|
||||
except Exception:
|
||||
time.sleep(timeout)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner_func
|
||||
|
||||
|
||||
@@ -163,8 +165,15 @@ def format_version(variant, version, offering, distance=None, shorthash=None, su
|
||||
|
||||
# Parse arguments.
|
||||
parser = argparse.ArgumentParser(description="Get the current version of Memgraph.")
|
||||
parser.add_argument("--open-source", action="store_true", help="set the current offering to 'open-source'")
|
||||
parser.add_argument("version", help="manual version override, if supplied the version isn't " "determined using git")
|
||||
parser.add_argument(
|
||||
"--open-source",
|
||||
action="store_true",
|
||||
help="set the current offering to 'open-source'",
|
||||
)
|
||||
parser.add_argument(
|
||||
"version",
|
||||
help="manual version override, if supplied the version isn't " "determined using git",
|
||||
)
|
||||
parser.add_argument("suffix", help="custom suffix for the current version being built")
|
||||
parser.add_argument(
|
||||
"--variant",
|
||||
@@ -173,7 +182,9 @@ parser.add_argument(
|
||||
help="which variant of the version string should be generated",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--memgraph-root-dir", help="The root directory of the checked out " "Memgraph repository.", default="."
|
||||
"--memgraph-root-dir",
|
||||
help="The root directory of the checked out " "Memgraph repository.",
|
||||
default=".",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -256,14 +267,27 @@ for version in versions:
|
||||
if current_version is None:
|
||||
raise Exception("You are attempting to determine the version for a very " "old version of Memgraph!")
|
||||
version, branch, master_branch_merge = current_version
|
||||
distance = int(get_output("git", "rev-list", "--count", "--first-parent", master_branch_merge + ".." + current_hash))
|
||||
distance = int(
|
||||
get_output(
|
||||
"git",
|
||||
"rev-list",
|
||||
"--count",
|
||||
"--first-parent",
|
||||
master_branch_merge + ".." + current_hash,
|
||||
)
|
||||
)
|
||||
version_str = ".".join(map(str, version)) + ".0"
|
||||
if distance == 0:
|
||||
print(format_version(args.variant, version_str, offering, suffix=args.suffix), end="")
|
||||
else:
|
||||
print(
|
||||
format_version(
|
||||
args.variant, version_str, offering, distance=distance, shorthash=current_hash_short, suffix=args.suffix
|
||||
args.variant,
|
||||
version_str,
|
||||
offering,
|
||||
distance=distance,
|
||||
shorthash=current_hash_short,
|
||||
suffix=args.suffix,
|
||||
),
|
||||
end="",
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ RUN yum -y update \
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-centos-7.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-centos-7.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-centos-7.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-centos-7-x86_64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-centos-7-x86_64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-centos-7-x86_64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
FROM centos:8
|
||||
|
||||
ARG TOOLCHAIN_VERSION
|
||||
|
||||
RUN dnf -y update \
|
||||
&& dnf install -y wget git
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-centos-8.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-centos-8.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-centos-8.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
14
release/package/centos-9/Dockerfile
Normal file
14
release/package/centos-9/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM quay.io/centos/centos:stream9
|
||||
|
||||
ARG TOOLCHAIN_VERSION
|
||||
|
||||
RUN yum -y update \
|
||||
&& yum install -y wget git
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-centos-9-x86_64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-centos-9-x86_64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-centos-9-x86_64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
@@ -10,8 +10,8 @@ RUN apt update && apt install -y \
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-debian-10.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-10.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-10.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-debian-10-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-10-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-10-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
17
release/package/debian-11-arm/Dockerfile
Normal file
17
release/package/debian-11-arm/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM debian:11
|
||||
|
||||
ARG TOOLCHAIN_VERSION
|
||||
|
||||
# Stops tzdata interactive configuration.
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
ca-certificates wget git
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-debian-11-arm64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-11-arm64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-11-arm64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
@@ -10,8 +10,8 @@ RUN apt update && apt install -y \
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-debian-11.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-11.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-11.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-debian-11-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-11-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-11-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
@@ -4,10 +4,10 @@ services:
|
||||
build:
|
||||
context: centos-7
|
||||
container_name: "mgbuild_centos-7"
|
||||
mgbuild_centos-8:
|
||||
mgbuild_centos-9:
|
||||
build:
|
||||
context: centos-8
|
||||
container_name: "mgbuild_centos-8"
|
||||
context: centos-9
|
||||
container_name: "mgbuild_centos-9"
|
||||
mgbuild_debian-10:
|
||||
build:
|
||||
context: debian-10
|
||||
@@ -24,3 +24,7 @@ services:
|
||||
build:
|
||||
context: ubuntu-20.04
|
||||
container_name: "mgbuild_ubuntu-20.04"
|
||||
mgbuild_ubuntu-22.04:
|
||||
build:
|
||||
context: ubuntu-22.04
|
||||
container_name: "mgbuild_ubuntu-22.04"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
SUPPORTED_OS=(centos-7 centos-8 debian-10 debian-11 ubuntu-18.04 ubuntu-20.04)
|
||||
SUPPORTED_OS=(centos-7 centos-9 debian-10 debian-11 ubuntu-18.04 ubuntu-20.04 ubuntu-22.04 debian-11-arm)
|
||||
PROJECT_ROOT="$SCRIPT_DIR/../.."
|
||||
TOOLCHAIN_VERSION="toolchain-v4"
|
||||
ACTIVATE_TOOLCHAIN="source /opt/${TOOLCHAIN_VERSION}/activate"
|
||||
@@ -67,14 +67,18 @@ make_package () {
|
||||
# environment/os/{os}.sh does not come within the toolchain package. When
|
||||
# migrating to the next version of toolchain do that, and remove the
|
||||
# TOOLCHAIN_RUN_DEPS installation from here.
|
||||
echo "Installing dependencies..."
|
||||
echo "Installing dependencies using '/memgraph/environment/os/$os.sh' script..."
|
||||
docker exec "$build_container" bash -c "/memgraph/environment/os/$os.sh install TOOLCHAIN_RUN_DEPS"
|
||||
docker exec "$build_container" bash -c "/memgraph/environment/os/$os.sh install MEMGRAPH_BUILD_DEPS"
|
||||
|
||||
echo "Building targeted package..."
|
||||
docker exec "$build_container" bash -c "cd /memgraph && $ACTIVATE_TOOLCHAIN && ./init"
|
||||
docker exec "$build_container" bash -c "cd $container_build_dir && rm -rf ./*"
|
||||
docker exec "$build_container" bash -c "cd $container_build_dir && $ACTIVATE_TOOLCHAIN && cmake -DCMAKE_BUILD_TYPE=release $telemetry_id_override_flag .."
|
||||
if [[ "$os" == "debian-11-arm" ]]; then
|
||||
docker exec "$build_container" bash -c "cd $container_build_dir && $ACTIVATE_TOOLCHAIN && cmake -DCMAKE_BUILD_TYPE=release -DMG_ARCH="ARM64" $telemetry_id_override_flag .."
|
||||
else
|
||||
docker exec "$build_container" bash -c "cd $container_build_dir && $ACTIVATE_TOOLCHAIN && cmake -DCMAKE_BUILD_TYPE=release $telemetry_id_override_flag .."
|
||||
fi
|
||||
# ' is used instead of " because we need to run make within the allowed
|
||||
# container resources.
|
||||
# shellcheck disable=SC2016
|
||||
|
||||
@@ -10,8 +10,8 @@ RUN apt update && apt install -y \
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
@@ -10,8 +10,8 @@ RUN apt update && apt install -y \
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
17
release/package/ubuntu-22.04/Dockerfile
Normal file
17
release/package/ubuntu-22.04/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ARG TOOLCHAIN_VERSION
|
||||
|
||||
# Stops tzdata interactive configuration.
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
ca-certificates wget git
|
||||
# Do NOT be smart here and clean the cache because the container is used in the
|
||||
# stateful context.
|
||||
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-ubuntu-22.04-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-22.04-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-22.04-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
@@ -84,6 +84,8 @@ std::string PermissionToString(Permission permission) {
|
||||
return "MODULE_WRITE";
|
||||
case Permission::WEBSOCKET:
|
||||
return "WEBSOCKET";
|
||||
case Permission::LABELS:
|
||||
return "LABELS";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,19 +185,107 @@ bool operator==(const Permissions &first, const Permissions &second) {
|
||||
|
||||
bool operator!=(const Permissions &first, const Permissions &second) { return !(first == second); }
|
||||
|
||||
LabelPermissions::LabelPermissions(const std::unordered_set<std::string> &grants,
|
||||
const std::unordered_set<std::string> &denies)
|
||||
: grants_(grants), denies_(denies) {}
|
||||
|
||||
PermissionLevel LabelPermissions::Has(const std::string &permission) const {
|
||||
if (denies_.find(permission) != denies_.end()) {
|
||||
return PermissionLevel::DENY;
|
||||
}
|
||||
|
||||
if (grants_.find(permission) != denies_.end()) {
|
||||
return PermissionLevel::GRANT;
|
||||
}
|
||||
|
||||
return PermissionLevel::NEUTRAL;
|
||||
}
|
||||
|
||||
void LabelPermissions::Grant(const std::string &permission) {
|
||||
auto deniedPermissionIter = denies_.find(permission);
|
||||
|
||||
if (deniedPermissionIter != denies_.end()) {
|
||||
denies_.erase(deniedPermissionIter);
|
||||
}
|
||||
|
||||
if (grants_.find(permission) == grants_.end()) {
|
||||
grants_.insert(permission);
|
||||
}
|
||||
}
|
||||
|
||||
void LabelPermissions::Revoke(const std::string &permission) {
|
||||
auto deniedPermissionIter = denies_.find(permission);
|
||||
auto grantedPermissionIter = grants_.find(permission);
|
||||
|
||||
if (deniedPermissionIter != denies_.end()) {
|
||||
denies_.erase(deniedPermissionIter);
|
||||
}
|
||||
|
||||
if (grantedPermissionIter != grants_.end()) {
|
||||
grants_.erase(grantedPermissionIter);
|
||||
}
|
||||
}
|
||||
|
||||
void LabelPermissions::Deny(const std::string &permission) {
|
||||
auto grantedPermissionIter = grants_.find(permission);
|
||||
|
||||
if (grantedPermissionIter != grants_.end()) {
|
||||
grants_.erase(grantedPermissionIter);
|
||||
}
|
||||
|
||||
if (denies_.find(permission) == denies_.end()) {
|
||||
denies_.insert(permission);
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> LabelPermissions::GetGrants() const { return grants_; }
|
||||
|
||||
std::unordered_set<std::string> LabelPermissions::GetDenies() const { return denies_; }
|
||||
|
||||
nlohmann::json LabelPermissions::Serialize() const {
|
||||
nlohmann::json data = nlohmann::json::object();
|
||||
data["grants"] = grants_;
|
||||
data["denies"] = denies_;
|
||||
return data;
|
||||
}
|
||||
|
||||
LabelPermissions LabelPermissions::Deserialize(const nlohmann::json &data) {
|
||||
if (!data.is_object()) {
|
||||
throw AuthException("Couldn't load permissions data!");
|
||||
}
|
||||
|
||||
return {LabelPermissions(data["grants"], data["denies"])};
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> LabelPermissions::grants() const { return grants_; }
|
||||
std::unordered_set<std::string> LabelPermissions::denies() const { return denies_; }
|
||||
|
||||
bool operator==(const LabelPermissions &first, const LabelPermissions &second) {
|
||||
return first.grants() == second.grants() && first.denies() == second.denies();
|
||||
}
|
||||
|
||||
bool operator!=(const LabelPermissions &first, const LabelPermissions &second) { return !(first == second); }
|
||||
|
||||
Role::Role(const std::string &rolename) : rolename_(utils::ToLowerCase(rolename)) {}
|
||||
|
||||
Role::Role(const std::string &rolename, const Permissions &permissions)
|
||||
: rolename_(utils::ToLowerCase(rolename)), permissions_(permissions) {}
|
||||
|
||||
Role::Role(const std::string &rolename, const Permissions &permissions, const LabelPermissions &labelPermissions)
|
||||
: rolename_(utils::ToLowerCase(rolename)), permissions_(permissions), labelPermissions_(labelPermissions) {}
|
||||
|
||||
const std::string &Role::rolename() const { return rolename_; }
|
||||
const Permissions &Role::permissions() const { return permissions_; }
|
||||
Permissions &Role::permissions() { return permissions_; }
|
||||
|
||||
LabelPermissions &Role::labelPermissions() { return labelPermissions_; }
|
||||
|
||||
nlohmann::json Role::Serialize() const {
|
||||
nlohmann::json data = nlohmann::json::object();
|
||||
data["rolename"] = rolename_;
|
||||
data["permissions"] = permissions_.Serialize();
|
||||
data["labelPermissions"] = labelPermissions_.Serialize();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -207,7 +297,9 @@ Role Role::Deserialize(const nlohmann::json &data) {
|
||||
throw AuthException("Couldn't load role data!");
|
||||
}
|
||||
auto permissions = Permissions::Deserialize(data["permissions"]);
|
||||
return {data["rolename"], permissions};
|
||||
auto labelPermissions = LabelPermissions::Deserialize(data["labelPermissions"]);
|
||||
|
||||
return {data["rolename"], permissions, labelPermissions};
|
||||
}
|
||||
|
||||
bool operator==(const Role &first, const Role &second) {
|
||||
@@ -219,6 +311,13 @@ User::User(const std::string &username) : username_(utils::ToLowerCase(username)
|
||||
User::User(const std::string &username, const std::string &password_hash, const Permissions &permissions)
|
||||
: username_(utils::ToLowerCase(username)), password_hash_(password_hash), permissions_(permissions) {}
|
||||
|
||||
User::User(const std::string &username, const std::string &password_hash, const Permissions &permissions,
|
||||
const LabelPermissions &labelPermissions)
|
||||
: username_(utils::ToLowerCase(username)),
|
||||
password_hash_(password_hash),
|
||||
permissions_(permissions),
|
||||
labelPermissions_(labelPermissions) {}
|
||||
|
||||
bool User::CheckPassword(const std::string &password) {
|
||||
if (password_hash_.empty()) return true;
|
||||
return VerifyPassword(password, password_hash_);
|
||||
@@ -271,6 +370,8 @@ const std::string &User::username() const { return username_; }
|
||||
const Permissions &User::permissions() const { return permissions_; }
|
||||
Permissions &User::permissions() { return permissions_; }
|
||||
|
||||
LabelPermissions &User::labelPermissions() { return labelPermissions_; }
|
||||
|
||||
const Role *User::role() const {
|
||||
if (role_.has_value()) {
|
||||
return &role_.value();
|
||||
@@ -283,6 +384,7 @@ nlohmann::json User::Serialize() const {
|
||||
data["username"] = username_;
|
||||
data["password_hash"] = password_hash_;
|
||||
data["permissions"] = permissions_.Serialize();
|
||||
data["labelPermissions"] = labelPermissions_.Serialize();
|
||||
// The role shouldn't be serialized here, it is stored as a foreign key.
|
||||
return data;
|
||||
}
|
||||
@@ -295,11 +397,14 @@ User User::Deserialize(const nlohmann::json &data) {
|
||||
throw AuthException("Couldn't load user data!");
|
||||
}
|
||||
auto permissions = Permissions::Deserialize(data["permissions"]);
|
||||
return {data["username"], data["password_hash"], permissions};
|
||||
auto labelPermissions = LabelPermissions::Deserialize(data["labelPermissions"]);
|
||||
|
||||
return {data["username"], data["password_hash"], permissions, labelPermissions};
|
||||
}
|
||||
|
||||
bool operator==(const User &first, const User &second) {
|
||||
return first.username_ == second.username_ && first.password_hash_ == second.password_hash_ &&
|
||||
first.permissions_ == second.permissions_ && first.role_ == second.role_;
|
||||
}
|
||||
|
||||
} // namespace memgraph::auth
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <string>
|
||||
|
||||
#include <json/json.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace memgraph::auth {
|
||||
// These permissions must have values that are applicable for usage in a
|
||||
@@ -38,7 +39,8 @@ enum class Permission : uint64_t {
|
||||
STREAM = 1U << 17U,
|
||||
MODULE_READ = 1U << 18U,
|
||||
MODULE_WRITE = 1U << 19U,
|
||||
WEBSOCKET = 1U << 20U
|
||||
WEBSOCKET = 1U << 20U,
|
||||
LABELS = 1U << 21U
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
@@ -88,16 +90,52 @@ bool operator==(const Permissions &first, const Permissions &second);
|
||||
|
||||
bool operator!=(const Permissions &first, const Permissions &second);
|
||||
|
||||
class LabelPermissions final {
|
||||
public:
|
||||
LabelPermissions(const std::unordered_set<std::string> &grants = {},
|
||||
const std::unordered_set<std::string> &denies = {});
|
||||
|
||||
PermissionLevel Has(const std::string &permission) const;
|
||||
|
||||
void Grant(const std::string &permission);
|
||||
|
||||
void Revoke(const std::string &permission);
|
||||
|
||||
void Deny(const std::string &permission);
|
||||
|
||||
std::unordered_set<std::string> GetGrants() const;
|
||||
std::unordered_set<std::string> GetDenies() const;
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
/// @throw AuthException if unable to deserialize.
|
||||
static LabelPermissions Deserialize(const nlohmann::json &data);
|
||||
|
||||
std::unordered_set<std::string> grants() const;
|
||||
std::unordered_set<std::string> denies() const;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> grants_{};
|
||||
std::unordered_set<std::string> denies_{};
|
||||
};
|
||||
|
||||
bool operator==(const LabelPermissions &first, const LabelPermissions &second);
|
||||
|
||||
bool operator!=(const LabelPermissions &first, const LabelPermissions &second);
|
||||
class Role final {
|
||||
public:
|
||||
Role(const std::string &rolename);
|
||||
|
||||
Role(const std::string &rolename, const Permissions &permissions);
|
||||
|
||||
Role(const std::string &rolename, const Permissions &permissions, const LabelPermissions &labelPermissions);
|
||||
|
||||
const std::string &rolename() const;
|
||||
const Permissions &permissions() const;
|
||||
Permissions &permissions();
|
||||
|
||||
LabelPermissions &labelPermissions();
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
/// @throw AuthException if unable to deserialize.
|
||||
@@ -108,6 +146,7 @@ class Role final {
|
||||
private:
|
||||
std::string rolename_;
|
||||
Permissions permissions_;
|
||||
LabelPermissions labelPermissions_;
|
||||
};
|
||||
|
||||
bool operator==(const Role &first, const Role &second);
|
||||
@@ -119,6 +158,9 @@ class User final {
|
||||
|
||||
User(const std::string &username, const std::string &password_hash, const Permissions &permissions);
|
||||
|
||||
User(const std::string &username, const std::string &password_hash, const Permissions &permissions,
|
||||
const LabelPermissions &labelPermissions);
|
||||
|
||||
/// @throw AuthException if unable to verify the password.
|
||||
bool CheckPassword(const std::string &password);
|
||||
|
||||
@@ -138,6 +180,8 @@ class User final {
|
||||
|
||||
const Role *role() const;
|
||||
|
||||
LabelPermissions &labelPermissions();
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
/// @throw AuthException if unable to deserialize.
|
||||
@@ -150,7 +194,9 @@ class User final {
|
||||
std::string password_hash_;
|
||||
Permissions permissions_;
|
||||
std::optional<Role> role_;
|
||||
LabelPermissions labelPermissions_;
|
||||
};
|
||||
|
||||
bool operator==(const User &first, const User &second);
|
||||
|
||||
} // namespace memgraph::auth
|
||||
|
||||
@@ -18,19 +18,24 @@ roles_config = config["roles"]
|
||||
# Initialize LDAP server.
|
||||
tls = None
|
||||
if server_config["encryption"] != "disabled":
|
||||
cert_file = server_config["cert_file"] if server_config["cert_file"] \
|
||||
else None
|
||||
cert_file = server_config["cert_file"] if server_config["cert_file"] else None
|
||||
key_file = server_config["key_file"] if server_config["key_file"] else None
|
||||
ca_file = server_config["ca_file"] if server_config["ca_file"] else None
|
||||
validate = ssl.CERT_REQUIRED if server_config["validate_cert"] \
|
||||
else ssl.CERT_NONE
|
||||
tls = ldap3.Tls(local_private_key_file=key_file,
|
||||
local_certificate_file=cert_file,
|
||||
ca_certs_file=ca_file,
|
||||
validate=validate)
|
||||
validate = ssl.CERT_REQUIRED if server_config["validate_cert"] else ssl.CERT_NONE
|
||||
tls = ldap3.Tls(
|
||||
local_private_key_file=key_file,
|
||||
local_certificate_file=cert_file,
|
||||
ca_certs_file=ca_file,
|
||||
validate=validate,
|
||||
)
|
||||
use_ssl = server_config["encryption"] == "ssl"
|
||||
server = ldap3.Server(server_config["host"], port=server_config["port"],
|
||||
tls=tls, use_ssl=use_ssl, get_info=ldap3.ALL)
|
||||
server = ldap3.Server(
|
||||
server_config["host"],
|
||||
port=server_config["port"],
|
||||
tls=tls,
|
||||
use_ssl=use_ssl,
|
||||
get_info=ldap3.ALL,
|
||||
)
|
||||
|
||||
|
||||
# Main authentication/authorization function.
|
||||
@@ -40,14 +45,12 @@ def authenticate(username, password):
|
||||
return {"authenticated": False, "role": ""}
|
||||
|
||||
# Create the DN of the user
|
||||
dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + \
|
||||
users_config["suffix"]
|
||||
dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + users_config["suffix"]
|
||||
|
||||
# Bind to the server
|
||||
conn = ldap3.Connection(server, dn, password)
|
||||
if server_config["encryption"] == "starttls" and not conn.start_tls():
|
||||
print("ERROR: Couldn't issue STARTTLS to the LDAP server!",
|
||||
file=sys.stderr)
|
||||
print("ERROR: Couldn't issue STARTTLS to the LDAP server!", file=sys.stderr)
|
||||
return {"authenticated": False, "role": ""}
|
||||
if not conn.bind():
|
||||
return {"authenticated": False, "role": ""}
|
||||
@@ -56,25 +59,32 @@ def authenticate(username, password):
|
||||
if roles_config["root_dn"] != "":
|
||||
# search for role
|
||||
search_filter = "(&(objectclass={objclass})({attr}={value}))".format(
|
||||
objclass=roles_config["root_objectclass"],
|
||||
attr=roles_config["user_attribute"],
|
||||
value=ldap3.utils.conv.escape_filter_chars(dn))
|
||||
succ = conn.search(roles_config["root_dn"], search_filter,
|
||||
search_scope=ldap3.LEVEL,
|
||||
attributes=[roles_config["role_attribute"]])
|
||||
objclass=roles_config["root_objectclass"],
|
||||
attr=roles_config["user_attribute"],
|
||||
value=ldap3.utils.conv.escape_filter_chars(dn),
|
||||
)
|
||||
succ = conn.search(
|
||||
roles_config["root_dn"],
|
||||
search_filter,
|
||||
search_scope=ldap3.LEVEL,
|
||||
attributes=[roles_config["role_attribute"]],
|
||||
)
|
||||
if not succ or len(conn.entries) == 0:
|
||||
return {"authenticated": True, "role": ""}
|
||||
if len(conn.entries) > 1:
|
||||
roles = list(map(lambda x: x[roles_config["role_attribute"]].value,
|
||||
conn.entries))
|
||||
roles = list(map(lambda x: x[roles_config["role_attribute"]].value, conn.entries))
|
||||
# Because we don't know exactly which role the user should have
|
||||
# we authorize the user with an empty role.
|
||||
print("WARNING: Found more than one role for "
|
||||
"user '" + username + "':", ", ".join(roles) + "!",
|
||||
file=sys.stderr)
|
||||
print(
|
||||
"WARNING: Found more than one role for " "user '" + username + "':",
|
||||
", ".join(roles) + "!",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return {"authenticated": True, "role": ""}
|
||||
return {"authenticated": True,
|
||||
"role": conn.entries[0][roles_config["role_attribute"]].value}
|
||||
return {
|
||||
"authenticated": True,
|
||||
"role": conn.entries[0][roles_config["role_attribute"]].value,
|
||||
}
|
||||
else:
|
||||
return {"authenticated": True, "role": ""}
|
||||
|
||||
|
||||
@@ -105,9 +105,16 @@ class WebsocketSession : public std::enable_shared_from_this<WebsocketSession<TS
|
||||
boost::asio::socket_base::keep_alive option(true);
|
||||
|
||||
// Set a decorator to change the Server of the handshake
|
||||
ws_.set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::response_type &res) {
|
||||
ws_.set_option(boost::beast::websocket::stream_base::decorator([&req](boost::beast::websocket::response_type &res) {
|
||||
res.set(boost::beast::http::field::server, std::string("Memgraph Bolt WS"));
|
||||
res.set(boost::beast::http::field::sec_websocket_protocol, "binary");
|
||||
|
||||
// We need to do this to support WASM clients, which explicitly send this flag
|
||||
// in their upgrade request
|
||||
// Neo4j client breaks when this flag is sent
|
||||
if (const auto secondary_protocol = req.base().find(boost::beast::http::field::sec_websocket_protocol);
|
||||
secondary_protocol != res.base().end() && secondary_protocol->value() == "binary") {
|
||||
res.set(boost::beast::http::field::sec_websocket_protocol, "binary");
|
||||
}
|
||||
}));
|
||||
ws_.binary(true);
|
||||
|
||||
@@ -162,7 +169,7 @@ class WebsocketSession : public std::enable_shared_from_this<WebsocketSession<TS
|
||||
boost::asio::bind_executor(strand_, std::bind_front(&WebsocketSession::OnRead, shared_from_this())));
|
||||
}
|
||||
|
||||
void OnRead(const boost::system::error_code &ec, [[maybe_unused]] const size_t bytes_transferred) {
|
||||
void OnRead(const boost::system::error_code &ec, const size_t bytes_transferred) {
|
||||
// This indicates that the WebsocketSession was closed
|
||||
if (ec == boost::beast::websocket::error::closed) {
|
||||
return;
|
||||
@@ -322,7 +329,8 @@ class Session final : public std::enable_shared_from_this<Session<TSession, TSes
|
||||
socket.lowest_layer().non_blocking(false);
|
||||
});
|
||||
timeout_timer_.expires_at(boost::asio::steady_timer::time_point::max());
|
||||
spdlog::info("Accepted a connection from {}:", service_name_, remote_endpoint_.address(), remote_endpoint_.port());
|
||||
spdlog::info("Accepted a connection from {}: {}:{}", service_name_, remote_endpoint_.address(),
|
||||
remote_endpoint_.port());
|
||||
}
|
||||
|
||||
void DoRead() {
|
||||
|
||||
@@ -57,6 +57,8 @@ auth::Permission PrivilegeToPermission(query::AuthQuery::Privilege privilege) {
|
||||
return auth::Permission::MODULE_WRITE;
|
||||
case query::AuthQuery::Privilege::WEBSOCKET:
|
||||
return auth::Permission::WEBSOCKET;
|
||||
case query::AuthQuery::Privilege::LABELS:
|
||||
return auth::Permission::LABELS;
|
||||
}
|
||||
}
|
||||
} // namespace memgraph::glue
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
namespace memgraph::integrations {
|
||||
|
||||
inline constexpr int64_t kDefaultCheckBatchLimit{1};
|
||||
inline constexpr int64_t kMinimumStartBatchLimit{1};
|
||||
inline constexpr std::chrono::milliseconds kDefaultCheckTimeout{30000};
|
||||
inline constexpr std::chrono::milliseconds kMinimumInterval{1};
|
||||
inline constexpr int64_t kMinimumSize{1};
|
||||
|
||||
@@ -74,6 +74,36 @@ utils::BasicResult<std::string, std::vector<Message>> GetBatch(RdKafka::KafkaCon
|
||||
|
||||
return std::move(batch);
|
||||
}
|
||||
|
||||
void CheckAndDestroyLastAssignmentIfNeeded(RdKafka::KafkaConsumer &consumer, const ConsumerInfo &info,
|
||||
std::vector<RdKafka::TopicPartition *> &last_assignment) {
|
||||
if (!last_assignment.empty()) {
|
||||
if (const auto err = consumer.assign(last_assignment); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerStartFailedException(info.consumer_name,
|
||||
fmt::format("Couldn't restore commited offsets: '{}'", RdKafka::err2str(err)));
|
||||
}
|
||||
RdKafka::TopicPartition::destroy(last_assignment);
|
||||
}
|
||||
}
|
||||
|
||||
void TryToConsumeBatch(RdKafka::KafkaConsumer &consumer, const ConsumerInfo &info,
|
||||
const ConsumerFunction &consumer_function, const std::vector<Message> &batch) {
|
||||
consumer_function(batch);
|
||||
std::vector<RdKafka::TopicPartition *> partitions;
|
||||
utils::OnScopeExit clear_partitions([&]() { RdKafka::TopicPartition::destroy(partitions); });
|
||||
|
||||
if (const auto err = consumer.assignment(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCommitFailedException(
|
||||
info.consumer_name, fmt::format("Couldn't get assignment to commit offsets: {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer.position(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCommitFailedException(info.consumer_name,
|
||||
fmt::format("Couldn't get offsets from librdkafka {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer.commitSync(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCommitFailedException(info.consumer_name, RdKafka::err2str(err));
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Message::Message(std::unique_ptr<RdKafka::Message> &&message) : message_{std::move(message)} {
|
||||
@@ -221,10 +251,21 @@ void Consumer::Start() {
|
||||
StartConsuming();
|
||||
}
|
||||
|
||||
void Consumer::StartIfStopped() {
|
||||
if (!is_running_) {
|
||||
StartConsuming();
|
||||
void Consumer::StartWithLimit(const uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const {
|
||||
if (is_running_) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
if (limit_batches < kMinimumStartBatchLimit) {
|
||||
throw ConsumerStartFailedException(
|
||||
info_.consumer_name, fmt::format("Batch limit has to be greater than or equal to {}", kMinimumStartBatchLimit));
|
||||
}
|
||||
if (timeout.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
throw ConsumerStartFailedException(
|
||||
info_.consumer_name,
|
||||
fmt::format("Timeout has to be greater than or equal to {} milliseconds", kMinimumInterval.count()));
|
||||
}
|
||||
|
||||
StartConsumingWithLimit(limit_batches, timeout);
|
||||
}
|
||||
|
||||
void Consumer::Stop() {
|
||||
@@ -244,7 +285,7 @@ void Consumer::StopIfRunning() {
|
||||
}
|
||||
}
|
||||
|
||||
void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> limit_batches,
|
||||
void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> limit_batches,
|
||||
const ConsumerFunction &check_consumer_function) const {
|
||||
// NOLINTNEXTLINE (modernize-use-nullptr)
|
||||
if (timeout.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
@@ -344,13 +385,7 @@ void Consumer::StartConsuming() {
|
||||
|
||||
is_running_.store(true);
|
||||
|
||||
if (!last_assignment_.empty()) {
|
||||
if (const auto err = consumer_->assign(last_assignment_); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerStartFailedException(info_.consumer_name,
|
||||
fmt::format("Couldn't restore commited offsets: '{}'", RdKafka::err2str(err)));
|
||||
}
|
||||
RdKafka::TopicPartition::destroy(last_assignment_);
|
||||
}
|
||||
CheckAndDestroyLastAssignmentIfNeeded(*consumer_, info_, last_assignment_);
|
||||
|
||||
thread_ = std::thread([this] {
|
||||
static constexpr auto kMaxThreadNameSize = utils::GetMaxThreadNameSize();
|
||||
@@ -361,33 +396,18 @@ void Consumer::StartConsuming() {
|
||||
while (is_running_) {
|
||||
auto maybe_batch = GetBatch(*consumer_, info_, is_running_);
|
||||
if (maybe_batch.HasError()) {
|
||||
spdlog::warn("Error happened in consumer {} while fetching messages: {}!", info_.consumer_name,
|
||||
maybe_batch.GetError());
|
||||
break;
|
||||
throw ConsumerReadMessagesFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
}
|
||||
const auto &batch = maybe_batch.GetValue();
|
||||
|
||||
if (batch.empty()) continue;
|
||||
if (batch.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
spdlog::info("Kafka consumer {} is processing a batch", info_.consumer_name);
|
||||
|
||||
try {
|
||||
consumer_function_(batch);
|
||||
std::vector<RdKafka::TopicPartition *> partitions;
|
||||
utils::OnScopeExit clear_partitions([&]() { RdKafka::TopicPartition::destroy(partitions); });
|
||||
|
||||
if (const auto err = consumer_->assignment(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCheckFailedException(
|
||||
info_.consumer_name, fmt::format("Couldn't get assignment to commit offsets: {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer_->position(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCheckFailedException(
|
||||
info_.consumer_name, fmt::format("Couldn't get offsets from librdkafka {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer_->commitSync(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
spdlog::warn("Committing offset of consumer {} failed: {}", info_.consumer_name, RdKafka::err2str(err));
|
||||
break;
|
||||
}
|
||||
TryToConsumeBatch(*consumer_, info_, consumer_function_, batch);
|
||||
} catch (const std::exception &e) {
|
||||
spdlog::warn("Error happened in consumer {} while processing a batch: {}!", info_.consumer_name, e.what());
|
||||
break;
|
||||
@@ -398,6 +418,44 @@ void Consumer::StartConsuming() {
|
||||
});
|
||||
}
|
||||
|
||||
void Consumer::StartConsumingWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const {
|
||||
MG_ASSERT(!is_running_, "Cannot start already running consumer!");
|
||||
|
||||
if (is_running_.exchange(true)) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
utils::OnScopeExit restore_is_running([this] { is_running_.store(false); });
|
||||
|
||||
CheckAndDestroyLastAssignmentIfNeeded(*consumer_, info_, last_assignment_);
|
||||
|
||||
const auto timeout_to_use = timeout.value_or(kDefaultCheckTimeout);
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
|
||||
for (uint64_t batch_count = 0; batch_count < limit_batches;) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - start >= timeout_to_use) {
|
||||
throw ConsumerStartFailedException(info_.consumer_name, "Timeout reached");
|
||||
}
|
||||
|
||||
const auto maybe_batch = GetBatch(*consumer_, info_, is_running_);
|
||||
if (maybe_batch.HasError()) {
|
||||
throw ConsumerReadMessagesFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
}
|
||||
const auto &batch = maybe_batch.GetValue();
|
||||
|
||||
if (batch.empty()) {
|
||||
continue;
|
||||
}
|
||||
++batch_count;
|
||||
|
||||
spdlog::info("Kafka consumer {} is processing a batch", info_.consumer_name);
|
||||
|
||||
TryToConsumeBatch(*consumer_, info_, consumer_function_, batch);
|
||||
|
||||
spdlog::info("Kafka consumer {} finished processing", info_.consumer_name);
|
||||
}
|
||||
}
|
||||
|
||||
void Consumer::StopConsuming() {
|
||||
is_running_.store(false);
|
||||
if (thread_.joinable()) thread_.join();
|
||||
|
||||
@@ -113,11 +113,19 @@ class Consumer final : public RdKafka::EventCb {
|
||||
/// This method will start a new thread which will poll all the topics for messages.
|
||||
///
|
||||
/// @throws ConsumerRunningException if the consumer is already running
|
||||
/// @throws ConsumerStartFailedException if the commited offsets cannot be restored
|
||||
void Start();
|
||||
|
||||
/// Starts consuming messages if it is not started already.
|
||||
/// Starts consuming messages.
|
||||
///
|
||||
void StartIfStopped();
|
||||
/// This method will start a new thread which will poll all the topics for messages.
|
||||
///
|
||||
/// @param limit_batches the consumer will only consume the given number of batches.
|
||||
/// @param timeout the maximum duration during which the command should run.
|
||||
///
|
||||
/// @throws ConsumerRunningException if the consumer is already running
|
||||
/// @throws ConsumerStartFailedException if the commited offsets cannot be restored
|
||||
void StartWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
|
||||
/// Stops consuming messages.
|
||||
///
|
||||
@@ -136,9 +144,9 @@ class Consumer final : public RdKafka::EventCb {
|
||||
/// used.
|
||||
/// @param check_consumer_function a function to feed the received messages in, only used during this dry-run.
|
||||
///
|
||||
/// @throws ConsumerRunningException if the consumer is alredy running.
|
||||
/// @throws ConsumerRunningException if the consumer is already running.
|
||||
/// @throws ConsumerCheckFailedException if check isn't successful.
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> limit_batches,
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> limit_batches,
|
||||
const ConsumerFunction &check_consumer_function) const;
|
||||
|
||||
/// Returns true if the consumer is actively consuming messages.
|
||||
@@ -157,6 +165,7 @@ class Consumer final : public RdKafka::EventCb {
|
||||
void event_cb(RdKafka::Event &event) override;
|
||||
|
||||
void StartConsuming();
|
||||
void StartConsumingWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
|
||||
void StopConsuming();
|
||||
|
||||
@@ -178,7 +187,6 @@ class Consumer final : public RdKafka::EventCb {
|
||||
ConsumerFunction consumer_function_;
|
||||
mutable std::atomic<bool> is_running_{false};
|
||||
mutable std::vector<RdKafka::TopicPartition *> last_assignment_; // Protected by is_running_
|
||||
std::optional<int64_t> limit_batches_{std::nullopt};
|
||||
std::unique_ptr<RdKafka::KafkaConsumer, std::function<void(RdKafka::KafkaConsumer *)>> consumer_;
|
||||
std::thread thread_;
|
||||
ConsumerRebalanceCb cb_;
|
||||
|
||||
@@ -64,4 +64,16 @@ class TopicNotFoundException : public KafkaStreamException {
|
||||
TopicNotFoundException(const std::string_view consumer_name, const std::string_view topic_name)
|
||||
: KafkaStreamException("Kafka consumer {} cannot find topic {}", consumer_name, topic_name) {}
|
||||
};
|
||||
|
||||
class ConsumerCommitFailedException : public KafkaStreamException {
|
||||
public:
|
||||
ConsumerCommitFailedException(const std::string_view consumer_name, const std::string_view error)
|
||||
: KafkaStreamException("Committing offset of consumer {} failed: {}", consumer_name, error) {}
|
||||
};
|
||||
|
||||
class ConsumerReadMessagesFailedException : public KafkaStreamException {
|
||||
public:
|
||||
ConsumerReadMessagesFailedException(const std::string_view consumer_name, const std::string_view error)
|
||||
: KafkaStreamException("Error happened in consumer {} while fetching messages: {}", consumer_name, error) {}
|
||||
};
|
||||
} // namespace memgraph::integrations::kafka
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
|
||||
#include "integrations/pulsar/consumer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <pulsar/Client.h>
|
||||
#include <pulsar/InitialPosition.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "integrations/constants.hpp"
|
||||
#include "integrations/pulsar/exceptions.hpp"
|
||||
#include "utils/concepts.hpp"
|
||||
@@ -33,6 +34,10 @@ namespace {
|
||||
template <typename T>
|
||||
concept PulsarConsumer = utils::SameAsAnyOf<T, pulsar_client::Consumer, pulsar_client::Reader>;
|
||||
|
||||
template <typename TFunc>
|
||||
concept PulsarMessageGetter =
|
||||
std::same_as<const pulsar_client::Message &, std::invoke_result_t<TFunc, const Message &>>;
|
||||
|
||||
pulsar_client::Result ConsumeMessage(pulsar_client::Consumer &consumer, pulsar_client::Message &message,
|
||||
int remaining_timeout_in_ms) {
|
||||
return consumer.receive(message, remaining_timeout_in_ms);
|
||||
@@ -97,6 +102,26 @@ pulsar_client::Client CreateClient(const std::string &service_url) {
|
||||
conf.setLogger(new SpdlogLoggerFactory);
|
||||
return {service_url, conf};
|
||||
}
|
||||
|
||||
template <PulsarConsumer TConsumer, PulsarMessageGetter TPulsarMessageGetter>
|
||||
void TryToConsumeBatch(TConsumer &consumer, const ConsumerInfo &info, const ConsumerFunction &consumer_function,
|
||||
pulsar_client::MessageId &last_message_id, const std::vector<Message> &batch,
|
||||
const TPulsarMessageGetter &message_getter) {
|
||||
consumer_function(batch);
|
||||
|
||||
auto has_message_failed = [&consumer, &info, &last_message_id, &message_getter](const auto &message) {
|
||||
if (const auto result = consumer.acknowledge(message_getter(message)); result != pulsar_client::ResultOk) {
|
||||
spdlog::warn("Acknowledging a message of consumer {} failed: {}", info.consumer_name, result);
|
||||
return true;
|
||||
}
|
||||
last_message_id = message_getter(message).getMessageId();
|
||||
return false;
|
||||
};
|
||||
|
||||
if (std::ranges::any_of(batch, has_message_failed)) {
|
||||
throw ConsumerAcknowledgeMessagesFailedException(info.consumer_name);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Message::Message(pulsar_client::Message &&message) : message_{std::move(message)} {}
|
||||
@@ -137,6 +162,24 @@ void Consumer::Start() {
|
||||
StartConsuming();
|
||||
}
|
||||
|
||||
void Consumer::StartWithLimit(const uint64_t limit_batches,
|
||||
const std::optional<std::chrono::milliseconds> timeout) const {
|
||||
if (is_running_) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
if (limit_batches < kMinimumStartBatchLimit) {
|
||||
throw ConsumerStartFailedException(
|
||||
info_.consumer_name, fmt::format("Batch limit has to be greater than or equal to {}", kMinimumStartBatchLimit));
|
||||
}
|
||||
if (timeout.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
throw ConsumerStartFailedException(
|
||||
info_.consumer_name,
|
||||
fmt::format("Timeout has to be greater than or equal to {} milliseconds", kMinimumInterval.count()));
|
||||
}
|
||||
|
||||
StartConsumingWithLimit(limit_batches, timeout);
|
||||
}
|
||||
|
||||
void Consumer::Stop() {
|
||||
if (!is_running_) {
|
||||
throw ConsumerStoppedException(info_.consumer_name);
|
||||
@@ -154,7 +197,7 @@ void Consumer::StopIfRunning() {
|
||||
}
|
||||
}
|
||||
|
||||
void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> limit_batches,
|
||||
void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> limit_batches,
|
||||
const ConsumerFunction &check_consumer_function) const {
|
||||
// NOLINTNEXTLINE (modernize-use-nullptr)
|
||||
if (timeout.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
@@ -240,9 +283,7 @@ void Consumer::StartConsuming() {
|
||||
auto maybe_batch = GetBatch(consumer_, info_, is_running_, last_message_id_);
|
||||
|
||||
if (maybe_batch.HasError()) {
|
||||
spdlog::warn("Error happened in consumer {} while fetching messages: {}!", info_.consumer_name,
|
||||
maybe_batch.GetError());
|
||||
break;
|
||||
throw ConsumerReadMessagesFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
}
|
||||
|
||||
const auto &batch = maybe_batch.GetValue();
|
||||
@@ -254,18 +295,8 @@ void Consumer::StartConsuming() {
|
||||
spdlog::info("Pulsar consumer {} is processing a batch", info_.consumer_name);
|
||||
|
||||
try {
|
||||
consumer_function_(batch);
|
||||
|
||||
if (std::any_of(batch.begin(), batch.end(), [&](const auto &message) {
|
||||
if (const auto result = consumer_.acknowledge(message.message_); result != pulsar_client::ResultOk) {
|
||||
spdlog::warn("Acknowledging a message of consumer {} failed: {}", info_.consumer_name, result);
|
||||
return true;
|
||||
}
|
||||
last_message_id_ = message.message_.getMessageId();
|
||||
return false;
|
||||
})) {
|
||||
break;
|
||||
}
|
||||
TryToConsumeBatch(consumer_, info_, consumer_function_, last_message_id_, batch,
|
||||
[&](const Message &message) -> const pulsar_client::Message & { return message.message_; });
|
||||
} catch (const std::exception &e) {
|
||||
spdlog::warn("Error happened in consumer {} while processing a batch: {}!", info_.consumer_name, e.what());
|
||||
break;
|
||||
@@ -277,6 +308,43 @@ void Consumer::StartConsuming() {
|
||||
});
|
||||
}
|
||||
|
||||
void Consumer::StartConsumingWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const {
|
||||
if (is_running_.exchange(true)) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
utils::OnScopeExit restore_is_running([this] { is_running_.store(false); });
|
||||
|
||||
const auto timeout_to_use = timeout.value_or(kDefaultCheckTimeout);
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
|
||||
for (uint64_t batch_count = 0; batch_count < limit_batches;) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - start >= timeout_to_use) {
|
||||
throw ConsumerCheckFailedException(info_.consumer_name, "Timeout reached");
|
||||
}
|
||||
|
||||
const auto maybe_batch = GetBatch(consumer_, info_, is_running_, last_message_id_);
|
||||
|
||||
if (maybe_batch.HasError()) {
|
||||
throw ConsumerReadMessagesFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
}
|
||||
|
||||
const auto &batch = maybe_batch.GetValue();
|
||||
|
||||
if (batch.empty()) {
|
||||
continue;
|
||||
}
|
||||
++batch_count;
|
||||
|
||||
spdlog::info("Pulsar consumer {} is processing a batch", info_.consumer_name);
|
||||
|
||||
TryToConsumeBatch(consumer_, info_, consumer_function_, last_message_id_, batch,
|
||||
[](const Message &message) -> const pulsar_client::Message & { return message.message_; });
|
||||
|
||||
spdlog::info("Pulsar consumer {} finished processing", info_.consumer_name);
|
||||
}
|
||||
}
|
||||
|
||||
void Consumer::StopConsuming() {
|
||||
is_running_.store(false);
|
||||
if (thread_.joinable()) {
|
||||
|
||||
@@ -58,25 +58,27 @@ class Consumer final {
|
||||
|
||||
bool IsRunning() const;
|
||||
void Start();
|
||||
void StartWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
void Stop();
|
||||
void StopIfRunning();
|
||||
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> limit_batches,
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> limit_batches,
|
||||
const ConsumerFunction &check_consumer_function) const;
|
||||
|
||||
const ConsumerInfo &Info() const;
|
||||
|
||||
private:
|
||||
void StartConsuming();
|
||||
void StartConsumingWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
void StopConsuming();
|
||||
|
||||
ConsumerInfo info_;
|
||||
mutable pulsar_client::Client client_;
|
||||
pulsar_client::Consumer consumer_;
|
||||
mutable pulsar_client::Consumer consumer_;
|
||||
ConsumerFunction consumer_function_;
|
||||
|
||||
mutable std::atomic<bool> is_running_{false};
|
||||
pulsar_client::MessageId last_message_id_{pulsar_client::MessageId::earliest()};
|
||||
mutable pulsar_client::MessageId last_message_id_{pulsar_client::MessageId::earliest()}; // Protected by is_running_
|
||||
std::thread thread_;
|
||||
};
|
||||
} // namespace memgraph::integrations::pulsar
|
||||
|
||||
@@ -55,4 +55,16 @@ class TopicNotFoundException : public PulsarStreamException {
|
||||
TopicNotFoundException(const std::string &consumer_name, const std::string &topic_name)
|
||||
: PulsarStreamException("Pulsar consumer {} cannot find topic {}", consumer_name, topic_name) {}
|
||||
};
|
||||
|
||||
class ConsumerReadMessagesFailedException : public PulsarStreamException {
|
||||
public:
|
||||
ConsumerReadMessagesFailedException(const std::string_view consumer_name, const std::string_view error)
|
||||
: PulsarStreamException("Error happened in consumer {} while fetching messages: {}", consumer_name, error) {}
|
||||
};
|
||||
|
||||
class ConsumerAcknowledgeMessagesFailedException : public PulsarStreamException {
|
||||
public:
|
||||
explicit ConsumerAcknowledgeMessagesFailedException(const std::string_view consumer_name)
|
||||
: PulsarStreamException("Acknowledging a message of consumer {} has failed!", consumer_name) {}
|
||||
};
|
||||
} // namespace memgraph::integrations::pulsar
|
||||
|
||||
@@ -501,7 +501,7 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
|
||||
if (first_user) {
|
||||
spdlog::info("{} is first created user. Granting all privileges.", username);
|
||||
GrantPrivilege(username, memgraph::query::kPrivilegesAll);
|
||||
GrantPrivilege(username, memgraph::query::kPrivilegesAll, {"*"});
|
||||
}
|
||||
|
||||
return user_added;
|
||||
@@ -747,8 +747,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void GrantPrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) override {
|
||||
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
|
||||
// TODO (mferencevic): should we first check that the
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -757,8 +758,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void DenyPrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) override {
|
||||
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
|
||||
// TODO (mferencevic): should we first check that the
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -767,8 +769,9 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void RevokePrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) override {
|
||||
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
|
||||
// TODO (mferencevic): should we first check that the
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -779,7 +782,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
private:
|
||||
template <class TEditFun>
|
||||
void EditPermissions(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges, const TEditFun &edit_fun) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels, const TEditFun &edit_fun) {
|
||||
if (!std::regex_match(user_or_role, name_regex_)) {
|
||||
throw memgraph::query::QueryRuntimeException("Invalid user or role name.");
|
||||
}
|
||||
@@ -799,11 +803,17 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
for (const auto &permission : permissions) {
|
||||
edit_fun(&user->permissions(), permission);
|
||||
}
|
||||
for (const auto &label : labels) {
|
||||
edit_fun(&user->labelPermissions(), label);
|
||||
}
|
||||
locked_auth->SaveUser(*user);
|
||||
} else {
|
||||
for (const auto &permission : permissions) {
|
||||
edit_fun(&role->permissions(), permission);
|
||||
}
|
||||
for (const auto &label : labels) {
|
||||
edit_fun(&role->labelPermissions(), label);
|
||||
}
|
||||
locked_auth->SaveRole(*role);
|
||||
}
|
||||
} catch (const memgraph::auth::AuthException &e) {
|
||||
|
||||
@@ -9,4 +9,5 @@ target_link_libraries(mg-memory mg-utils fmt)
|
||||
|
||||
if (ENABLE_JEMALLOC)
|
||||
target_link_libraries(mg-memory Jemalloc::Jemalloc)
|
||||
target_compile_definitions(mg-memory PRIVATE USE_JEMALLOC=1)
|
||||
endif()
|
||||
|
||||
@@ -170,7 +170,7 @@ enum class CsvParserState {
|
||||
EXPECT_DELIMITER,
|
||||
};
|
||||
|
||||
bool SubstringStartsWith(const std::string_view &str, size_t pos, const std::string_view &what) {
|
||||
bool SubstringStartsWith(const std::string_view str, size_t pos, const std::string_view what) {
|
||||
return memgraph::utils::StartsWith(memgraph::utils::Substr(str, pos), what);
|
||||
}
|
||||
|
||||
|
||||
@@ -310,11 +310,11 @@ class DbAccessor final {
|
||||
return std::make_optional<VertexAccessor>(*value);
|
||||
}
|
||||
|
||||
storage::PropertyId NameToProperty(const std::string_view &name) { return accessor_->NameToProperty(name); }
|
||||
storage::PropertyId NameToProperty(const std::string_view name) { return accessor_->NameToProperty(name); }
|
||||
|
||||
storage::LabelId NameToLabel(const std::string_view &name) { return accessor_->NameToLabel(name); }
|
||||
storage::LabelId NameToLabel(const std::string_view name) { return accessor_->NameToLabel(name); }
|
||||
|
||||
storage::EdgeTypeId NameToEdgeType(const std::string_view &name) { return accessor_->NameToEdgeType(name); }
|
||||
storage::EdgeTypeId NameToEdgeType(const std::string_view name) { return accessor_->NameToEdgeType(name); }
|
||||
|
||||
const std::string &PropertyToName(storage::PropertyId prop) const { return accessor_->PropertyToName(prop); }
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ const char *kInternalPropertyId = "__mg_id__";
|
||||
const char *kInternalVertexLabel = "__mg_vertex__";
|
||||
|
||||
/// A helper function that escapes label, edge type and property names.
|
||||
std::string EscapeName(const std::string_view &value) {
|
||||
std::string EscapeName(const std::string_view value) {
|
||||
std::string out;
|
||||
out.reserve(value.size() + 2);
|
||||
out.append(1, '`');
|
||||
|
||||
@@ -2239,9 +2239,11 @@ cpp<#
|
||||
(user "std::string" :scope :public)
|
||||
(role "std::string" :scope :public)
|
||||
(user-or-role "std::string" :scope :public)
|
||||
|
||||
(password "Expression *" :initval "nullptr" :scope :public
|
||||
:slk-save #'slk-save-ast-pointer
|
||||
:slk-load (slk-load-ast-pointer "Expression"))
|
||||
(labels "std::vector<std::string>" :scope :public)
|
||||
(privileges "std::vector<Privilege>" :scope :public))
|
||||
(:public
|
||||
(lcp:define-enum action
|
||||
@@ -2253,7 +2255,7 @@ cpp<#
|
||||
(lcp:define-enum privilege
|
||||
(create delete match merge set remove index stats auth constraint
|
||||
dump replication durability read_file free_memory trigger config stream module_read module_write
|
||||
websocket)
|
||||
websocket labels)
|
||||
(:serialize))
|
||||
#>cpp
|
||||
AuthQuery() = default;
|
||||
@@ -2264,13 +2266,14 @@ cpp<#
|
||||
#>cpp
|
||||
AuthQuery(Action action, std::string user, std::string role,
|
||||
std::string user_or_role, Expression *password,
|
||||
std::vector<Privilege> privileges)
|
||||
std::vector<std::string> labels ,std::vector<Privilege> privileges)
|
||||
: action_(action),
|
||||
user_(user),
|
||||
role_(role),
|
||||
user_or_role_(user_or_role),
|
||||
password_(password),
|
||||
privileges_(privileges) {}
|
||||
labels_(labels),
|
||||
privileges_(privileges){}
|
||||
cpp<#)
|
||||
(:private
|
||||
#>cpp
|
||||
@@ -2295,7 +2298,8 @@ const std::vector<AuthQuery::Privilege> kPrivilegesAll = {
|
||||
AuthQuery::Privilege::FREE_MEMORY, AuthQuery::Privilege::TRIGGER,
|
||||
AuthQuery::Privilege::CONFIG, AuthQuery::Privilege::STREAM,
|
||||
AuthQuery::Privilege::MODULE_READ, AuthQuery::Privilege::MODULE_WRITE,
|
||||
AuthQuery::Privilege::WEBSOCKET};
|
||||
AuthQuery::Privilege::WEBSOCKET,
|
||||
AuthQuery::Privilege::LABELS};
|
||||
cpp<#
|
||||
|
||||
(lcp:define-class info-query (query)
|
||||
@@ -2391,6 +2395,9 @@ cpp<#
|
||||
(lcp:define-enum sync-mode
|
||||
(sync async)
|
||||
(:serialize))
|
||||
(lcp:define-enum replica-state
|
||||
(ready replicating recovery invalid)
|
||||
(:serialize))
|
||||
#>cpp
|
||||
ReplicationQuery() = default;
|
||||
|
||||
|
||||
@@ -775,6 +775,23 @@ antlrcpp::Any CypherMainVisitor::visitDropStream(MemgraphCypher::DropStreamConte
|
||||
antlrcpp::Any CypherMainVisitor::visitStartStream(MemgraphCypher::StartStreamContext *ctx) {
|
||||
auto *stream_query = storage_->Create<StreamQuery>();
|
||||
stream_query->action_ = StreamQuery::Action::START_STREAM;
|
||||
|
||||
if (ctx->BATCH_LIMIT()) {
|
||||
if (!ctx->batchLimit->numberLiteral() || !ctx->batchLimit->numberLiteral()->integerLiteral()) {
|
||||
throw SemanticException("Batch limit should be an integer literal!");
|
||||
}
|
||||
stream_query->batch_limit_ = ctx->batchLimit->accept(this);
|
||||
}
|
||||
if (ctx->TIMEOUT()) {
|
||||
if (!ctx->timeout->numberLiteral() || !ctx->timeout->numberLiteral()->integerLiteral()) {
|
||||
throw SemanticException("Timeout should be an integer literal!");
|
||||
}
|
||||
if (!ctx->BATCH_LIMIT()) {
|
||||
throw SemanticException("Parameter TIMEOUT can only be defined if BATCH_LIMIT is defined");
|
||||
}
|
||||
stream_query->timeout_ = ctx->timeout->accept(this);
|
||||
}
|
||||
|
||||
stream_query->stream_name_ = ctx->streamName()->symbolicName()->accept(this).as<std::string>();
|
||||
return stream_query;
|
||||
}
|
||||
@@ -1268,7 +1285,11 @@ antlrcpp::Any CypherMainVisitor::visitGrantPrivilege(MemgraphCypher::GrantPrivil
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
for (auto *privilege : ctx->privilegeList()->privilege()) {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
if (privilege->LABELS()) {
|
||||
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
|
||||
} else {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* grant all privileges */
|
||||
@@ -1286,7 +1307,11 @@ antlrcpp::Any CypherMainVisitor::visitDenyPrivilege(MemgraphCypher::DenyPrivileg
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
for (auto *privilege : ctx->privilegeList()->privilege()) {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
if (privilege->LABELS()) {
|
||||
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
|
||||
} else {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* deny all privileges */
|
||||
@@ -1304,7 +1329,11 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege(MemgraphCypher::RevokePriv
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
for (auto *privilege : ctx->privilegeList()->privilege()) {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
if (privilege->LABELS()) {
|
||||
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
|
||||
} else {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* revoke all privileges */
|
||||
@@ -1313,6 +1342,22 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege(MemgraphCypher::RevokePriv
|
||||
return auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthQuery*
|
||||
*/
|
||||
antlrcpp::Any CypherMainVisitor::visitLabelList(MemgraphCypher::LabelListContext *ctx) {
|
||||
std::vector<std::string> labels;
|
||||
for (auto *label : ctx->label()) {
|
||||
if (label->ASTERISK()) {
|
||||
labels.push_back("*");
|
||||
} else {
|
||||
labels.push_back(label->symbolicName()->accept(this).as<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthQuery::Privilege
|
||||
*/
|
||||
@@ -1338,6 +1383,10 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege(MemgraphCypher::PrivilegeContext
|
||||
if (ctx->MODULE_READ()) return AuthQuery::Privilege::MODULE_READ;
|
||||
if (ctx->MODULE_WRITE()) return AuthQuery::Privilege::MODULE_WRITE;
|
||||
if (ctx->WEBSOCKET()) return AuthQuery::Privilege::WEBSOCKET;
|
||||
if (ctx->LABELS()) {
|
||||
// fill labels in authquery
|
||||
return AuthQuery::Privilege::LABELS;
|
||||
}
|
||||
LOG_FATAL("Should not get here - unknown privilege!");
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +473,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
|
||||
*/
|
||||
antlrcpp::Any visitPrivilege(MemgraphCypher::PrivilegeContext *ctx) override;
|
||||
|
||||
/**
|
||||
* @return AuthQuery::LabelList
|
||||
*/
|
||||
antlrcpp::Any visitLabelList(MemgraphCypher::LabelListContext *ctx) override;
|
||||
|
||||
/**
|
||||
* @return AuthQuery*
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,7 @@ memgraphCypherKeyword : cypherKeyword
|
||||
| IDENTIFIED
|
||||
| ISOLATION
|
||||
| KAFKA
|
||||
| LABELS
|
||||
| LEVEL
|
||||
| LOAD
|
||||
| LOCK
|
||||
@@ -254,10 +255,15 @@ privilege : CREATE
|
||||
| MODULE_READ
|
||||
| MODULE_WRITE
|
||||
| WEBSOCKET
|
||||
| LABELS labels=labelList
|
||||
;
|
||||
|
||||
privilegeList : privilege ( ',' privilege )* ;
|
||||
|
||||
labelList : COLON label ( ',' COLON label )* ;
|
||||
|
||||
label : ( '*' | symbolicName ) ;
|
||||
|
||||
showPrivileges : SHOW PRIVILEGES FOR userOrRole=userOrRoleName ;
|
||||
|
||||
showRoleForUser : SHOW ROLE FOR user=userOrRoleName ;
|
||||
@@ -351,7 +357,7 @@ pulsarCreateStream : CREATE PULSAR STREAM streamName ( pulsarCreateStreamConfig
|
||||
|
||||
dropStream : DROP STREAM streamName ;
|
||||
|
||||
startStream : START STREAM streamName ;
|
||||
startStream : START STREAM streamName ( BATCH_LIMIT batchLimit=literal ) ? ( TIMEOUT timeout=literal ) ? ;
|
||||
|
||||
startAllStreams : START ALL STREAMS ;
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ IDENTIFIED : I D E N T I F I E D ;
|
||||
IGNORE : I G N O R E ;
|
||||
ISOLATION : I S O L A T I O N ;
|
||||
KAFKA : K A F K A ;
|
||||
LABELS : L A B E L S ;
|
||||
LEVEL : L E V E L ;
|
||||
LOAD : L O A D ;
|
||||
LOCK : L O C K ;
|
||||
|
||||
@@ -204,8 +204,9 @@ const trie::Trie kKeywords = {"union",
|
||||
"pulsar",
|
||||
"service_url",
|
||||
"version",
|
||||
"websocket"
|
||||
"foreach"};
|
||||
"websocket",
|
||||
"foreach",
|
||||
"labels"};
|
||||
|
||||
// Unicode codepoints that are allowed at the start of the unescaped name.
|
||||
const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts(
|
||||
|
||||
@@ -716,7 +716,7 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
|
||||
}
|
||||
|
||||
template <class TRecordAccessor>
|
||||
storage::PropertyValue GetProperty(const TRecordAccessor &record_accessor, const std::string_view &name) {
|
||||
storage::PropertyValue GetProperty(const TRecordAccessor &record_accessor, const std::string_view name) {
|
||||
auto maybe_prop = record_accessor.GetProperty(view_, dba_->NameToProperty(name));
|
||||
if (maybe_prop.HasError() && maybe_prop.GetError() == storage::Error::NONEXISTENT_OBJECT) {
|
||||
// This is a very nasty and temporary hack in order to make MERGE work.
|
||||
|
||||
@@ -232,6 +232,25 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler {
|
||||
replica.timeout = *repl_info.timeout;
|
||||
}
|
||||
|
||||
replica.current_timestamp_of_replica = repl_info.timestamp_info.current_timestamp_of_replica;
|
||||
replica.current_number_of_timestamp_behind_master =
|
||||
repl_info.timestamp_info.current_number_of_timestamp_behind_master;
|
||||
|
||||
switch (repl_info.state) {
|
||||
case storage::replication::ReplicaState::READY:
|
||||
replica.state = ReplicationQuery::ReplicaState::READY;
|
||||
break;
|
||||
case storage::replication::ReplicaState::REPLICATING:
|
||||
replica.state = ReplicationQuery::ReplicaState::REPLICATING;
|
||||
break;
|
||||
case storage::replication::ReplicaState::RECOVERY:
|
||||
replica.state = ReplicationQuery::ReplicaState::RECOVERY;
|
||||
break;
|
||||
case storage::replication::ReplicaState::INVALID:
|
||||
replica.state = ReplicationQuery::ReplicaState::INVALID;
|
||||
break;
|
||||
}
|
||||
|
||||
return replica;
|
||||
};
|
||||
|
||||
@@ -263,6 +282,8 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
std::string rolename = auth_query->role_;
|
||||
std::string user_or_role = auth_query->user_or_role_;
|
||||
std::vector<AuthQuery::Privilege> privileges = auth_query->privileges_;
|
||||
std::vector<std::string> labels = auth_query->labels_;
|
||||
// std::vector<storage::LabelId> labels = NamesToLabels(labels, db_accessor);
|
||||
auto password = EvaluateOptionalExpression(auth_query->password_, &evaluator);
|
||||
|
||||
Callback callback;
|
||||
@@ -277,7 +298,8 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
|
||||
if (license_check_result.HasError() && enterprise_only_methods.contains(auth_query->action_)) {
|
||||
throw utils::BasicException(
|
||||
utils::license::LicenseCheckErrorToString(license_check_result.GetError(), "advanced authentication features"));
|
||||
utils::license::LicenseCheckErrorToString(license_check_result.GetError(), "advanced authentication
|
||||
features"));
|
||||
}
|
||||
|
||||
switch (auth_query->action_) {
|
||||
@@ -292,7 +314,7 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
// If the license is not valid we create users with admin access
|
||||
if (!valid_enterprise_license) {
|
||||
spdlog::warn("Granting all the privileges to {}.", username);
|
||||
auth->GrantPrivilege(username, kPrivilegesAll);
|
||||
auth->GrantPrivilege(username, kPrivilegesAll, {});
|
||||
}
|
||||
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
@@ -367,20 +389,20 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::GRANT_PRIVILEGE:
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->GrantPrivilege(user_or_role, privileges);
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->GrantPrivilege(user_or_role, privileges, labels);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::DENY_PRIVILEGE:
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->DenyPrivilege(user_or_role, privileges);
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->DenyPrivilege(user_or_role, privileges, labels);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::REVOKE_PRIVILEGE: {
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->RevokePrivilege(user_or_role, privileges);
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->RevokePrivilege(user_or_role, privileges, labels);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
@@ -476,6 +498,10 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
} else if (timeout.IsInt()) {
|
||||
maybe_timeout = static_cast<double>(timeout.ValueInt());
|
||||
}
|
||||
if (maybe_timeout && *maybe_timeout <= 0.0) {
|
||||
throw utils::BasicException("Parameter TIMEOUT must be strictly greater than 0.");
|
||||
}
|
||||
|
||||
callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, name, socket_address, sync_mode,
|
||||
maybe_timeout, replica_check_frequency]() mutable {
|
||||
handler.RegisterReplica(name, std::string(socket_address.ValueString()), sync_mode, maybe_timeout,
|
||||
@@ -486,6 +512,7 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
fmt::format("Replica {} is registered.", repl_query->replica_name_));
|
||||
return callback;
|
||||
}
|
||||
|
||||
case ReplicationQuery::Action::DROP_REPLICA: {
|
||||
const auto &name = repl_query->replica_name_;
|
||||
callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, name]() mutable {
|
||||
@@ -496,8 +523,15 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
fmt::format("Replica {} is dropped.", repl_query->replica_name_));
|
||||
return callback;
|
||||
}
|
||||
|
||||
case ReplicationQuery::Action::SHOW_REPLICAS: {
|
||||
callback.header = {"name", "socket_address", "sync_mode", "timeout"};
|
||||
callback.header = {"name",
|
||||
"socket_address",
|
||||
"sync_mode",
|
||||
"timeout",
|
||||
"current_timestamp_of_replica",
|
||||
"number_of_timestamp_behind_master",
|
||||
"state"};
|
||||
callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, replica_nfields = callback.header.size()] {
|
||||
const auto &replicas = handler.ShowReplicas();
|
||||
auto typed_replicas = std::vector<std::vector<TypedValue>>{};
|
||||
@@ -508,6 +542,7 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
|
||||
typed_replica.emplace_back(TypedValue(replica.name));
|
||||
typed_replica.emplace_back(TypedValue(replica.socket_address));
|
||||
|
||||
switch (replica.sync_mode) {
|
||||
case ReplicationQuery::SyncMode::SYNC:
|
||||
typed_replica.emplace_back(TypedValue("sync"));
|
||||
@@ -516,12 +551,32 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
typed_replica.emplace_back(TypedValue("async"));
|
||||
break;
|
||||
}
|
||||
|
||||
if (replica.timeout) {
|
||||
typed_replica.emplace_back(TypedValue(*replica.timeout));
|
||||
} else {
|
||||
typed_replica.emplace_back(TypedValue());
|
||||
}
|
||||
|
||||
typed_replica.emplace_back(TypedValue(static_cast<int64_t>(replica.current_timestamp_of_replica)));
|
||||
typed_replica.emplace_back(
|
||||
TypedValue(static_cast<int64_t>(replica.current_number_of_timestamp_behind_master)));
|
||||
|
||||
switch (replica.state) {
|
||||
case ReplicationQuery::ReplicaState::READY:
|
||||
typed_replica.emplace_back(TypedValue("ready"));
|
||||
break;
|
||||
case ReplicationQuery::ReplicaState::REPLICATING:
|
||||
typed_replica.emplace_back(TypedValue("replicating"));
|
||||
break;
|
||||
case ReplicationQuery::ReplicaState::RECOVERY:
|
||||
typed_replica.emplace_back(TypedValue("recovery"));
|
||||
break;
|
||||
case ReplicationQuery::ReplicaState::INVALID:
|
||||
typed_replica.emplace_back(TypedValue("invalid"));
|
||||
break;
|
||||
}
|
||||
|
||||
typed_replicas.emplace_back(std::move(typed_replica));
|
||||
}
|
||||
return typed_replicas;
|
||||
@@ -655,12 +710,26 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
return callback;
|
||||
}
|
||||
case StreamQuery::Action::START_STREAM: {
|
||||
callback.fn = [interpreter_context, stream_name = stream_query->stream_name_]() {
|
||||
interpreter_context->streams.Start(stream_name);
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::START_STREAM,
|
||||
fmt::format("Started stream {}.", stream_query->stream_name_));
|
||||
const auto batch_limit = GetOptionalValue<int64_t>(stream_query->batch_limit_, evaluator);
|
||||
const auto timeout = GetOptionalValue<std::chrono::milliseconds>(stream_query->timeout_, evaluator);
|
||||
|
||||
if (batch_limit.has_value()) {
|
||||
if (batch_limit.value() < 0) {
|
||||
throw utils::BasicException("Parameter BATCH_LIMIT cannot hold negative value");
|
||||
}
|
||||
|
||||
callback.fn = [interpreter_context, stream_name = stream_query->stream_name_, batch_limit, timeout]() {
|
||||
interpreter_context->streams.StartWithLimit(stream_name, static_cast<uint64_t>(batch_limit.value()), timeout);
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
} else {
|
||||
callback.fn = [interpreter_context, stream_name = stream_query->stream_name_]() {
|
||||
interpreter_context->streams.Start(stream_name);
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::START_STREAM,
|
||||
fmt::format("Started stream {}.", stream_query->stream_name_));
|
||||
}
|
||||
return callback;
|
||||
}
|
||||
case StreamQuery::Action::START_ALL_STREAMS: {
|
||||
@@ -730,9 +799,15 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
}
|
||||
case StreamQuery::Action::CHECK_STREAM: {
|
||||
callback.header = {"queries", "raw messages"};
|
||||
|
||||
const auto batch_limit = GetOptionalValue<int64_t>(stream_query->batch_limit_, evaluator);
|
||||
if (batch_limit.has_value() && batch_limit.value() < 0) {
|
||||
throw utils::BasicException("Parameter BATCH_LIMIT cannot hold negative value");
|
||||
}
|
||||
|
||||
callback.fn = [interpreter_context, stream_name = stream_query->stream_name_,
|
||||
timeout = GetOptionalValue<std::chrono::milliseconds>(stream_query->timeout_, evaluator),
|
||||
batch_limit = GetOptionalValue<int64_t>(stream_query->batch_limit_, evaluator)]() mutable {
|
||||
batch_limit]() mutable {
|
||||
return interpreter_context->streams.Check(stream_name, timeout, batch_limit);
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::CHECK_STREAM,
|
||||
|
||||
@@ -99,14 +99,16 @@ class AuthQueryHandler {
|
||||
virtual std::vector<std::vector<TypedValue>> GetPrivileges(const std::string &user_or_role) = 0;
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
virtual void GrantPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges) = 0;
|
||||
virtual void GrantPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) = 0;
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
virtual void DenyPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges) = 0;
|
||||
virtual void DenyPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) = 0;
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
virtual void RevokePrivilege(const std::string &user_or_role,
|
||||
const std::vector<AuthQuery::Privilege> &privileges) = 0;
|
||||
virtual void RevokePrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) = 0;
|
||||
};
|
||||
|
||||
enum class QueryHandlerResult { COMMIT, ABORT, NOTHING };
|
||||
@@ -127,6 +129,9 @@ class ReplicationQueryHandler {
|
||||
std::string socket_address;
|
||||
ReplicationQuery::SyncMode sync_mode;
|
||||
std::optional<double> timeout;
|
||||
uint64_t current_timestamp_of_replica;
|
||||
uint64_t current_number_of_timestamp_behind_master;
|
||||
ReplicationQuery::ReplicaState state;
|
||||
};
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
|
||||
@@ -3697,7 +3697,7 @@ std::unordered_map<std::string, int64_t> CallProcedure::GetAndResetCounters() {
|
||||
|
||||
namespace {
|
||||
|
||||
void CallCustomProcedure(const std::string_view &fully_qualified_procedure_name, const mgp_proc &proc,
|
||||
void CallCustomProcedure(const std::string_view fully_qualified_procedure_name, const mgp_proc &proc,
|
||||
const std::vector<Expression *> &args, mgp_graph &graph, ExpressionEvaluator *evaluator,
|
||||
utils::MemoryResource *memory, std::optional<size_t> memory_limit, mgp_result *result) {
|
||||
static_assert(std::uses_allocator_v<mgp_value, utils::Allocator<mgp_value>>,
|
||||
|
||||
@@ -1054,7 +1054,7 @@ std::unique_ptr<Module> LoadModuleFromFile(const std::filesystem::path &path) {
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ModuleRegistry::RegisterModule(const std::string_view &name, std::unique_ptr<Module> module) {
|
||||
bool ModuleRegistry::RegisterModule(const std::string_view name, std::unique_ptr<Module> module) {
|
||||
MG_ASSERT(!name.empty(), "Module name cannot be empty");
|
||||
MG_ASSERT(module, "Tried to register an invalid module");
|
||||
if (modules_.find(name) != modules_.end()) {
|
||||
@@ -1163,7 +1163,7 @@ void ModuleRegistry::UnloadAndLoadModulesFromDirectories() {
|
||||
}
|
||||
}
|
||||
|
||||
ModulePtr ModuleRegistry::GetModuleNamed(const std::string_view &name) const {
|
||||
ModulePtr ModuleRegistry::GetModuleNamed(const std::string_view name) const {
|
||||
std::shared_lock<utils::RWLock> guard(lock_);
|
||||
auto found_it = modules_.find(name);
|
||||
if (found_it == modules_.end()) return nullptr;
|
||||
|
||||
@@ -77,7 +77,7 @@ class ModuleRegistry final {
|
||||
mutable utils::RWLock lock_{utils::RWLock::Priority::WRITE};
|
||||
std::unique_ptr<utils::MemoryResource> shared_{std::make_unique<utils::ResourceWithOutOfMemoryException>()};
|
||||
|
||||
bool RegisterModule(const std::string_view &name, std::unique_ptr<Module> module);
|
||||
bool RegisterModule(std::string_view name, std::unique_ptr<Module> module);
|
||||
|
||||
void DoUnloadAllModules();
|
||||
|
||||
@@ -105,7 +105,7 @@ class ModuleRegistry final {
|
||||
///
|
||||
/// Return true if the module was loaded or reloaded successfully, false
|
||||
/// otherwise.
|
||||
bool LoadOrReloadModuleFromName(const std::string_view name);
|
||||
bool LoadOrReloadModuleFromName(std::string_view name);
|
||||
|
||||
/// Atomically unload all modules and then load all possible modules from the
|
||||
/// set directories.
|
||||
@@ -115,7 +115,7 @@ class ModuleRegistry final {
|
||||
|
||||
/// Find a module with given name or return nullptr.
|
||||
/// Takes a read lock.
|
||||
ModulePtr GetModuleNamed(const std::string_view &name) const;
|
||||
ModulePtr GetModuleNamed(std::string_view name) const;
|
||||
|
||||
/// Remove all loaded (non-builtin) modules.
|
||||
/// Takes a write lock.
|
||||
@@ -175,7 +175,7 @@ extern ModuleRegistry gModuleRegistry;
|
||||
/// inside this function. ModulePtr must be kept alive to make sure it won't be
|
||||
/// unloaded.
|
||||
std::optional<std::pair<procedure::ModulePtr, const mgp_proc *>> FindProcedure(
|
||||
const ModuleRegistry &module_registry, const std::string_view fully_qualified_procedure_name,
|
||||
const ModuleRegistry &module_registry, std::string_view fully_qualified_procedure_name,
|
||||
utils::MemoryResource *memory);
|
||||
|
||||
/// Return the ModulePtr and `mgp_trans *` of the found transformation after resolving
|
||||
@@ -183,7 +183,7 @@ std::optional<std::pair<procedure::ModulePtr, const mgp_proc *>> FindProcedure(
|
||||
/// inside this function. ModulePtr must be kept alive to make sure it won't be
|
||||
/// unloaded.
|
||||
std::optional<std::pair<procedure::ModulePtr, const mgp_trans *>> FindTransformation(
|
||||
const ModuleRegistry &module_registry, const std::string_view fully_qualified_transformation_name,
|
||||
const ModuleRegistry &module_registry, std::string_view fully_qualified_transformation_name,
|
||||
utils::MemoryResource *memory);
|
||||
|
||||
/// Return the ModulePtr and `mgp_func *` of the found function after resolving
|
||||
@@ -191,7 +191,7 @@ std::optional<std::pair<procedure::ModulePtr, const mgp_trans *>> FindTransforma
|
||||
/// std::nullopt is returned. `memory` is used for temporary allocations
|
||||
/// inside this function. ModulePtr must be kept alive to make sure it won't be unloaded.
|
||||
std::optional<std::pair<procedure::ModulePtr, const mgp_func *>> FindFunction(
|
||||
const ModuleRegistry &module_registry, const std::string_view fully_qualified_function_name,
|
||||
const ModuleRegistry &module_registry, std::string_view fully_qualified_function_name,
|
||||
utils::MemoryResource *memory);
|
||||
|
||||
template <typename T>
|
||||
|
||||
@@ -52,10 +52,11 @@ concept Stream = requires(TStream stream) {
|
||||
typename TStream::Message;
|
||||
TStream{std::string{""}, typename TStream::StreamInfo{}, ConsumerFunction<typename TStream::Message>{}};
|
||||
{ stream.Start() } -> std::same_as<void>;
|
||||
{ stream.StartWithLimit(uint64_t{}, std::optional<std::chrono::milliseconds>{}) } -> std::same_as<void>;
|
||||
{ stream.Stop() } -> std::same_as<void>;
|
||||
{ stream.IsRunning() } -> std::same_as<bool>;
|
||||
{
|
||||
stream.Check(std::optional<std::chrono::milliseconds>{}, std::optional<int64_t>{},
|
||||
stream.Check(std::optional<std::chrono::milliseconds>{}, std::optional<uint64_t>{},
|
||||
ConsumerFunction<typename TStream::Message>{})
|
||||
} -> std::same_as<void>;
|
||||
requires std::same_as<std::decay_t<decltype(std::declval<typename TStream::StreamInfo>().common_info)>,
|
||||
|
||||
@@ -44,10 +44,13 @@ KafkaStream::StreamInfo KafkaStream::Info(std::string transformation_name) const
|
||||
}
|
||||
|
||||
void KafkaStream::Start() { consumer_->Start(); }
|
||||
void KafkaStream::StartWithLimit(uint64_t batch_limit, std::optional<std::chrono::milliseconds> timeout) const {
|
||||
consumer_->StartWithLimit(batch_limit, timeout);
|
||||
}
|
||||
void KafkaStream::Stop() { consumer_->Stop(); }
|
||||
bool KafkaStream::IsRunning() const { return consumer_->IsRunning(); }
|
||||
|
||||
void KafkaStream::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> batch_limit,
|
||||
void KafkaStream::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> batch_limit,
|
||||
const ConsumerFunction<integrations::kafka::Message> &consumer_function) const {
|
||||
consumer_->Check(timeout, batch_limit, consumer_function);
|
||||
}
|
||||
@@ -106,10 +109,12 @@ PulsarStream::StreamInfo PulsarStream::Info(std::string transformation_name) con
|
||||
}
|
||||
|
||||
void PulsarStream::Start() { consumer_->Start(); }
|
||||
void PulsarStream::StartWithLimit(uint64_t batch_limit, std::optional<std::chrono::milliseconds> timeout) const {
|
||||
consumer_->StartWithLimit(batch_limit, timeout);
|
||||
}
|
||||
void PulsarStream::Stop() { consumer_->Stop(); }
|
||||
bool PulsarStream::IsRunning() const { return consumer_->IsRunning(); }
|
||||
|
||||
void PulsarStream::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> batch_limit,
|
||||
void PulsarStream::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> batch_limit,
|
||||
const ConsumerFunction<Message> &consumer_function) const {
|
||||
consumer_->Check(timeout, batch_limit, consumer_function);
|
||||
}
|
||||
|
||||
@@ -36,10 +36,11 @@ struct KafkaStream {
|
||||
StreamInfo Info(std::string transformation_name) const;
|
||||
|
||||
void Start();
|
||||
void StartWithLimit(uint64_t batch_limit, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
void Stop();
|
||||
bool IsRunning() const;
|
||||
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> batch_limit,
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> batch_limit,
|
||||
const ConsumerFunction<Message> &consumer_function) const;
|
||||
|
||||
utils::BasicResult<std::string> SetStreamOffset(int64_t offset);
|
||||
@@ -71,10 +72,11 @@ struct PulsarStream {
|
||||
StreamInfo Info(std::string transformation_name) const;
|
||||
|
||||
void Start();
|
||||
void StartWithLimit(uint64_t batch_limit, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
void Stop();
|
||||
bool IsRunning() const;
|
||||
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> batch_limit,
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> batch_limit,
|
||||
const ConsumerFunction<Message> &consumer_function) const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -456,7 +456,7 @@ void Streams::Create(const std::string &stream_name, typename TStream::StreamInf
|
||||
|
||||
try {
|
||||
std::visit(
|
||||
[&](auto &&stream_data) {
|
||||
[&](const auto &stream_data) {
|
||||
const auto stream_source_ptr = stream_data.stream_source->ReadLock();
|
||||
Persist(CreateStatus(stream_name, stream_data.transformation_name, stream_data.owner, *stream_source_ptr));
|
||||
},
|
||||
@@ -575,7 +575,7 @@ void Streams::RestoreStreams() {
|
||||
auto it = CreateConsumer<T>(*locked_streams_map, stream_name, std::move(status.info), std::move(status.owner));
|
||||
if (status.is_running) {
|
||||
std::visit(
|
||||
[&](auto &&stream_data) {
|
||||
[&](const auto &stream_data) {
|
||||
auto stream_source_ptr = stream_data.stream_source->Lock();
|
||||
stream_source_ptr->Start();
|
||||
},
|
||||
@@ -617,7 +617,7 @@ void Streams::Drop(const std::string &stream_name) {
|
||||
// function can be executing with the consumer, nothing else.
|
||||
// By acquiring the write lock here for the consumer, we make sure there is
|
||||
// no running Test function for this consumer, therefore it can be erased.
|
||||
std::visit([&](auto &&stream_data) { stream_data.stream_source->Lock(); }, it->second);
|
||||
std::visit([&](const auto &stream_data) { stream_data.stream_source->Lock(); }, it->second);
|
||||
|
||||
locked_streams->erase(it);
|
||||
if (!storage_.Delete(stream_name)) {
|
||||
@@ -632,7 +632,7 @@ void Streams::Start(const std::string &stream_name) {
|
||||
auto it = GetStream(*locked_streams, stream_name);
|
||||
|
||||
std::visit(
|
||||
[&, this](auto &&stream_data) {
|
||||
[&, this](const auto &stream_data) {
|
||||
auto stream_source_ptr = stream_data.stream_source->Lock();
|
||||
stream_source_ptr->Start();
|
||||
Persist(CreateStatus(stream_name, stream_data.transformation_name, stream_data.owner, *stream_source_ptr));
|
||||
@@ -640,12 +640,27 @@ void Streams::Start(const std::string &stream_name) {
|
||||
it->second);
|
||||
}
|
||||
|
||||
void Streams::StartWithLimit(const std::string &stream_name, uint64_t batch_limit,
|
||||
std::optional<std::chrono::milliseconds> timeout) const {
|
||||
std::optional locked_streams{streams_.ReadLock()};
|
||||
auto it = GetStream(**locked_streams, stream_name);
|
||||
|
||||
std::visit(
|
||||
[&](const auto &stream_data) {
|
||||
const auto locked_stream_source = stream_data.stream_source->ReadLock();
|
||||
locked_streams.reset();
|
||||
|
||||
locked_stream_source->StartWithLimit(batch_limit, timeout);
|
||||
},
|
||||
it->second);
|
||||
}
|
||||
|
||||
void Streams::Stop(const std::string &stream_name) {
|
||||
auto locked_streams = streams_.Lock();
|
||||
auto it = GetStream(*locked_streams, stream_name);
|
||||
|
||||
std::visit(
|
||||
[&, this](auto &&stream_data) {
|
||||
[&, this](const auto &stream_data) {
|
||||
auto stream_source_ptr = stream_data.stream_source->Lock();
|
||||
stream_source_ptr->Stop();
|
||||
|
||||
@@ -657,7 +672,7 @@ void Streams::Stop(const std::string &stream_name) {
|
||||
void Streams::StartAll() {
|
||||
for (auto locked_streams = streams_.Lock(); auto &[stream_name, stream_data] : *locked_streams) {
|
||||
std::visit(
|
||||
[&stream_name = stream_name, this](auto &&stream_data) {
|
||||
[&stream_name = stream_name, this](const auto &stream_data) {
|
||||
auto locked_stream_source = stream_data.stream_source->Lock();
|
||||
if (!locked_stream_source->IsRunning()) {
|
||||
locked_stream_source->Start();
|
||||
@@ -672,7 +687,7 @@ void Streams::StartAll() {
|
||||
void Streams::StopAll() {
|
||||
for (auto locked_streams = streams_.Lock(); auto &[stream_name, stream_data] : *locked_streams) {
|
||||
std::visit(
|
||||
[&stream_name = stream_name, this](auto &&stream_data) {
|
||||
[&stream_name = stream_name, this](const auto &stream_data) {
|
||||
auto locked_stream_source = stream_data.stream_source->Lock();
|
||||
if (locked_stream_source->IsRunning()) {
|
||||
locked_stream_source->Stop();
|
||||
@@ -689,7 +704,7 @@ std::vector<StreamStatus<>> Streams::GetStreamInfo() const {
|
||||
{
|
||||
for (auto locked_streams = streams_.ReadLock(); const auto &[stream_name, stream_data] : *locked_streams) {
|
||||
std::visit(
|
||||
[&, &stream_name = stream_name](auto &&stream_data) {
|
||||
[&, &stream_name = stream_name](const auto &stream_data) {
|
||||
auto locked_stream_source = stream_data.stream_source->ReadLock();
|
||||
auto info = locked_stream_source->Info(stream_data.transformation_name);
|
||||
result.emplace_back(StreamStatus<>{stream_name, StreamType(*locked_stream_source),
|
||||
@@ -703,12 +718,12 @@ std::vector<StreamStatus<>> Streams::GetStreamInfo() const {
|
||||
}
|
||||
|
||||
TransformationResult Streams::Check(const std::string &stream_name, std::optional<std::chrono::milliseconds> timeout,
|
||||
std::optional<int64_t> batch_limit) const {
|
||||
std::optional<uint64_t> batch_limit) const {
|
||||
std::optional locked_streams{streams_.ReadLock()};
|
||||
auto it = GetStream(**locked_streams, stream_name);
|
||||
|
||||
return std::visit(
|
||||
[&](auto &&stream_data) {
|
||||
[&](const auto &stream_data) {
|
||||
// This depends on the fact that Drop will first acquire a write lock to the consumer, and erase it only after
|
||||
// that
|
||||
const auto locked_stream_source = stream_data.stream_source->ReadLock();
|
||||
|
||||
@@ -115,6 +115,17 @@ class Streams final {
|
||||
/// @throws ConsumerRunningException if the consumer is already running
|
||||
void Start(const std::string &stream_name);
|
||||
|
||||
/// Start consuming from a stream.
|
||||
///
|
||||
/// @param stream_name name of the stream that needs to be started
|
||||
/// @param batch_limit number of batches we want to consume before stopping
|
||||
/// @param timeout the maximum duration during which the command should run.
|
||||
///
|
||||
/// @throws StreamsException if the stream doesn't exist
|
||||
/// @throws ConsumerRunningException if the consumer is already running
|
||||
void StartWithLimit(const std::string &stream_name, uint64_t batch_limit,
|
||||
std::optional<std::chrono::milliseconds> timeout) const;
|
||||
|
||||
/// Stop consuming from a stream.
|
||||
///
|
||||
/// @param stream_name name of the stream that needs to be stopped
|
||||
@@ -142,6 +153,7 @@ class Streams final {
|
||||
///
|
||||
/// @param stream_name name of the stream we want to test
|
||||
/// @param batch_limit number of batches we want to test before stopping
|
||||
/// @param timeout the maximum duration during which the command should run.
|
||||
///
|
||||
/// @returns A vector of vectors of TypedValue. Each subvector contains two elements, the query string and the
|
||||
/// nullable parameters map.
|
||||
@@ -151,7 +163,7 @@ class Streams final {
|
||||
/// @throws ConsumerCheckFailedException if the transformation function throws any std::exception during processing
|
||||
TransformationResult Check(const std::string &stream_name,
|
||||
std::optional<std::chrono::milliseconds> timeout = std::nullopt,
|
||||
std::optional<int64_t> batch_limit = std::nullopt) const;
|
||||
std::optional<uint64_t> batch_limit = std::nullopt) const;
|
||||
|
||||
private:
|
||||
template <Stream TStream>
|
||||
|
||||
@@ -407,7 +407,7 @@ DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(int, Int, int_v)
|
||||
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(bool, Bool, bool_v)
|
||||
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(int64_t, Int, int_v)
|
||||
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(double, Double, double_v)
|
||||
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const std::string_view &, String, string_v)
|
||||
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const std::string_view, String, string_v)
|
||||
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const TypedValue::TVector &, List, list_v)
|
||||
|
||||
TypedValue &TypedValue::operator=(const std::vector<TypedValue> &other) {
|
||||
|
||||
@@ -185,7 +185,7 @@ class TypedValue {
|
||||
new (&string_v) TString(value, memory_);
|
||||
}
|
||||
|
||||
explicit TypedValue(const std::string_view &value, utils::MemoryResource *memory = utils::NewDeleteResource())
|
||||
explicit TypedValue(const std::string_view value, utils::MemoryResource *memory = utils::NewDeleteResource())
|
||||
: memory_(memory), type_(Type::String) {
|
||||
new (&string_v) TString(value, memory_);
|
||||
}
|
||||
@@ -420,7 +420,7 @@ class TypedValue {
|
||||
TypedValue &operator=(bool);
|
||||
TypedValue &operator=(int64_t);
|
||||
TypedValue &operator=(double);
|
||||
TypedValue &operator=(const std::string_view &);
|
||||
TypedValue &operator=(std::string_view);
|
||||
TypedValue &operator=(const TVector &);
|
||||
TypedValue &operator=(const std::vector<TypedValue> &);
|
||||
TypedValue &operator=(const TMap &);
|
||||
|
||||
@@ -166,7 +166,7 @@ inline void Save(const char *obj, Builder *builder) {
|
||||
builder->Save(reinterpret_cast<const uint8_t *>(obj), size);
|
||||
}
|
||||
|
||||
inline void Save(const std::string_view &obj, Builder *builder) {
|
||||
inline void Save(const std::string_view obj, Builder *builder) {
|
||||
uint64_t size = obj.size();
|
||||
Save(size, builder);
|
||||
builder->Save(reinterpret_cast<const uint8_t *>(obj.data()), size);
|
||||
|
||||
@@ -27,7 +27,7 @@ void WriteSize(Encoder *encoder, uint64_t size) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void Encoder::Initialize(const std::filesystem::path &path, const std::string_view &magic, uint64_t version) {
|
||||
void Encoder::Initialize(const std::filesystem::path &path, const std::string_view magic, uint64_t version) {
|
||||
file_.Open(path, utils::OutputFile::Mode::OVERWRITE_EXISTING);
|
||||
Write(reinterpret_cast<const uint8_t *>(magic.data()), magic.size());
|
||||
auto version_encoded = utils::HostToLittleEndian(version);
|
||||
@@ -73,7 +73,7 @@ void Encoder::WriteDouble(double value) {
|
||||
Write(reinterpret_cast<const uint8_t *>(&value_uint), sizeof(value_uint));
|
||||
}
|
||||
|
||||
void Encoder::WriteString(const std::string_view &value) {
|
||||
void Encoder::WriteString(const std::string_view value) {
|
||||
WriteMarker(Marker::TYPE_STRING);
|
||||
WriteSize(this, value.size());
|
||||
Write(reinterpret_cast<const uint8_t *>(value.data()), value.size());
|
||||
|
||||
@@ -34,14 +34,14 @@ class BaseEncoder {
|
||||
virtual void WriteBool(bool value) = 0;
|
||||
virtual void WriteUint(uint64_t value) = 0;
|
||||
virtual void WriteDouble(double value) = 0;
|
||||
virtual void WriteString(const std::string_view &value) = 0;
|
||||
virtual void WriteString(std::string_view value) = 0;
|
||||
virtual void WritePropertyValue(const PropertyValue &value) = 0;
|
||||
};
|
||||
|
||||
/// Encoder that is used to generate a snapshot/WAL.
|
||||
class Encoder final : public BaseEncoder {
|
||||
public:
|
||||
void Initialize(const std::filesystem::path &path, const std::string_view &magic, uint64_t version);
|
||||
void Initialize(const std::filesystem::path &path, std::string_view magic, uint64_t version);
|
||||
|
||||
void OpenExisting(const std::filesystem::path &path);
|
||||
|
||||
@@ -54,7 +54,7 @@ class Encoder final : public BaseEncoder {
|
||||
void WriteBool(bool value) override;
|
||||
void WriteUint(uint64_t value) override;
|
||||
void WriteDouble(double value) override;
|
||||
void WriteString(const std::string_view &value) override;
|
||||
void WriteString(std::string_view value) override;
|
||||
void WritePropertyValue(const PropertyValue &value) override;
|
||||
|
||||
uint64_t GetPosition();
|
||||
|
||||
@@ -29,8 +29,8 @@ class NameIdMapper final {
|
||||
bool operator<(const MapNameToId &other) { return name < other.name; }
|
||||
bool operator==(const MapNameToId &other) { return name == other.name; }
|
||||
|
||||
bool operator<(const std::string_view &other) { return name < other; }
|
||||
bool operator==(const std::string_view &other) { return name == other; }
|
||||
bool operator<(const std::string_view other) const { return name < other; }
|
||||
bool operator==(const std::string_view other) const { return name == other; }
|
||||
};
|
||||
|
||||
struct MapIdToName {
|
||||
@@ -46,7 +46,7 @@ class NameIdMapper final {
|
||||
|
||||
public:
|
||||
/// @throw std::bad_alloc if unable to insert a new mapping
|
||||
uint64_t NameToId(const std::string_view &name) {
|
||||
uint64_t NameToId(const std::string_view name) {
|
||||
auto name_to_id_acc = name_to_id_.access();
|
||||
auto found = name_to_id_acc.find(name);
|
||||
uint64_t id;
|
||||
|
||||
@@ -44,6 +44,7 @@ Storage::ReplicationClient::ReplicationClient(std::string name, Storage *storage
|
||||
TryInitializeClientSync();
|
||||
|
||||
if (config.timeout && replica_state_ != replication::ReplicaState::INVALID) {
|
||||
MG_ASSERT(*config.timeout > 0);
|
||||
timeout_.emplace(*config.timeout);
|
||||
timeout_dispatcher_.emplace();
|
||||
}
|
||||
@@ -537,6 +538,34 @@ std::vector<Storage::ReplicationClient::RecoveryStep> Storage::ReplicationClient
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
Storage::TimestampInfo Storage::ReplicationClient::GetTimestampInfo() {
|
||||
Storage::TimestampInfo info;
|
||||
info.current_timestamp_of_replica = 0;
|
||||
info.current_number_of_timestamp_behind_master = 0;
|
||||
|
||||
try {
|
||||
auto stream{rpc_client_->Stream<replication::TimestampRpc>()};
|
||||
const auto response = stream.AwaitResponse();
|
||||
const auto is_success = response.success;
|
||||
if (!is_success) {
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
HandleRpcFailure();
|
||||
}
|
||||
auto main_time_stamp = storage_->last_commit_timestamp_.load();
|
||||
info.current_timestamp_of_replica = response.current_commit_timestamp;
|
||||
info.current_number_of_timestamp_behind_master = response.current_commit_timestamp - main_time_stamp;
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
{
|
||||
std::unique_lock client_guard(client_lock_);
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
}
|
||||
HandleRpcFailure(); // mutex already unlocked, if the new enqueued task dispatches immediately it probably won't
|
||||
// block
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
////// TimeoutDispatcher //////
|
||||
void Storage::ReplicationClient::TimeoutDispatcher::WaitForTaskToFinish() {
|
||||
// Wait for the previous timeout task to finish
|
||||
|
||||
@@ -124,6 +124,8 @@ class Storage::ReplicationClient {
|
||||
|
||||
const auto &Endpoint() const { return rpc_client_->Endpoint(); }
|
||||
|
||||
Storage::TimestampInfo GetTimestampInfo();
|
||||
|
||||
private:
|
||||
void FinalizeTransactionReplicationInternal();
|
||||
|
||||
|
||||
@@ -80,6 +80,10 @@ Storage::ReplicationServer::ReplicationServer(Storage *storage, io::network::End
|
||||
spdlog::debug("Received CurrentWalRpc");
|
||||
this->CurrentWalHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_->Register<replication::TimestampRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received TimestampRpc");
|
||||
this->TimestampHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_->Start();
|
||||
}
|
||||
|
||||
@@ -284,6 +288,14 @@ void Storage::ReplicationServer::LoadWal(replication::Decoder *decoder) {
|
||||
}
|
||||
}
|
||||
|
||||
void Storage::ReplicationServer::TimestampHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::TimestampReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
replication::TimestampRes res{true, storage_->last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
}
|
||||
|
||||
Storage::ReplicationServer::~ReplicationServer() {
|
||||
if (rpc_server_) {
|
||||
rpc_server_->Shutdown();
|
||||
|
||||
@@ -34,6 +34,7 @@ class Storage::ReplicationServer {
|
||||
void SnapshotHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
void WalFilesHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
void CurrentWalHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
void TimestampHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
|
||||
void LoadWal(replication::Decoder *decoder);
|
||||
uint64_t ReadAndApplyDelta(durability::BaseDecoder *decoder);
|
||||
|
||||
@@ -67,6 +67,12 @@ cpp<#
|
||||
((success :bool)
|
||||
(current-commit-timestamp :uint64_t))))
|
||||
|
||||
(lcp:define-rpc timestamp
|
||||
(:request ())
|
||||
(:response
|
||||
((success :bool)
|
||||
(current-commit-timestamp :uint64_t))))
|
||||
|
||||
(lcp:pop-namespace) ;; replication
|
||||
(lcp:pop-namespace) ;; storage
|
||||
(lcp:pop-namespace) ;; memgraph
|
||||
|
||||
@@ -30,7 +30,7 @@ void Encoder::WriteDouble(double value) {
|
||||
slk::Save(value, builder_);
|
||||
}
|
||||
|
||||
void Encoder::WriteString(const std::string_view &value) {
|
||||
void Encoder::WriteString(const std::string_view value) {
|
||||
WriteMarker(durability::Marker::TYPE_STRING);
|
||||
slk::Save(value, builder_);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class Encoder final : public durability::BaseEncoder {
|
||||
|
||||
void WriteDouble(double value) override;
|
||||
|
||||
void WriteString(const std::string_view &value) override;
|
||||
void WriteString(std::string_view value) override;
|
||||
|
||||
void WritePropertyValue(const PropertyValue &value) override;
|
||||
|
||||
|
||||
@@ -306,6 +306,13 @@ Storage::Storage(Config config)
|
||||
uuid_(utils::GenerateUUID()),
|
||||
epoch_id_(utils::GenerateUUID()),
|
||||
global_locker_(file_retainer_.AddLocker()) {
|
||||
if (config_.durability.snapshot_wal_mode == Config::Durability::SnapshotWalMode::DISABLED &&
|
||||
replication_role_ == ReplicationRole::MAIN) {
|
||||
spdlog::warn(
|
||||
"The instance has the MAIN replication role, but durability logs and snapshots are disabled. Please consider "
|
||||
"enabling durability by using --storage-snapshot-interval-sec and --storage-wal-enabled flags because "
|
||||
"without write-ahead logs this instance is not replicating any data.");
|
||||
}
|
||||
if (config_.durability.snapshot_wal_mode != Config::Durability::SnapshotWalMode::DISABLED ||
|
||||
config_.durability.snapshot_on_exit || config_.durability.recover_on_startup) {
|
||||
// Create the directory initially to crash the database in case of
|
||||
@@ -805,11 +812,11 @@ const std::string &Storage::Accessor::EdgeTypeToName(EdgeTypeId edge_type) const
|
||||
return storage_->EdgeTypeToName(edge_type);
|
||||
}
|
||||
|
||||
LabelId Storage::Accessor::NameToLabel(const std::string_view &name) { return storage_->NameToLabel(name); }
|
||||
LabelId Storage::Accessor::NameToLabel(const std::string_view name) { return storage_->NameToLabel(name); }
|
||||
|
||||
PropertyId Storage::Accessor::NameToProperty(const std::string_view &name) { return storage_->NameToProperty(name); }
|
||||
PropertyId Storage::Accessor::NameToProperty(const std::string_view name) { return storage_->NameToProperty(name); }
|
||||
|
||||
EdgeTypeId Storage::Accessor::NameToEdgeType(const std::string_view &name) { return storage_->NameToEdgeType(name); }
|
||||
EdgeTypeId Storage::Accessor::NameToEdgeType(const std::string_view name) { return storage_->NameToEdgeType(name); }
|
||||
|
||||
void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; }
|
||||
|
||||
@@ -1114,13 +1121,13 @@ const std::string &Storage::EdgeTypeToName(EdgeTypeId edge_type) const {
|
||||
return name_id_mapper_.IdToName(edge_type.AsUint());
|
||||
}
|
||||
|
||||
LabelId Storage::NameToLabel(const std::string_view &name) { return LabelId::FromUint(name_id_mapper_.NameToId(name)); }
|
||||
LabelId Storage::NameToLabel(const std::string_view name) { return LabelId::FromUint(name_id_mapper_.NameToId(name)); }
|
||||
|
||||
PropertyId Storage::NameToProperty(const std::string_view &name) {
|
||||
PropertyId Storage::NameToProperty(const std::string_view name) {
|
||||
return PropertyId::FromUint(name_id_mapper_.NameToId(name));
|
||||
}
|
||||
|
||||
EdgeTypeId Storage::NameToEdgeType(const std::string_view &name) {
|
||||
EdgeTypeId Storage::NameToEdgeType(const std::string_view name) {
|
||||
return EdgeTypeId::FromUint(name_id_mapper_.NameToId(name));
|
||||
}
|
||||
|
||||
@@ -1879,13 +1886,22 @@ utils::BasicResult<Storage::RegisterReplicaError> Storage::RegisterReplica(
|
||||
MG_ASSERT(replication_role_.load() == ReplicationRole::MAIN, "Only main instance can register a replica!");
|
||||
|
||||
const bool name_exists = replication_clients_.WithLock([&](auto &clients) {
|
||||
return std::any_of(clients.begin(), clients.end(), [&](auto &client) { return client->Name() == name; });
|
||||
return std::any_of(clients.begin(), clients.end(), [&name](const auto &client) { return client->Name() == name; });
|
||||
});
|
||||
|
||||
if (name_exists) {
|
||||
return RegisterReplicaError::NAME_EXISTS;
|
||||
}
|
||||
|
||||
const auto end_point_exists = replication_clients_.WithLock([&endpoint](auto &clients) {
|
||||
return std::any_of(clients.begin(), clients.end(),
|
||||
[&endpoint](const auto &client) { return client->Endpoint() == endpoint; });
|
||||
});
|
||||
|
||||
if (end_point_exists) {
|
||||
return RegisterReplicaError::END_POINT_EXISTS;
|
||||
}
|
||||
|
||||
MG_ASSERT(replication_mode == replication::ReplicationMode::SYNC || !config.timeout,
|
||||
"Only SYNC mode can have a timeout set");
|
||||
|
||||
@@ -1898,10 +1914,15 @@ utils::BasicResult<Storage::RegisterReplicaError> Storage::RegisterReplica(
|
||||
// Another thread could have added a client with same name while
|
||||
// we were connecting to this client.
|
||||
if (std::any_of(clients.begin(), clients.end(),
|
||||
[&](auto &other_client) { return client->Name() == other_client->Name(); })) {
|
||||
[&](const auto &other_client) { return client->Name() == other_client->Name(); })) {
|
||||
return RegisterReplicaError::NAME_EXISTS;
|
||||
}
|
||||
|
||||
if (std::any_of(clients.begin(), clients.end(),
|
||||
[&client](const auto &other_client) { return client->Endpoint() == other_client->Endpoint(); })) {
|
||||
return RegisterReplicaError::END_POINT_EXISTS;
|
||||
}
|
||||
|
||||
clients.push_back(std::move(client));
|
||||
return {};
|
||||
});
|
||||
@@ -1933,7 +1954,8 @@ std::vector<Storage::ReplicaInfo> Storage::ReplicasInfo() {
|
||||
replica_info.reserve(clients.size());
|
||||
std::transform(clients.begin(), clients.end(), std::back_inserter(replica_info),
|
||||
[](const auto &client) -> ReplicaInfo {
|
||||
return {client->Name(), client->Mode(), client->Timeout(), client->Endpoint(), client->State()};
|
||||
return {client->Name(), client->Mode(), client->Timeout(),
|
||||
client->Endpoint(), client->State(), client->GetTimestampInfo()};
|
||||
});
|
||||
return replica_info;
|
||||
});
|
||||
|
||||
@@ -283,13 +283,13 @@ class Storage final {
|
||||
const std::string &EdgeTypeToName(EdgeTypeId edge_type) const;
|
||||
|
||||
/// @throw std::bad_alloc if unable to insert a new mapping
|
||||
LabelId NameToLabel(const std::string_view &name);
|
||||
LabelId NameToLabel(std::string_view name);
|
||||
|
||||
/// @throw std::bad_alloc if unable to insert a new mapping
|
||||
PropertyId NameToProperty(const std::string_view &name);
|
||||
PropertyId NameToProperty(std::string_view name);
|
||||
|
||||
/// @throw std::bad_alloc if unable to insert a new mapping
|
||||
EdgeTypeId NameToEdgeType(const std::string_view &name);
|
||||
EdgeTypeId NameToEdgeType(std::string_view name);
|
||||
|
||||
bool LabelIndexExists(LabelId label) const { return storage_->indices_.label_index.IndexExists(label); }
|
||||
|
||||
@@ -343,13 +343,13 @@ class Storage final {
|
||||
const std::string &EdgeTypeToName(EdgeTypeId edge_type) const;
|
||||
|
||||
/// @throw std::bad_alloc if unable to insert a new mapping
|
||||
LabelId NameToLabel(const std::string_view &name);
|
||||
LabelId NameToLabel(std::string_view name);
|
||||
|
||||
/// @throw std::bad_alloc if unable to insert a new mapping
|
||||
PropertyId NameToProperty(const std::string_view &name);
|
||||
PropertyId NameToProperty(std::string_view name);
|
||||
|
||||
/// @throw std::bad_alloc if unable to insert a new mapping
|
||||
EdgeTypeId NameToEdgeType(const std::string_view &name);
|
||||
EdgeTypeId NameToEdgeType(std::string_view name);
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
bool CreateIndex(LabelId label, std::optional<uint64_t> desired_commit_timestamp = {});
|
||||
@@ -411,7 +411,7 @@ class Storage final {
|
||||
|
||||
bool SetMainReplicationRole();
|
||||
|
||||
enum class RegisterReplicaError : uint8_t { NAME_EXISTS, CONNECTION_FAILED };
|
||||
enum class RegisterReplicaError : uint8_t { NAME_EXISTS, END_POINT_EXISTS, CONNECTION_FAILED };
|
||||
|
||||
/// @pre The instance should have a MAIN role
|
||||
/// @pre Timeout can only be set for SYNC replication
|
||||
@@ -425,12 +425,18 @@ class Storage final {
|
||||
|
||||
ReplicationRole GetReplicationRole() const;
|
||||
|
||||
struct TimestampInfo {
|
||||
uint64_t current_timestamp_of_replica;
|
||||
uint64_t current_number_of_timestamp_behind_master;
|
||||
};
|
||||
|
||||
struct ReplicaInfo {
|
||||
std::string name;
|
||||
replication::ReplicationMode mode;
|
||||
std::optional<double> timeout;
|
||||
io::network::Endpoint endpoint;
|
||||
replication::ReplicaState state;
|
||||
TimestampInfo timestamp_info;
|
||||
};
|
||||
|
||||
std::vector<ReplicaInfo> ReplicasInfo();
|
||||
|
||||
@@ -370,7 +370,7 @@ void OutputFile::Write(const uint8_t *data, size_t size) {
|
||||
}
|
||||
|
||||
void OutputFile::Write(const char *data, size_t size) { Write(reinterpret_cast<const uint8_t *>(data), size); }
|
||||
void OutputFile::Write(const std::string_view &data) { Write(data.data(), data.size()); }
|
||||
void OutputFile::Write(const std::string_view data) { Write(data.data(), data.size()); }
|
||||
|
||||
size_t OutputFile::SeekFile(const Position position, const ssize_t offset) {
|
||||
int whence;
|
||||
|
||||
@@ -209,7 +209,7 @@ class OutputFile {
|
||||
/// the program.
|
||||
void Write(const uint8_t *data, size_t size);
|
||||
void Write(const char *data, size_t size);
|
||||
void Write(const std::string_view &data);
|
||||
void Write(std::string_view data);
|
||||
|
||||
/// This method gets the current absolute position in the file. On failure and
|
||||
/// misuse it crashes the program.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
namespace memgraph::utils {
|
||||
|
||||
inline uint64_t Fnv(const std::string_view &s) {
|
||||
inline uint64_t Fnv(const std::string_view s) {
|
||||
// fnv1a is recommended so use it as the default implementation.
|
||||
uint64_t hash = 14695981039346656037UL;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
namespace memgraph::utils {
|
||||
|
||||
/** Remove whitespace characters from the start of a string. */
|
||||
inline std::string_view LTrim(const std::string_view &s) {
|
||||
inline std::string_view LTrim(const std::string_view s) {
|
||||
size_t start = 0;
|
||||
while (start < s.size() && isspace(s[start])) {
|
||||
++start;
|
||||
@@ -38,7 +38,7 @@ inline std::string_view LTrim(const std::string_view &s) {
|
||||
}
|
||||
|
||||
/** Remove characters found in `chars` from the start of a string. */
|
||||
inline std::string_view LTrim(const std::string_view &s, const std::string_view &chars) {
|
||||
inline std::string_view LTrim(const std::string_view s, const std::string_view chars) {
|
||||
size_t start = 0;
|
||||
while (start < s.size() && chars.find(s[start]) != std::string::npos) {
|
||||
++start;
|
||||
@@ -47,7 +47,7 @@ inline std::string_view LTrim(const std::string_view &s, const std::string_view
|
||||
}
|
||||
|
||||
/** Remove whitespace characters from the end of a string. */
|
||||
inline std::string_view RTrim(const std::string_view &s) {
|
||||
inline std::string_view RTrim(const std::string_view s) {
|
||||
size_t count = s.size();
|
||||
while (count > static_cast<size_t>(0) && isspace(s[count - 1])) {
|
||||
--count;
|
||||
@@ -56,7 +56,7 @@ inline std::string_view RTrim(const std::string_view &s) {
|
||||
}
|
||||
|
||||
/** Remove characters found in `chars` from the end of a string. */
|
||||
inline std::string_view RTrim(const std::string_view &s, const std::string_view &chars) {
|
||||
inline std::string_view RTrim(const std::string_view s, const std::string_view chars) {
|
||||
size_t count = s.size();
|
||||
while (count > static_cast<size_t>(0) && chars.find(s[count - 1]) != std::string::npos) {
|
||||
--count;
|
||||
@@ -65,7 +65,7 @@ inline std::string_view RTrim(const std::string_view &s, const std::string_view
|
||||
}
|
||||
|
||||
/** Remove whitespace characters from the start and from the end of a string. */
|
||||
inline std::string_view Trim(const std::string_view &s) {
|
||||
inline std::string_view Trim(const std::string_view s) {
|
||||
size_t start = 0;
|
||||
size_t count = s.size();
|
||||
while (start < s.size() && isspace(s[start])) {
|
||||
@@ -78,7 +78,7 @@ inline std::string_view Trim(const std::string_view &s) {
|
||||
}
|
||||
|
||||
/** Remove characters found in `chars` from the start and the end of `s`. */
|
||||
inline std::string_view Trim(const std::string_view &s, const std::string_view &chars) {
|
||||
inline std::string_view Trim(const std::string_view s, const std::string_view chars) {
|
||||
size_t start = 0;
|
||||
size_t count = s.size();
|
||||
while (start < s.size() && chars.find(s[start]) != std::string::npos) {
|
||||
@@ -97,7 +97,7 @@ inline std::string_view Trim(const std::string_view &s, const std::string_view &
|
||||
*/
|
||||
template <class TAllocator>
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *ToLowerCase(
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const std::string_view &s) {
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const std::string_view s) {
|
||||
out->resize(s.size());
|
||||
std::transform(s.begin(), s.end(), out->begin(), [](char c) { return tolower(c); });
|
||||
return out;
|
||||
@@ -107,7 +107,7 @@ std::basic_string<char, std::char_traits<char>, TAllocator> *ToLowerCase(
|
||||
* Lowercase all characters of a string.
|
||||
* Transformation is locale independent.
|
||||
*/
|
||||
inline std::string ToLowerCase(const std::string_view &s) {
|
||||
inline std::string ToLowerCase(const std::string_view s) {
|
||||
std::string res;
|
||||
ToLowerCase(&res, s);
|
||||
return res;
|
||||
@@ -120,7 +120,7 @@ inline std::string ToLowerCase(const std::string_view &s) {
|
||||
*/
|
||||
template <class TAllocator>
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *ToUpperCase(
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const std::string_view &s) {
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const std::string_view s) {
|
||||
out->resize(s.size());
|
||||
std::transform(s.begin(), s.end(), out->begin(), [](char c) { return toupper(c); });
|
||||
return out;
|
||||
@@ -130,7 +130,7 @@ std::basic_string<char, std::char_traits<char>, TAllocator> *ToUpperCase(
|
||||
* Uppercase all characters of a string and store the result in `out`.
|
||||
* Transformation is locale independent.
|
||||
*/
|
||||
inline std::string ToUpperCase(const std::string_view &s) {
|
||||
inline std::string ToUpperCase(const std::string_view s) {
|
||||
std::string res;
|
||||
ToUpperCase(&res, s);
|
||||
return res;
|
||||
@@ -143,7 +143,7 @@ inline std::string ToUpperCase(const std::string_view &s) {
|
||||
template <class TCollection, class TAllocator>
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *Join(
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const TCollection &strings,
|
||||
const std::string_view &separator) {
|
||||
const std::string_view separator) {
|
||||
out->clear();
|
||||
if (strings.empty()) return out;
|
||||
int64_t total_size = 0;
|
||||
@@ -163,7 +163,7 @@ std::basic_string<char, std::char_traits<char>, TAllocator> *Join(
|
||||
/**
|
||||
* Join the `strings` collection separated by a given separator.
|
||||
*/
|
||||
inline std::string Join(const std::vector<std::string> &strings, const std::string_view &separator) {
|
||||
inline std::string Join(const std::vector<std::string> &strings, const std::string_view separator) {
|
||||
std::string res;
|
||||
Join(&res, strings, separator);
|
||||
return res;
|
||||
@@ -175,8 +175,8 @@ inline std::string Join(const std::vector<std::string> &strings, const std::stri
|
||||
*/
|
||||
template <class TAllocator>
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *Replace(
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const std::string_view &src,
|
||||
const std::string_view &match, const std::string_view &replacement) {
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const std::string_view src,
|
||||
const std::string_view match, const std::string_view replacement) {
|
||||
// TODO: This could be implemented much more efficiently.
|
||||
*out = src;
|
||||
for (size_t pos = out->find(match); pos != std::string::npos; pos = out->find(match, pos + replacement.size())) {
|
||||
@@ -186,8 +186,8 @@ std::basic_string<char, std::char_traits<char>, TAllocator> *Replace(
|
||||
}
|
||||
|
||||
/** Replace all occurrences of `match` in `src` with `replacement`. */
|
||||
inline std::string Replace(const std::string_view &src, const std::string_view &match,
|
||||
const std::string_view &replacement) {
|
||||
inline std::string Replace(const std::string_view src, const std::string_view match,
|
||||
const std::string_view replacement) {
|
||||
std::string res;
|
||||
Replace(&res, src, match, replacement);
|
||||
return res;
|
||||
@@ -200,8 +200,8 @@ inline std::string Replace(const std::string_view &src, const std::string_view &
|
||||
* @return pointer to `out`.
|
||||
*/
|
||||
template <class TString, class TAllocator>
|
||||
std::vector<TString, TAllocator> *Split(std::vector<TString, TAllocator> *out, const std::string_view &src,
|
||||
const std::string_view &delimiter, int splits = -1) {
|
||||
std::vector<TString, TAllocator> *Split(std::vector<TString, TAllocator> *out, const std::string_view src,
|
||||
const std::string_view delimiter, int splits = -1) {
|
||||
out->clear();
|
||||
if (src.empty()) return out;
|
||||
size_t index = 0;
|
||||
@@ -220,7 +220,7 @@ std::vector<TString, TAllocator> *Split(std::vector<TString, TAllocator> *out, c
|
||||
* The vector will have at most `splits` + 1 elements. Negative value of
|
||||
* `splits` indicates to perform all possible splits.
|
||||
*/
|
||||
inline std::vector<std::string> Split(const std::string_view &src, const std::string_view &delimiter, int splits = -1) {
|
||||
inline std::vector<std::string> Split(const std::string_view src, const std::string_view delimiter, int splits = -1) {
|
||||
std::vector<std::string> res;
|
||||
Split(&res, src, delimiter, splits);
|
||||
return res;
|
||||
@@ -234,7 +234,7 @@ inline std::vector<std::string> Split(const std::string_view &src, const std::st
|
||||
* @return pointer to `out`.
|
||||
*/
|
||||
template <class TString, class TAllocator>
|
||||
std::vector<TString, TAllocator> *Split(std::vector<TString, TAllocator> *out, const std::string_view &src) {
|
||||
std::vector<TString, TAllocator> *Split(std::vector<TString, TAllocator> *out, const std::string_view src) {
|
||||
out->clear();
|
||||
if (src.empty()) return out;
|
||||
// TODO: Investigate how much regex allocate and perhaps replace with custom
|
||||
@@ -256,7 +256,7 @@ std::vector<TString, TAllocator> *Split(std::vector<TString, TAllocator> *out, c
|
||||
* Additionally, the result will not contain empty strings at the start or end
|
||||
* as if the string was trimmed before splitting.
|
||||
*/
|
||||
inline std::vector<std::string> Split(const std::string_view &src) {
|
||||
inline std::vector<std::string> Split(const std::string_view src) {
|
||||
std::vector<std::string> res;
|
||||
Split(&res, src);
|
||||
return res;
|
||||
@@ -271,8 +271,8 @@ inline std::vector<std::string> Split(const std::string_view &src) {
|
||||
* @return pointer to `out`.
|
||||
*/
|
||||
template <class TString, class TAllocator>
|
||||
std::vector<TString, TAllocator> *RSplit(std::vector<TString, TAllocator> *out, const std::string_view &src,
|
||||
const std::string_view &delimiter, int splits = -1) {
|
||||
std::vector<TString, TAllocator> *RSplit(std::vector<TString, TAllocator> *out, const std::string_view src,
|
||||
const std::string_view delimiter, int splits = -1) {
|
||||
out->clear();
|
||||
if (src.empty()) return out;
|
||||
size_t index = src.size();
|
||||
@@ -295,8 +295,7 @@ std::vector<TString, TAllocator> *RSplit(std::vector<TString, TAllocator> *out,
|
||||
* have at most `splits` + 1 elements. Negative value of `splits` indicates to
|
||||
* perform all possible splits.
|
||||
*/
|
||||
inline std::vector<std::string> RSplit(const std::string_view &src, const std::string_view &delimiter,
|
||||
int splits = -1) {
|
||||
inline std::vector<std::string> RSplit(const std::string_view src, const std::string_view delimiter, int splits = -1) {
|
||||
std::vector<std::string> res;
|
||||
RSplit(&res, src, delimiter, splits);
|
||||
return res;
|
||||
@@ -309,7 +308,7 @@ inline std::vector<std::string> RSplit(const std::string_view &src, const std::s
|
||||
*
|
||||
* @throw BasicException if unable to parse the whole string.
|
||||
*/
|
||||
inline int64_t ParseInt(const std::string_view &s) {
|
||||
inline int64_t ParseInt(const std::string_view s) {
|
||||
// stol would be nicer but it uses current locale so we shouldn't use it.
|
||||
int64_t t = 0;
|
||||
// NOTE: Constructing std::istringstream will make a copy of the string, which
|
||||
@@ -336,7 +335,7 @@ inline int64_t ParseInt(const std::string_view &s) {
|
||||
*
|
||||
* @throw BasicException if unable to parse the whole string.
|
||||
*/
|
||||
inline double ParseDouble(const std::string_view &s) {
|
||||
inline double ParseDouble(const std::string_view s) {
|
||||
// stod would be nicer but it uses current locale so we shouldn't use it.
|
||||
double t = 0.0;
|
||||
// NOTE: Constructing std::istringstream will make a copy of the string, which
|
||||
@@ -357,17 +356,17 @@ inline double ParseDouble(const std::string_view &s) {
|
||||
}
|
||||
|
||||
/** Check if the given string `s` ends with the given `suffix`. */
|
||||
inline bool EndsWith(const std::string_view &s, const std::string_view &suffix) {
|
||||
inline bool EndsWith(const std::string_view s, const std::string_view suffix) {
|
||||
return s.size() >= suffix.size() && s.compare(s.size() - suffix.size(), std::string::npos, suffix) == 0;
|
||||
}
|
||||
|
||||
/** Check if the given string `s` starts with the given `prefix`. */
|
||||
inline bool StartsWith(const std::string_view &s, const std::string_view &prefix) {
|
||||
inline bool StartsWith(const std::string_view s, const std::string_view prefix) {
|
||||
return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0;
|
||||
}
|
||||
|
||||
/** Perform case-insensitive string equality test. */
|
||||
inline bool IEquals(const std::string_view &lhs, const std::string_view &rhs) {
|
||||
inline bool IEquals(const std::string_view lhs, const std::string_view rhs) {
|
||||
if (lhs.size() != rhs.size()) return false;
|
||||
for (size_t i = 0; i < lhs.size(); ++i) {
|
||||
if (tolower(lhs[i]) != tolower(rhs[i])) return false;
|
||||
@@ -406,7 +405,7 @@ inline std::string RandomString(size_t length) {
|
||||
*/
|
||||
template <class TAllocator>
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *Escape(
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const std::string_view &src) {
|
||||
std::basic_string<char, std::char_traits<char>, TAllocator> *out, const std::string_view src) {
|
||||
out->clear();
|
||||
out->reserve(src.size() + 2);
|
||||
out->append(1, '"');
|
||||
@@ -433,7 +432,7 @@ std::basic_string<char, std::char_traits<char>, TAllocator> *Escape(
|
||||
}
|
||||
|
||||
/** Escape all whitespace and quotation characters in the given string. */
|
||||
inline std::string Escape(const std::string_view &src) {
|
||||
inline std::string Escape(const std::string_view src) {
|
||||
std::string res;
|
||||
Escape(&res, src);
|
||||
return res;
|
||||
@@ -445,7 +444,7 @@ inline std::string Escape(const std::string_view &src) {
|
||||
* clamped to a valid interval. Therefore, this function never throws
|
||||
* std::out_of_range, unlike std::basic_string::substr.
|
||||
*/
|
||||
inline std::string_view Substr(const std::string_view &string, size_t pos = 0, size_t count = std::string::npos) {
|
||||
inline std::string_view Substr(const std::string_view string, size_t pos = 0, size_t count = std::string::npos) {
|
||||
if (pos >= string.size()) return std::string_view(string.data(), 0);
|
||||
auto len = std::min(string.size() - pos, count);
|
||||
return string.substr(pos, len);
|
||||
|
||||
@@ -15,33 +15,31 @@
|
||||
import sys
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
driver = GraphDatabase.driver('bolt://localhost:7687',
|
||||
auth=basic_auth('', ''),
|
||||
encrypted=False)
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False)
|
||||
session = driver.session()
|
||||
|
||||
session.run('MATCH (n) DETACH DELETE n').consume()
|
||||
print('Database cleared.')
|
||||
session.run("MATCH (n) DETACH DELETE n").consume()
|
||||
print("Database cleared.")
|
||||
|
||||
session.run('CREATE (alice:Person {name: "Alice", age: 22})').consume()
|
||||
print('Record created.')
|
||||
print("Record created.")
|
||||
|
||||
node = session.run('MATCH (n) RETURN n').single()['n']
|
||||
print('Record matched.')
|
||||
node = session.run("MATCH (n) RETURN n").single()["n"]
|
||||
print("Record matched.")
|
||||
|
||||
label = list(node.labels)[0]
|
||||
name = node['name']
|
||||
age = node['age']
|
||||
name = node["name"]
|
||||
age = node["age"]
|
||||
|
||||
if label != 'Person' or name != 'Alice' or age != 22:
|
||||
print('Data does not match')
|
||||
if label != "Person" or name != "Alice" or age != 22:
|
||||
print("Data does not match")
|
||||
sys.exit(1)
|
||||
|
||||
print('Label: %s' % label)
|
||||
print('name: %s' % name)
|
||||
print('age: %s' % age)
|
||||
print("Label: %s" % label)
|
||||
print("name: %s" % name)
|
||||
print("age: %s" % age)
|
||||
|
||||
session.close()
|
||||
driver.close()
|
||||
|
||||
print('All ok!')
|
||||
print("All ok!")
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687",
|
||||
auth=basic_auth("", ""),
|
||||
encrypted=False)
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False)
|
||||
|
||||
query_template = 'CREATE (n {name:"%s"})'
|
||||
template_size = len(query_template) - 2 # because of %s
|
||||
@@ -26,10 +24,11 @@ max_len = 1000000
|
||||
# binary search because we have to find the maximum size (in number of chars)
|
||||
# of a query that can be executed via driver
|
||||
while True:
|
||||
assert min_len > 0 and max_len > 0, \
|
||||
"The lengths have to be positive values! If this happens something" \
|
||||
" is terrible wrong with min & max lengths OR the database" \
|
||||
assert min_len > 0 and max_len > 0, (
|
||||
"The lengths have to be positive values! If this happens something"
|
||||
" is terrible wrong with min & max lengths OR the database"
|
||||
" isn't available."
|
||||
)
|
||||
property_size = (max_len + min_len) // 2
|
||||
try:
|
||||
driver.session().run(query_template % ("a" * property_size)).consume()
|
||||
@@ -42,8 +41,7 @@ while True:
|
||||
|
||||
assert property_size == max_len, "max_len probably has to be increased!"
|
||||
|
||||
print("\nThe max length of a query from Python driver is: %s\n" %
|
||||
(template_size + property_size))
|
||||
print("\nThe max length of a query from Python driver is: %s\n" % (template_size + property_size))
|
||||
|
||||
# sessions are not closed bacause all sessions that are
|
||||
# executed with wrong query size might be broken
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
from neo4j.exceptions import ClientError, TransientError
|
||||
|
||||
|
||||
def tx_error(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
|
||||
print(a[0])
|
||||
@@ -22,17 +23,19 @@ def tx_error(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
|
||||
print(a[0])
|
||||
|
||||
|
||||
def tx_good(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
|
||||
print(a[0])
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
|
||||
print(a[0])
|
||||
|
||||
|
||||
def tx_too_long(tx):
|
||||
tx.run("MATCH (a), (b), (c), (d), (e), (f) RETURN COUNT(*) AS cnt")
|
||||
|
||||
with GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""),
|
||||
encrypted=False) as driver:
|
||||
|
||||
with GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False) as driver:
|
||||
|
||||
def add_person(f, name, name2):
|
||||
with driver.session() as session:
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
# Set up C++ functions for e2e tests
|
||||
function(add_query_module target_name src)
|
||||
add_library(${target_name} SHARED ${src})
|
||||
SET_TARGET_PROPERTIES(${target_name} PROPERTIES PREFIX "")
|
||||
target_include_directories(${target_name} PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
add_library(${target_name} SHARED ${src})
|
||||
SET_TARGET_PROPERTIES(${target_name} PROPERTIES PREFIX "")
|
||||
target_include_directories(${target_name} PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
endfunction()
|
||||
|
||||
|
||||
function(copy_e2e_python_files TARGET_PREFIX FILE_NAME)
|
||||
add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME}
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME})
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME})
|
||||
endfunction()
|
||||
|
||||
function(copy_e2e_python_files_from_parent_folder TARGET_PREFIX EXTRA_PATH FILE_NAME)
|
||||
add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/${EXTRA_PATH}/${FILE_NAME}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME}
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${EXTRA_PATH}/${FILE_NAME})
|
||||
endfunction()
|
||||
|
||||
function(copy_e2e_cpp_files TARGET_PREFIX FILE_NAME)
|
||||
add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME}
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME})
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME})
|
||||
endfunction()
|
||||
|
||||
add_subdirectory(server)
|
||||
|
||||
240
tests/e2e/interactive_mg_runner.py
Normal file
240
tests/e2e/interactive_mg_runner.py
Normal file
@@ -0,0 +1,240 @@
|
||||
# Copyright 2022 Memgraph Ltd.
|
||||
#
|
||||
# Use of this software is governed by the Business Source License
|
||||
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
# License, and you may not use this file except in compliance with the Business Source License.
|
||||
#
|
||||
# As of the Change Date specified in that file, in accordance with
|
||||
# the Business Source License, use of this software will be governed
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# licenses/APL.txt.
|
||||
|
||||
# TODO(gitbuda): Add action to print the context/cluster.
|
||||
# TODO(gitbuda): Add action to print logs of each Memgraph instance.
|
||||
# TODO(gitbuda): Polish naming within script.
|
||||
# TODO(gitbuda): Consider moving this somewhere higher in the project or even put inside GQLAlchmey.
|
||||
|
||||
# The idea here is to implement simple interactive runner of Memgraph instances because:
|
||||
# * it should be possible to manually create new test cases first
|
||||
# by just running this script and executing command manually from e.g. mgconsole,
|
||||
# running single instance of Memgraph is easy but running multiple instances and
|
||||
# controlling them is not that easy
|
||||
# * it should be easy to create new operational test without huge knowledge overhead
|
||||
# by e.g. calling `process_actions` from any e2e Python test, the test will contain the
|
||||
# string with all actions and should run test code in a different thread.
|
||||
#
|
||||
# NOTE: The intention here is not to provide infrastructure to write data
|
||||
# correctness tests or any heavy workload, the intention is to being able to
|
||||
# easily test e2e "operational" cases, simple cluster setup and basic Memgraph
|
||||
# operational queries. For any type of data correctness tests Jepsen or similar
|
||||
# approaches have to be employed.
|
||||
# NOTE: The instance description / context should be compatible with tests/e2e/runner.py
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
import time
|
||||
import sys
|
||||
from inspect import signature
|
||||
|
||||
import yaml
|
||||
from memgraph import MemgraphInstanceRunner
|
||||
from memgraph import extract_bolt_port
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", ".."))
|
||||
BUILD_DIR = os.path.join(PROJECT_DIR, "build")
|
||||
MEMGRAPH_BINARY = os.path.join(BUILD_DIR, "memgraph")
|
||||
|
||||
# Cluster description, injectable as the context.
|
||||
# If the script argument is not provided, the following will be used as a default.
|
||||
MEMGRAPH_INSTANCES_DESCRIPTION = {
|
||||
"replica1": {
|
||||
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
|
||||
"log_file": "replica1.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"],
|
||||
},
|
||||
"replica2": {
|
||||
"args": ["--bolt-port", "7689", "--log-level=TRACE"],
|
||||
"log_file": "replica2.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
|
||||
},
|
||||
"main": {
|
||||
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
|
||||
"log_file": "main.log",
|
||||
"setup_queries": [
|
||||
"REGISTER REPLICA replica1 SYNC TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
|
||||
],
|
||||
},
|
||||
}
|
||||
MEMGRAPH_INSTANCES = {}
|
||||
ACTIONS = {
|
||||
"info": lambda context: info(context),
|
||||
"stop": lambda context, name: stop(context, name),
|
||||
"start": lambda context, name: start(context, name),
|
||||
"sleep": lambda context, delta: time.sleep(float(delta)),
|
||||
"exit": lambda context: sys.exit(1),
|
||||
"quit": lambda context: sys.exit(1),
|
||||
}
|
||||
|
||||
log = logging.getLogger("memgraph.tests.e2e")
|
||||
|
||||
|
||||
def load_args():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("--actions", required=False, help="What actions to run", default="")
|
||||
parser.add_argument(
|
||||
"--context-yaml",
|
||||
required=False,
|
||||
help="YAML file with the cluster description",
|
||||
default="",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def is_port_in_use(port: int) -> bool:
|
||||
import socket
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
return s.connect_ex(("localhost", port)) == 0
|
||||
|
||||
|
||||
def _start_instance(name, args, log_file, queries, use_ssl, procdir):
|
||||
assert (
|
||||
name not in MEMGRAPH_INSTANCES.keys()
|
||||
), "If this raises, you are trying to start an instance with the same name than one already running."
|
||||
assert not is_port_in_use(
|
||||
extract_bolt_port(args)
|
||||
), "If this raises, you are trying to start an instance on a port already used by one already running instance."
|
||||
mg_instance = MemgraphInstanceRunner(MEMGRAPH_BINARY, use_ssl)
|
||||
MEMGRAPH_INSTANCES[name] = mg_instance
|
||||
log_file_path = os.path.join(BUILD_DIR, "logs", log_file)
|
||||
binary_args = args + ["--log-file", log_file_path]
|
||||
|
||||
if len(procdir) != 0:
|
||||
binary_args.append("--query-modules-directory=" + procdir)
|
||||
|
||||
mg_instance.start(args=binary_args)
|
||||
for query in queries:
|
||||
mg_instance.query(query)
|
||||
|
||||
assert mg_instance.is_running(), "An error occured after starting Memgraph instance: application stopped running."
|
||||
|
||||
|
||||
def stop_all():
|
||||
for mg_instance in MEMGRAPH_INSTANCES.values():
|
||||
mg_instance.stop()
|
||||
MEMGRAPH_INSTANCES.clear()
|
||||
|
||||
|
||||
def stop_instance(context, name):
|
||||
for key, _ in context.items():
|
||||
if key != name:
|
||||
continue
|
||||
MEMGRAPH_INSTANCES[name].stop()
|
||||
MEMGRAPH_INSTANCES.pop(name)
|
||||
|
||||
|
||||
def stop(context, name):
|
||||
if name != "all":
|
||||
stop_instance(context, name)
|
||||
return
|
||||
|
||||
stop_all()
|
||||
|
||||
|
||||
def kill(context, name):
|
||||
for key in context.keys():
|
||||
if key != name:
|
||||
continue
|
||||
MEMGRAPH_INSTANCES[name].kill()
|
||||
MEMGRAPH_INSTANCES.pop(name)
|
||||
|
||||
|
||||
@atexit.register
|
||||
def cleanup():
|
||||
stop_all()
|
||||
|
||||
|
||||
def start_instance(context, name, procdir):
|
||||
mg_instances = {}
|
||||
|
||||
for key, value in context.items():
|
||||
if key != name:
|
||||
continue
|
||||
args = value["args"]
|
||||
log_file = value["log_file"]
|
||||
queries = []
|
||||
if "setup_queries" in value:
|
||||
queries = value["setup_queries"]
|
||||
use_ssl = False
|
||||
if "ssl" in value:
|
||||
use_ssl = bool(value["ssl"])
|
||||
value.pop("ssl")
|
||||
|
||||
instance = _start_instance(name, args, log_file, queries, use_ssl, procdir)
|
||||
mg_instances[name] = instance
|
||||
|
||||
assert len(mg_instances) == 1
|
||||
|
||||
|
||||
def start_all(context, procdir=""):
|
||||
stop_all()
|
||||
for key, _ in context.items():
|
||||
start_instance(context, key, procdir)
|
||||
|
||||
|
||||
def start(context, name, procdir=""):
|
||||
if name != "all":
|
||||
start_instance(context, name, procdir)
|
||||
return
|
||||
|
||||
start_all(context)
|
||||
|
||||
|
||||
def info(context):
|
||||
print("{:<15s}{:>6s}".format("NAME", "STATUS"))
|
||||
for name, _ in context.items():
|
||||
if name not in MEMGRAPH_INSTANCES:
|
||||
continue
|
||||
instance = MEMGRAPH_INSTANCES[name]
|
||||
print("{:<15s}{:>6s}".format(name, "UP" if instance.is_running() else "DOWN"))
|
||||
|
||||
|
||||
def process_actions(context, actions):
|
||||
actions = actions.split(" ")
|
||||
actions.reverse()
|
||||
while len(actions) > 0:
|
||||
name = actions.pop()
|
||||
action = ACTIONS[name]
|
||||
args_no = len(signature(action).parameters) - 1
|
||||
assert (
|
||||
args_no >= 0
|
||||
), "Wrong action definition, each action has to accept at least 1 argument which is the context."
|
||||
assert args_no <= 1, "Actions with more than one user argument are not yet supported"
|
||||
if args_no == 0:
|
||||
action(context)
|
||||
if args_no == 1:
|
||||
action(context, actions.pop())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = load_args()
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(asctime)s %(name)s] %(message)s")
|
||||
|
||||
if args.context_yaml == "":
|
||||
context = MEMGRAPH_INSTANCES_DESCRIPTION
|
||||
else:
|
||||
with open(args.context_yaml, "r") as f:
|
||||
context = yaml.load(f, Loader=yaml.FullLoader)
|
||||
if args.actions != "":
|
||||
process_actions(context, args.actions)
|
||||
sys.exit(0)
|
||||
|
||||
while True:
|
||||
choice = input("ACTION>")
|
||||
process_actions(context, choice)
|
||||
@@ -106,6 +106,7 @@ def test_try_to_write(connection, function_type):
|
||||
f"MATCH (n) RETURN {function_type}_write.try_to_write(n, 'property', 1);",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("function_type", ["py", "c"])
|
||||
def test_case_sensitivity(connection, function_type):
|
||||
cursor = connection.cursor()
|
||||
|
||||
@@ -106,3 +106,10 @@ class MemgraphInstanceRunner:
|
||||
self.proc_mg.terminate()
|
||||
code = self.proc_mg.wait()
|
||||
assert code == 0, "The Memgraph process exited with non-zero!"
|
||||
|
||||
def kill(self):
|
||||
if not self.is_running():
|
||||
return
|
||||
self.proc_mg.kill()
|
||||
code = self.proc_mg.wait()
|
||||
assert code == -9, "The killed Memgraph process exited with non-nine!"
|
||||
|
||||
@@ -9,3 +9,6 @@ target_link_libraries(memgraph__e2e__replication__read_write_benchmark gflags js
|
||||
copy_e2e_python_files(replication_show common.py)
|
||||
copy_e2e_python_files(replication_show conftest.py)
|
||||
copy_e2e_python_files(replication_show show.py)
|
||||
copy_e2e_python_files(replication_show show_while_creating_invalid_state.py)
|
||||
copy_e2e_python_files_from_parent_folder(replication_show ".." memgraph.py)
|
||||
copy_e2e_python_files_from_parent_folder(replication_show ".." interactive_mg_runner.py)
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from common import execute_and_fetch_all
|
||||
|
||||
|
||||
@@ -30,17 +32,80 @@ def test_show_replicas(connection):
|
||||
cursor = connection(7687, "main").cursor()
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
|
||||
expected_column_names = {"name", "socket_address", "sync_mode", "timeout"}
|
||||
expected_column_names = {
|
||||
"name",
|
||||
"socket_address",
|
||||
"sync_mode",
|
||||
"timeout",
|
||||
"current_timestamp_of_replica",
|
||||
"number_of_timestamp_behind_master",
|
||||
"state",
|
||||
}
|
||||
actual_column_names = {x.name for x in cursor.description}
|
||||
assert expected_column_names == actual_column_names
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0),
|
||||
("replica_3", "127.0.0.1:10003", "async", None),
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
|
||||
|
||||
def test_show_replicas_while_inserting_data(connection):
|
||||
# Goal is to check the timestamp are correctly computed from the information we get from replicas.
|
||||
# 0/ Check original state of replicas.
|
||||
# 1/ Add some data on main.
|
||||
# 2/ Check state of replicas.
|
||||
# 3/ Execute a read only query.
|
||||
# 4/ Check that the states have not changed.
|
||||
|
||||
# 0/
|
||||
cursor = connection(7687, "main").cursor()
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
|
||||
expected_column_names = {
|
||||
"name",
|
||||
"socket_address",
|
||||
"sync_mode",
|
||||
"timeout",
|
||||
"current_timestamp_of_replica",
|
||||
"number_of_timestamp_behind_master",
|
||||
"state",
|
||||
}
|
||||
actual_column_names = {x.name for x in cursor.description}
|
||||
assert expected_column_names == actual_column_names
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
|
||||
# 1/
|
||||
execute_and_fetch_all(cursor, "CREATE (n1:Number {name: 'forty_two', value:42});")
|
||||
time.sleep(1)
|
||||
|
||||
# 2/
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 4, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0, 4, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 4, 0, "ready"),
|
||||
}
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
print("actual_data=" + str(actual_data))
|
||||
print("expected_data=" + str(expected_data))
|
||||
assert expected_data == actual_data
|
||||
|
||||
# 3/
|
||||
res = execute_and_fetch_all(cursor, "MATCH (node) return node;")
|
||||
assert 1 == len(res)
|
||||
|
||||
# 4/
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
assert expected_data == actual_data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-rA"]))
|
||||
|
||||
164
tests/e2e/replication/show_while_creating_invalid_state.py
Normal file
164
tests/e2e/replication/show_while_creating_invalid_state.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# Copyright 2022 Memgraph Ltd.
|
||||
#
|
||||
# Use of this software is governed by the Business Source License
|
||||
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
# License, and you may not use this file except in compliance with the Business Source License.
|
||||
#
|
||||
# As of the Change Date specified in that file, in accordance with
|
||||
# the Business Source License, use of this software will be governed
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# licenses/APL.txt.
|
||||
|
||||
import sys
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from common import execute_and_fetch_all
|
||||
import interactive_mg_runner
|
||||
import mgclient
|
||||
|
||||
interactive_mg_runner.SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
interactive_mg_runner.PROJECT_DIR = os.path.normpath(
|
||||
os.path.join(interactive_mg_runner.SCRIPT_DIR, "..", "..", "..", "..")
|
||||
)
|
||||
interactive_mg_runner.BUILD_DIR = os.path.normpath(os.path.join(interactive_mg_runner.PROJECT_DIR, "build"))
|
||||
interactive_mg_runner.MEMGRAPH_BINARY = os.path.normpath(os.path.join(interactive_mg_runner.BUILD_DIR, "memgraph"))
|
||||
|
||||
MEMGRAPH_INSTANCES_DESCRIPTION = {
|
||||
"replica_1": {
|
||||
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
|
||||
"log_file": "replica1.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"],
|
||||
},
|
||||
"replica_2": {
|
||||
"args": ["--bolt-port", "7689", "--log-level=TRACE"],
|
||||
"log_file": "replica2.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
|
||||
},
|
||||
"replica_3": {
|
||||
"args": ["--bolt-port", "7690", "--log-level=TRACE"],
|
||||
"log_file": "replica3.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10003;"],
|
||||
},
|
||||
"replica_4": {
|
||||
"args": ["--bolt-port", "7691", "--log-level=TRACE"],
|
||||
"log_file": "replica4.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10004;"],
|
||||
},
|
||||
"main": {
|
||||
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
|
||||
"log_file": "main.log",
|
||||
"setup_queries": [
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001';",
|
||||
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002';",
|
||||
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003';",
|
||||
"REGISTER REPLICA replica_4 ASYNC TO '127.0.0.1:10004';",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_show_replicas(connection):
|
||||
# Goal of this test is to check the SHOW REPLICAS command.
|
||||
# 0/ We start all replicas manually: we want to be able to kill them ourselves without relying on external tooling to kill processes.
|
||||
# 1/ We check that all replicas have the correct state: they should all be ready.
|
||||
# 2/ We drop one replica. It should not appear anymore in the SHOW REPLICAS command.
|
||||
# 3/ We kill another replica. It should become invalid in the SHOW REPLICAS command.
|
||||
|
||||
# 0/
|
||||
interactive_mg_runner.start_all(MEMGRAPH_INSTANCES_DESCRIPTION)
|
||||
|
||||
cursor = connection(7687, "main").cursor()
|
||||
|
||||
# 1/
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
EXPECTED_COLUMN_NAMES = {
|
||||
"name",
|
||||
"socket_address",
|
||||
"sync_mode",
|
||||
"timeout",
|
||||
"current_timestamp_of_replica",
|
||||
"number_of_timestamp_behind_master",
|
||||
"state",
|
||||
}
|
||||
|
||||
actual_column_names = {x.name for x in cursor.description}
|
||||
assert EXPECTED_COLUMN_NAMES == actual_column_names
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2, 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "ready"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
|
||||
# 2/
|
||||
execute_and_fetch_all(cursor, "DROP REPLICA replica_2")
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "ready"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
|
||||
# 3/
|
||||
interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "replica_1")
|
||||
interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "replica_3")
|
||||
interactive_mg_runner.stop(MEMGRAPH_INSTANCES_DESCRIPTION, "replica_4")
|
||||
|
||||
# We leave some time for the main to realise the replicas are down.
|
||||
time.sleep(2)
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "invalid"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "invalid"),
|
||||
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "invalid"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
|
||||
|
||||
def test_add_replica_invalid_timeout(connection):
|
||||
# Goal of this test is to check the registration of replica with invalid timeout raises an exception
|
||||
CONFIGURATION = {
|
||||
"replica_1": {
|
||||
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
|
||||
"log_file": "replica1.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"],
|
||||
},
|
||||
"main": {
|
||||
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
|
||||
"log_file": "main.log",
|
||||
"setup_queries": [],
|
||||
},
|
||||
}
|
||||
|
||||
interactive_mg_runner.start_all(CONFIGURATION)
|
||||
|
||||
cursor = connection(7687, "main").cursor()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 0 TO '127.0.0.1:10001';",
|
||||
)
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT -5 TO '127.0.0.1:10001';",
|
||||
)
|
||||
|
||||
actual_data = execute_and_fetch_all(cursor, "SHOW REPLICAS;")
|
||||
assert 0 == len(actual_data)
|
||||
|
||||
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10001';")
|
||||
actual_data = execute_and_fetch_all(cursor, "SHOW REPLICAS;")
|
||||
assert 1 == len(actual_data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-rA"]))
|
||||
@@ -29,7 +29,7 @@ template_cluster: &template_cluster
|
||||
args: ["--bolt-port", "7687", "--log-level=TRACE"]
|
||||
log_file: "replication-e2e-main.log"
|
||||
setup_queries: [
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 0 TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
|
||||
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003'"
|
||||
]
|
||||
@@ -69,8 +69,12 @@ workloads:
|
||||
args: ["--bolt-port", "7687", "--log-level=TRACE"]
|
||||
log_file: "replication-e2e-main.log"
|
||||
setup_queries: [
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 0 TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
|
||||
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003'"
|
||||
]
|
||||
validation_queries: []
|
||||
|
||||
- name: "Show while creating invalid state"
|
||||
binary: "tests/e2e/pytest_runner.sh"
|
||||
args: ["replication/show_while_creating_invalid_state.py"]
|
||||
|
||||
@@ -18,12 +18,11 @@ from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from memgraph import MemgraphInstanceRunner
|
||||
import interactive_mg_runner
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", ".."))
|
||||
BUILD_DIR = os.path.join(PROJECT_DIR, "build")
|
||||
MEMGRAPH_BINARY = os.path.join(BUILD_DIR, "memgraph")
|
||||
|
||||
log = logging.getLogger("memgraph.tests.e2e")
|
||||
|
||||
@@ -51,37 +50,26 @@ def run(args):
|
||||
continue
|
||||
log.info("%s STARTED.", workload_name)
|
||||
# Setup.
|
||||
mg_instances = {}
|
||||
|
||||
@atexit.register
|
||||
def cleanup():
|
||||
for mg_instance in mg_instances.values():
|
||||
mg_instance.stop()
|
||||
interactive_mg_runner.stop_all()
|
||||
|
||||
for name, config in workload["cluster"].items():
|
||||
use_ssl = False
|
||||
if "ssl" in config:
|
||||
use_ssl = bool(config["ssl"])
|
||||
config.pop("ssl")
|
||||
mg_instance = MemgraphInstanceRunner(MEMGRAPH_BINARY, use_ssl)
|
||||
mg_instances[name] = mg_instance
|
||||
log_file_path = os.path.join(BUILD_DIR, "logs", config["log_file"])
|
||||
binary_args = config["args"] + ["--log-file", log_file_path]
|
||||
if "cluster" in workload:
|
||||
procdir = ""
|
||||
if "proc" in workload:
|
||||
procdir = "--query-modules-directory=" + os.path.join(BUILD_DIR, workload["proc"])
|
||||
binary_args.append(procdir)
|
||||
mg_instance.start(args=binary_args)
|
||||
for query in config.get("setup_queries", []):
|
||||
mg_instance.query(query)
|
||||
procdir = os.path.join(BUILD_DIR, workload["proc"])
|
||||
interactive_mg_runner.start_all(workload["cluster"], procdir)
|
||||
|
||||
# Test.
|
||||
mg_test_binary = os.path.join(BUILD_DIR, workload["binary"])
|
||||
subprocess.run([mg_test_binary] + workload["args"], check=True, stderr=subprocess.STDOUT)
|
||||
# Validation.
|
||||
for name, config in workload["cluster"].items():
|
||||
for validation in config.get("validation_queries", []):
|
||||
mg_instance = mg_instances[name]
|
||||
data = mg_instance.query(validation["query"])[0][0]
|
||||
assert data == validation["expected"]
|
||||
if "cluster" in workload:
|
||||
for name, config in workload["cluster"].items():
|
||||
for validation in config.get("validation_queries", []):
|
||||
mg_instance = interactive_mg_runner.MEMGRAPH_INSTANCES[name]
|
||||
data = mg_instance.query(validation["query"])[0][0]
|
||||
assert data == validation["expected"]
|
||||
cleanup()
|
||||
log.info("%s PASSED.", workload_name)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# licenses/APL.txt.
|
||||
|
||||
import mgclient
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from multiprocessing import Manager, Process, Value
|
||||
@@ -112,6 +113,16 @@ def start_stream(cursor, stream_name):
|
||||
assert get_is_running(cursor, stream_name)
|
||||
|
||||
|
||||
def start_stream_with_limit(cursor, stream_name, batch_limit, timeout=None):
|
||||
if timeout is not None:
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"START STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout} ",
|
||||
)
|
||||
else:
|
||||
execute_and_fetch_all(cursor, f"START STREAM {stream_name} BATCH_LIMIT {batch_limit}")
|
||||
|
||||
|
||||
def stop_stream(cursor, stream_name):
|
||||
execute_and_fetch_all(cursor, f"STOP STREAM {stream_name}")
|
||||
|
||||
@@ -148,7 +159,12 @@ def pulsar_default_namespace_topic(topic):
|
||||
|
||||
|
||||
def test_start_and_stop_during_check(
|
||||
operation, connection, stream_creator, message_sender, already_stopped_error, batchSize
|
||||
operation,
|
||||
connection,
|
||||
stream_creator,
|
||||
message_sender,
|
||||
already_stopped_error,
|
||||
batchSize,
|
||||
):
|
||||
# This test is quite complex. The goal is to call START/STOP queries
|
||||
# while a CHECK query is waiting for its result. Because the Global
|
||||
@@ -253,10 +269,11 @@ def test_start_checked_stream_after_timeout(connection, stream_creator):
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(cursor, stream_creator("test_stream"))
|
||||
|
||||
TIMEOUT_MS = 2000
|
||||
TIMEOUT_IN_MS = 2000
|
||||
TIMEOUT_IN_SECONDS = TIMEOUT_IN_MS / 1000
|
||||
|
||||
def call_check():
|
||||
execute_and_fetch_all(connect().cursor(), f"CHECK STREAM test_stream TIMEOUT {TIMEOUT_MS}")
|
||||
execute_and_fetch_all(connect().cursor(), f"CHECK STREAM test_stream TIMEOUT {TIMEOUT_IN_MS}")
|
||||
|
||||
check_stream_proc = Process(target=call_check, daemon=True)
|
||||
|
||||
@@ -266,7 +283,7 @@ def test_start_checked_stream_after_timeout(connection, stream_creator):
|
||||
start_stream(cursor, "test_stream")
|
||||
end = time.time()
|
||||
|
||||
assert (end - start) < 1.3 * TIMEOUT_MS, "The START STREAM was blocked too long"
|
||||
assert (end - start) < 1.3 * TIMEOUT_IN_SECONDS, "The START STREAM was blocked too long"
|
||||
assert get_is_running(cursor, "test_stream")
|
||||
stop_stream(cursor, "test_stream")
|
||||
|
||||
@@ -308,24 +325,42 @@ def test_check_stream_same_number_of_queries_than_messages(connection, stream_cr
|
||||
|
||||
expected_queries_and_raw_messages_1 = (
|
||||
[ # queries
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 01"}, QUERY_LITERAL: "Message: 01"},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 02"}, QUERY_LITERAL: "Message: 02"},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 01"},
|
||||
QUERY_LITERAL: "Message: 01",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 02"},
|
||||
QUERY_LITERAL: "Message: 02",
|
||||
},
|
||||
],
|
||||
["01", "02"], # raw message
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_2 = (
|
||||
[ # queries
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 03"}, QUERY_LITERAL: "Message: 03"},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 04"}, QUERY_LITERAL: "Message: 04"},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 03"},
|
||||
QUERY_LITERAL: "Message: 03",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 04"},
|
||||
QUERY_LITERAL: "Message: 04",
|
||||
},
|
||||
],
|
||||
["03", "04"], # raw message
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_3 = (
|
||||
[ # queries
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 05"}, QUERY_LITERAL: "Message: 05"},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 06"}, QUERY_LITERAL: "Message: 06"},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 05"},
|
||||
QUERY_LITERAL: "Message: 05",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 06"},
|
||||
QUERY_LITERAL: "Message: 06",
|
||||
},
|
||||
],
|
||||
["05", "06"], # raw message
|
||||
)
|
||||
@@ -380,20 +415,32 @@ def test_check_stream_different_number_of_queries_than_messages(connection, stre
|
||||
|
||||
expected_queries_and_raw_messages_2 = (
|
||||
[ # queries
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 03"}, QUERY_LITERAL: "Message: 03"},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 04"}, QUERY_LITERAL: "Message: 04"},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 03"},
|
||||
QUERY_LITERAL: "Message: 03",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 04"},
|
||||
QUERY_LITERAL: "Message: 04",
|
||||
},
|
||||
],
|
||||
["03", "04"], # raw message
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_3 = (
|
||||
[ # queries
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: b_05"}, QUERY_LITERAL: "Message: b_05"},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: b_05"},
|
||||
QUERY_LITERAL: "Message: b_05",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: extra_b_05"},
|
||||
QUERY_LITERAL: "Message: extra_b_05",
|
||||
},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 06"}, QUERY_LITERAL: "Message: 06"},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 06"},
|
||||
QUERY_LITERAL: "Message: 06",
|
||||
},
|
||||
],
|
||||
["b_05", "06"], # raw message
|
||||
)
|
||||
@@ -401,3 +448,261 @@ def test_check_stream_different_number_of_queries_than_messages(connection, stre
|
||||
assert expected_queries_and_raw_messages_1 == results.value[0]
|
||||
assert expected_queries_and_raw_messages_2 == results.value[1]
|
||||
assert expected_queries_and_raw_messages_3 == results.value[2]
|
||||
|
||||
|
||||
def test_start_stream_with_batch_limit(connection, stream_creator, messages_sender):
|
||||
STREAM_NAME = "test"
|
||||
BATCH_LIMIT = 5
|
||||
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(cursor, stream_creator(STREAM_NAME))
|
||||
|
||||
def start_new_stream_with_limit(stream_name, batch_limit):
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
start_stream_with_limit(cursor, stream_name, batch_limit)
|
||||
|
||||
thread_stream_running = Process(target=start_new_stream_with_limit, daemon=True, args=(STREAM_NAME, BATCH_LIMIT))
|
||||
thread_stream_running.start()
|
||||
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
messages_sender(BATCH_LIMIT - 1)
|
||||
|
||||
# We have not sent enough batches to reach the limit. We check that the stream is still correctly running.
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
# We send a last message to reach the batch_limit
|
||||
messages_sender(1)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# We check that the stream has correctly stoped.
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
|
||||
def test_start_stream_with_batch_limit_timeout(connection, stream_creator):
|
||||
# We check that we get the expected exception when trying to run START STREAM while providing TIMEOUT and not BATCH_LIMIT
|
||||
STREAM_NAME = "test"
|
||||
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(cursor, stream_creator(STREAM_NAME))
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(cursor, f"START STREAM {STREAM_NAME} TIMEOUT 3000")
|
||||
|
||||
|
||||
def test_start_stream_with_batch_limit_reaching_timeout(connection, stream_creator):
|
||||
# We check that we get the expected exception when running START STREAM while providing TIMEOUT and BATCH_LIMIT
|
||||
STREAM_NAME = "test"
|
||||
BATCH_LIMIT = 5
|
||||
TIMEOUT = 3000
|
||||
TIMEOUT_IN_SECONDS = TIMEOUT / 1000
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(cursor, stream_creator(STREAM_NAME, BATCH_SIZE))
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"START STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}",
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
assert (
|
||||
end_time - start_time
|
||||
) >= TIMEOUT_IN_SECONDS, "The START STREAM has probably thrown due to something else than timeout!"
|
||||
|
||||
|
||||
def test_start_stream_with_batch_limit_while_check_running(
|
||||
connection, stream_creator, message_sender, setup_function=None
|
||||
):
|
||||
# 1/ We check we get the correct exception calling START STREAM with BATCH_LIMIT while a CHECK STREAM is already running.
|
||||
# 2/ Afterwards, we terminate the CHECK STREAM and start a START STREAM with BATCH_LIMIT
|
||||
def start_check_stream(stream_name, batch_limit, timeout):
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}",
|
||||
)
|
||||
|
||||
def start_new_stream_with_limit(stream_name, batch_limit, timeout):
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
start_stream_with_limit(cursor, stream_name, batch_limit, timeout=timeout)
|
||||
|
||||
STREAM_NAME = "test_check_and_batch_limit"
|
||||
BATCH_LIMIT = 1
|
||||
TIMEOUT = 10000
|
||||
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(cursor, stream_creator(STREAM_NAME))
|
||||
|
||||
# 0/ Extra setup needed for Kafka to works correctly if Check stream is execute before any messages have been consumed.
|
||||
if setup_function is not None:
|
||||
setup_function(start_check_stream, cursor, STREAM_NAME, BATCH_LIMIT, TIMEOUT)
|
||||
|
||||
# 1/
|
||||
thread_stream_check = Process(target=start_check_stream, daemon=True, args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT))
|
||||
thread_stream_check.start()
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
start_stream_with_limit(cursor, STREAM_NAME, BATCH_LIMIT, timeout=TIMEOUT)
|
||||
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
message_sender(SIMPLE_MSG)
|
||||
thread_stream_check.join()
|
||||
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
# 2/
|
||||
thread_stream_running = Process(
|
||||
target=start_new_stream_with_limit,
|
||||
daemon=True,
|
||||
args=(STREAM_NAME, BATCH_LIMIT + 1, TIMEOUT),
|
||||
) # Sending BATCH_LIMIT + 1 messages as BATCH_LIMIT messages have already been sent during the CHECK STREAM (and not consumed)
|
||||
thread_stream_running.start()
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
message_sender(SIMPLE_MSG)
|
||||
time.sleep(2)
|
||||
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
|
||||
def test_check_while_stream_with_batch_limit_running(connection, stream_creator, message_sender):
|
||||
# 1/ We check we get the correct exception calling CHECK STREAM while START STREAM with BATCH_LIMIT is already running
|
||||
# 2/ Afterwards, we terminate the START STREAM with BATCH_LIMIT and start a CHECK STREAM
|
||||
def start_new_stream_with_limit(stream_name, batch_limit, timeout):
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
start_stream_with_limit(cursor, stream_name, batch_limit, timeout=timeout)
|
||||
|
||||
def start_check_stream(stream_name, batch_limit, timeout):
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}",
|
||||
)
|
||||
|
||||
STREAM_NAME = "test_batch_limit_and_check"
|
||||
BATCH_LIMIT = 1
|
||||
TIMEOUT = 10000
|
||||
TIMEOUT_IN_SECONDS = TIMEOUT / 1000
|
||||
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(cursor, stream_creator(STREAM_NAME))
|
||||
|
||||
# 1/
|
||||
thread_stream_running = Process(
|
||||
target=start_new_stream_with_limit,
|
||||
daemon=True,
|
||||
args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT),
|
||||
)
|
||||
start_time = time.time()
|
||||
thread_stream_running.start()
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}",
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
|
||||
message_sender(SIMPLE_MSG)
|
||||
time.sleep(2)
|
||||
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
# 2/
|
||||
thread_stream_check = Process(target=start_check_stream, daemon=True, args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT))
|
||||
start_time = time.time()
|
||||
thread_stream_check.start()
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
message_sender(SIMPLE_MSG)
|
||||
time.sleep(2)
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
|
||||
def test_start_stream_with_batch_limit_with_invalid_batch_limit(connection, stream_creator):
|
||||
# We check that we get a correct exception when giving a negative batch_limit
|
||||
STREAM_NAME = "test_batch_limit_invalid_batch_limit"
|
||||
TIMEOUT = 10000
|
||||
TIMEOUT_IN_SECONDS = TIMEOUT / 1000
|
||||
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(cursor, stream_creator(STREAM_NAME))
|
||||
time.sleep(2)
|
||||
|
||||
# 1/ checking with batch_limit=-10
|
||||
batch_limit = -10
|
||||
start_time = time.time()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
start_stream_with_limit(cursor, STREAM_NAME, batch_limit, timeout=TIMEOUT)
|
||||
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The START STREAM has probably thrown due to timeout!"
|
||||
|
||||
# 2/ checking with batch_limit=0
|
||||
batch_limit = 0
|
||||
start_time = time.time()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
start_stream_with_limit(cursor, STREAM_NAME, batch_limit, timeout=TIMEOUT)
|
||||
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The START STREAM has probably thrown due to timeout!"
|
||||
|
||||
|
||||
def test_check_stream_with_batch_limit_with_invalid_batch_limit(connection, stream_creator):
|
||||
# We check that we get a correct exception when giving a negative batch_limit
|
||||
STREAM_NAME = "test_batch_limit_invalid_batch_limit"
|
||||
TIMEOUT = 10000
|
||||
TIMEOUT_IN_SECONDS = TIMEOUT / 1000
|
||||
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(cursor, stream_creator(STREAM_NAME))
|
||||
time.sleep(2)
|
||||
|
||||
# 1/ checking with batch_limit=-10
|
||||
batch_limit = -10
|
||||
start_time = time.time()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}",
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
|
||||
# 2/ checking with batch_limit=0
|
||||
batch_limit = 0
|
||||
start_time = time.time()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}",
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
|
||||
@@ -37,29 +37,22 @@ def connection():
|
||||
|
||||
|
||||
def get_topics(num):
|
||||
return [f'topic_{i}' for i in range(num)]
|
||||
return [f"topic_{i}" for i in range(num)]
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def kafka_topics():
|
||||
admin_client = KafkaAdminClient(
|
||||
bootstrap_servers="localhost:9092",
|
||||
client_id="test")
|
||||
admin_client = KafkaAdminClient(bootstrap_servers="localhost:9092", client_id="test")
|
||||
# The issue arises if we remove default kafka topics, e.g.
|
||||
# "__consumer_offsets"
|
||||
previous_topics = [
|
||||
topic for topic in admin_client.list_topics() if topic != "__consumer_offsets"]
|
||||
previous_topics = [topic for topic in admin_client.list_topics() if topic != "__consumer_offsets"]
|
||||
if previous_topics:
|
||||
admin_client.delete_topics(topics=previous_topics, timeout_ms=5000)
|
||||
|
||||
topics = get_topics(3)
|
||||
topics_to_create = []
|
||||
for topic in topics:
|
||||
topics_to_create.append(
|
||||
NewTopic(
|
||||
name=topic,
|
||||
num_partitions=1,
|
||||
replication_factor=1))
|
||||
topics_to_create.append(NewTopic(name=topic, num_partitions=1, replication_factor=1))
|
||||
|
||||
admin_client.create_topics(new_topics=topics_to_create, timeout_ms=5000)
|
||||
yield topics
|
||||
@@ -80,6 +73,5 @@ def pulsar_client():
|
||||
def pulsar_topics():
|
||||
topics = get_topics(3)
|
||||
for topic in topics:
|
||||
requests.delete(
|
||||
f'http://127.0.0.1:6652/admin/v2/persistent/public/default/{topic}?force=true')
|
||||
requests.delete(f"http://127.0.0.1:6652/admin/v2/persistent/public/default/{topic}?force=true")
|
||||
yield topics
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user