Compare commits
25 Commits
separate_r
...
fix-kafka-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5370fd0f6 | ||
|
|
5f509532f2 | ||
|
|
307f7e1827 | ||
|
|
4b3ba908c7 | ||
|
|
030b554ffd | ||
|
|
4bc5d749b2 | ||
|
|
a426ef9cc3 | ||
|
|
60e167d676 | ||
|
|
3f8befde79 | ||
|
|
9355e58e73 | ||
|
|
476968e2c8 | ||
|
|
97183fb9da | ||
|
|
adb65b2fff | ||
|
|
3bf2cf65ab | ||
|
|
8f3f693f20 | ||
|
|
2e51e703c3 | ||
|
|
adf7533751 | ||
|
|
509183e985 | ||
|
|
1fe2190747 | ||
|
|
762fe6a65d | ||
|
|
1ad87927b7 | ||
|
|
0a39b6d838 | ||
|
|
cec19c4b54 | ||
|
|
0d5e8cd9d8 | ||
|
|
2a404c81a0 |
@@ -19,3 +19,16 @@ repos:
|
||||
rev: v13.0.0
|
||||
hooks:
|
||||
- id: clang-format
|
||||
# - repo: local
|
||||
# hooks:
|
||||
# - id: clang-tidy
|
||||
# name: clang-tidy
|
||||
# description: Runs clang-tidy and checks for errors
|
||||
# entry: python ./tools/pre-commit/clang-tidy.py
|
||||
# language: python
|
||||
# files: ^src/
|
||||
# types: [c++, text]
|
||||
# fail_fast: true
|
||||
# require_serial: true
|
||||
# args: [--compile_commands_path=build]
|
||||
# pass_filenames: false
|
||||
|
||||
@@ -42,6 +42,10 @@ endif()
|
||||
|
||||
project(memgraph LANGUAGES C CXX)
|
||||
|
||||
# NOTE: once in a while this needs to be toggled to check headers are
|
||||
# correct and PCH isn't masking any include issues
|
||||
set(CMAKE_DISABLE_PRECOMPILE_HEADERS OFF)
|
||||
|
||||
#TODO: upgrade to cmake 3.24 + CheckIPOSupported
|
||||
#cmake_policy(SET CMP0138 NEW)
|
||||
#include(CheckIPOSupported)
|
||||
@@ -211,8 +215,8 @@ set(CMAKE_CXX_FLAGS_RELWITHDEBINFO
|
||||
# ** Static linking is allowed only for executables! **
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++")
|
||||
|
||||
# Use gold linker to speedup build
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold")
|
||||
# Use lld linker to speedup build
|
||||
add_link_options(-fuse-ld=lld) # TODO: use mold linker
|
||||
|
||||
# release flags
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG")
|
||||
|
||||
384
include/mgp.hpp
384
include/mgp.hpp
File diff suppressed because it is too large
Load Diff
@@ -261,3 +261,7 @@ import_external_library(librdtsc STATIC
|
||||
|
||||
# setup ctre
|
||||
import_header_library(ctre ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# setup absl (cmake sub_directory tolerant)
|
||||
set(ABSL_PROPAGATE_CXX_STD ON)
|
||||
add_subdirectory(absl EXCLUDE_FROM_ALL)
|
||||
|
||||
@@ -123,6 +123,7 @@ declare -A primary_urls=(
|
||||
["pulsar"]="http://$local_cache_host/git/pulsar.git"
|
||||
["librdtsc"]="http://$local_cache_host/git/librdtsc.git"
|
||||
["ctre"]="http://$local_cache_host/file/hanickadot/compile-time-regular-expressions/v3.7.2/single-header/ctre.hpp"
|
||||
["absl"]="https://$local_cache_host/git/abseil-cpp.git"
|
||||
)
|
||||
|
||||
# The goal of secondary urls is to have links to the "source of truth" of
|
||||
@@ -140,7 +141,7 @@ declare -A secondary_urls=(
|
||||
["rocksdb"]="https://github.com/facebook/rocksdb.git"
|
||||
["mgclient"]="https://github.com/memgraph/mgclient.git"
|
||||
["pymgclient"]="https://github.com/memgraph/pymgclient.git"
|
||||
["mgconsole"]="http://github.com/memgraph/mgconsole.git"
|
||||
["mgconsole"]="https://github.com/memgraph/mgconsole.git"
|
||||
["spdlog"]="https://github.com/gabime/spdlog"
|
||||
["nlohmann"]="https://raw.githubusercontent.com/nlohmann/json/4f8fba14066156b73f1189a2b8bd568bde5284c5/single_include/nlohmann/json.hpp"
|
||||
["neo4j"]="https://dist.neo4j.org/neo4j-community-5.6.0-unix.tar.gz"
|
||||
@@ -149,6 +150,7 @@ declare -A secondary_urls=(
|
||||
["pulsar"]="https://github.com/apache/pulsar.git"
|
||||
["librdtsc"]="https://github.com/gabrieleara/librdtsc.git"
|
||||
["ctre"]="https://raw.githubusercontent.com/hanickadot/compile-time-regular-expressions/v3.7.2/single-header/ctre.hpp"
|
||||
["absl"]="https://github.com/abseil/abseil-cpp.git"
|
||||
)
|
||||
|
||||
# antlr
|
||||
@@ -246,3 +248,7 @@ mkdir -p ctre
|
||||
cd ctre
|
||||
file_get_try_double "${primary_urls[ctre]}" "${secondary_urls[ctre]}"
|
||||
cd ..
|
||||
|
||||
# abseil 20230125.3
|
||||
absl_ref="20230125.3"
|
||||
repo_clone_try_double "${primary_urls[absl]}" "${secondary_urls[absl]}" "absl" "$absl_ref"
|
||||
|
||||
202
licenses/third-party/abseil-cpp/LICENSE
vendored
Normal file
202
licenses/third-party/abseil-cpp/LICENSE
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
218
licenses/third-party/compile-time-regular-expressions/LICENSE
vendored
Normal file
218
licenses/third-party/compile-time-regular-expressions/LICENSE
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--- LLVM Exceptions to the Apache 2.0 License ----
|
||||
|
||||
As an exception, if, as a result of your compiling your source code, portions
|
||||
of this Software are embedded into an Object form of such source code, you
|
||||
may redistribute such embedded portions in such Object form without complying
|
||||
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
|
||||
|
||||
In addition, if you combine or link compiled forms of this Software with
|
||||
software that is licensed under the GPLv2 ("Combined Software") and if a
|
||||
court of competent jurisdiction determines that the patent provision (Section
|
||||
3), the indemnity provision (Section 9) or other Section of the License
|
||||
conflicts with the conditions of the GPLv2, you may retroactively and
|
||||
prospectively choose to deem waived or otherwise exclude such Section(s) of
|
||||
the License, but only in their entirety and only with respect to the Combined
|
||||
Software.
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -18,7 +18,11 @@ void ProcImpl(std::vector<mgp::Value> arguments, mgp::Graph graph, mgp::RecordFa
|
||||
|
||||
void SampleReadProc(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
|
||||
try {
|
||||
mgp::memory = memory;
|
||||
// The outcommented way of assigning the memory pointer is still
|
||||
// working, but it is deprecated because of certain concurrency
|
||||
// issues. Please use the guard instead.
|
||||
// mgp::memory = memory;
|
||||
mgp::MemoryDispatcherGuard guard(memory);
|
||||
|
||||
std::vector<mgp::Value> arguments;
|
||||
for (size_t i = 0; i < mgp::list_size(args); i++) {
|
||||
@@ -34,7 +38,11 @@ void SampleReadProc(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *resul
|
||||
}
|
||||
|
||||
void AddXNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
|
||||
mgp::memory = memory;
|
||||
// The outcommented way of assigning the memory pointer is still
|
||||
// working, but it is deprecated because of certain concurrency
|
||||
// issues. Please use the guard instead.
|
||||
// mgp::memory = memory;
|
||||
mgp::MemoryDispatcherGuard guard(memory);
|
||||
auto graph = mgp::Graph(memgraph_graph);
|
||||
|
||||
std::vector<mgp::Value> arguments;
|
||||
@@ -49,7 +57,11 @@ void AddXNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mg
|
||||
}
|
||||
|
||||
void Multiply(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory) {
|
||||
mgp::memory = memory;
|
||||
// The outcommented way of assigning the memory pointer is still
|
||||
// working, but it is deprecated because of certain concurrency
|
||||
// issues. Please use the guard instead.
|
||||
// mgp::memory = memory;
|
||||
mgp::MemoryDispatcherGuard guard(memory);
|
||||
|
||||
std::vector<mgp::Value> arguments;
|
||||
for (size_t i = 0; i < mgp::list_size(args); i++) {
|
||||
@@ -67,7 +79,11 @@ void Multiply(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_m
|
||||
|
||||
extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) {
|
||||
try {
|
||||
mgp::memory = memory;
|
||||
// The outcommented way of assigning the memory pointer is still
|
||||
// working, but it is deprecated because of certain concurrency
|
||||
// issues. Please use the guard instead.
|
||||
// mgp::memory = memory;
|
||||
mgp::MemoryDispatcherGuard guard(memory);
|
||||
|
||||
AddProcedure(SampleReadProc, "return_true", mgp::ProcedureType::Read,
|
||||
{mgp::Parameter("param_1", mgp::Type::Int), mgp::Parameter("param_2", mgp::Type::Double, 2.3)},
|
||||
@@ -77,7 +93,11 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *mem
|
||||
}
|
||||
|
||||
try {
|
||||
mgp::memory = memory;
|
||||
// The outcommented way of assigning the memory pointer is still
|
||||
// working, but it is deprecated because of certain concurrency
|
||||
// issues. Please use the guard instead.
|
||||
// mgp::memory = memory;
|
||||
mgp::MemoryDispatcherGuard guard(memory);
|
||||
|
||||
mgp::AddProcedure(AddXNodes, "add_x_nodes", mgp::ProcedureType::Write, {mgp::Parameter("param_1", mgp::Type::Int)},
|
||||
{}, module, memory);
|
||||
@@ -87,7 +107,11 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *mem
|
||||
}
|
||||
|
||||
try {
|
||||
mgp::memory = memory;
|
||||
// The outcommented way of assigning the memory pointer is still
|
||||
// working, but it is deprecated because of certain concurrency
|
||||
// issues. Please use the guard instead.
|
||||
// mgp::memory = memory;
|
||||
mgp::MemoryDispatcherGuard guard(memory);
|
||||
|
||||
mgp::AddFunction(Multiply, "multiply",
|
||||
{mgp::Parameter("int", mgp::Type::Int), mgp::Parameter("int", mgp::Type::Int, (int64_t)3)}, module,
|
||||
|
||||
@@ -31,15 +31,19 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
# ----------------------------------------------------------------------------
|
||||
set(mg_single_node_v2_sources
|
||||
memgraph.cpp
|
||||
flags/isolation_level.cpp
|
||||
flags/memory_limit.cpp
|
||||
flags/log_level.cpp
|
||||
flags/general.cpp
|
||||
flags/audit.cpp
|
||||
flags/bolt.cpp
|
||||
)
|
||||
|
||||
set(mg_single_node_v2_libs stdc++fs Threads::Threads
|
||||
mg-telemetry mg-query mg-communication mg-memory mg-utils mg-auth mg-license mg-settings mg-glue mg-audit)
|
||||
|
||||
# memgraph main executable
|
||||
add_executable(memgraph ${mg_single_node_v2_sources})
|
||||
target_include_directories(memgraph PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
target_link_libraries(memgraph ${mg_single_node_v2_libs})
|
||||
target_link_libraries(memgraph stdc++fs Threads::Threads
|
||||
mg-telemetry mg-communication mg-memory mg-utils mg-license mg-settings mg-glue)
|
||||
|
||||
# NOTE: `include/mg_procedure.syms` describes a pattern match for symbols which
|
||||
# should be dynamically exported, so that `dlopen` can correctly link the
|
||||
|
||||
@@ -22,3 +22,4 @@ target_link_libraries(mg-communication Boost::headers Threads::Threads mg-utils
|
||||
find_package(OpenSSL REQUIRED)
|
||||
target_link_libraries(mg-communication ${OPENSSL_LIBRARIES})
|
||||
target_include_directories(mg-communication SYSTEM PUBLIC ${OPENSSL_INCLUDE_DIR})
|
||||
target_precompile_headers(mg-communication INTERFACE http/server.hpp <boost/beast/websocket.hpp> bolt/v1/session.hpp)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -43,6 +43,7 @@ class Buffer final {
|
||||
Buffer(Buffer &&) = delete;
|
||||
Buffer &operator=(const Buffer &) = delete;
|
||||
Buffer &operator=(Buffer &&) = delete;
|
||||
~Buffer() = default;
|
||||
|
||||
/**
|
||||
* This class provides all functions from the buffer that are needed to allow
|
||||
@@ -50,12 +51,13 @@ class Buffer final {
|
||||
*/
|
||||
class ReadEnd {
|
||||
public:
|
||||
ReadEnd(Buffer *buffer);
|
||||
explicit ReadEnd(Buffer *buffer);
|
||||
|
||||
ReadEnd(const ReadEnd &) = delete;
|
||||
ReadEnd(ReadEnd &&) = delete;
|
||||
ReadEnd &operator=(const ReadEnd &) = delete;
|
||||
ReadEnd &operator=(ReadEnd &&) = delete;
|
||||
~ReadEnd() = default;
|
||||
|
||||
uint8_t *data();
|
||||
|
||||
@@ -77,12 +79,13 @@ class Buffer final {
|
||||
*/
|
||||
class WriteEnd {
|
||||
public:
|
||||
WriteEnd(Buffer *buffer);
|
||||
explicit WriteEnd(Buffer *buffer);
|
||||
|
||||
WriteEnd(const WriteEnd &) = delete;
|
||||
WriteEnd(WriteEnd &&) = delete;
|
||||
WriteEnd &operator=(const WriteEnd &) = delete;
|
||||
WriteEnd &operator=(WriteEnd &&) = delete;
|
||||
~WriteEnd() = default;
|
||||
|
||||
io::network::StreamBuffer Allocate();
|
||||
|
||||
|
||||
@@ -26,10 +26,7 @@ class Server final {
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
public:
|
||||
explicit Server(io::network::Endpoint endpoint, TSessionContext *session_context, ServerContext *context)
|
||||
: listener_{Listener<TRequestHandler, TSessionContext>::Create(
|
||||
ioc_, session_context, context,
|
||||
tcp::endpoint{boost::asio::ip::make_address(endpoint.address), endpoint.port})} {}
|
||||
explicit Server(io::network::Endpoint endpoint, TSessionContext *session_context, ServerContext *context);
|
||||
|
||||
Server(const Server &) = delete;
|
||||
Server(Server &&) = delete;
|
||||
@@ -41,11 +38,7 @@ class Server final {
|
||||
"Server wasn't shutdown properly");
|
||||
}
|
||||
|
||||
void Start() {
|
||||
MG_ASSERT(!background_thread_, "The server was already started!");
|
||||
listener_->Run();
|
||||
background_thread_.emplace([this] { ioc_.run(); });
|
||||
}
|
||||
void Start();
|
||||
|
||||
void Shutdown() { ioc_.stop(); }
|
||||
|
||||
@@ -55,7 +48,7 @@ class Server final {
|
||||
}
|
||||
}
|
||||
bool IsRunning() const { return background_thread_ && !ioc_.stopped(); }
|
||||
tcp::endpoint GetEndpoint() const { return listener_->GetEndpoint(); }
|
||||
tcp::endpoint GetEndpoint() const;
|
||||
|
||||
private:
|
||||
boost::asio::io_context ioc_;
|
||||
@@ -63,4 +56,22 @@ class Server final {
|
||||
std::shared_ptr<Listener<TRequestHandler, TSessionContext>> listener_;
|
||||
std::optional<std::thread> background_thread_;
|
||||
};
|
||||
template <class TRequestHandler, typename TSessionContext>
|
||||
Server<TRequestHandler, TSessionContext>::Server(io::network::Endpoint endpoint, TSessionContext *session_context,
|
||||
ServerContext *context)
|
||||
: listener_{Listener<TRequestHandler, TSessionContext>::Create(
|
||||
ioc_, session_context, context,
|
||||
tcp::endpoint{boost::asio::ip::make_address(endpoint.address), endpoint.port})} {}
|
||||
|
||||
template <class TRequestHandler, typename TSessionContext>
|
||||
void Server<TRequestHandler, TSessionContext>::Start() {
|
||||
MG_ASSERT(!background_thread_, "The server was already started!");
|
||||
listener_->Run();
|
||||
background_thread_.emplace([this] { ioc_.run(); });
|
||||
}
|
||||
|
||||
template <class TRequestHandler, typename TSessionContext>
|
||||
boost::asio::ip::tcp::endpoint Server<TRequestHandler, TSessionContext>::GetEndpoint() const {
|
||||
return listener_->GetEndpoint();
|
||||
}
|
||||
} // namespace memgraph::communication::http
|
||||
|
||||
@@ -58,11 +58,11 @@ class Server final {
|
||||
* Constructs and binds server to endpoint, operates on session data and
|
||||
* invokes workers_count workers
|
||||
*/
|
||||
Server(const io::network::Endpoint &endpoint, TSessionContext *session_context, ServerContext *context,
|
||||
Server(io::network::Endpoint endpoint, TSessionContext *session_context, ServerContext *context,
|
||||
int inactivity_timeout_sec, const std::string &service_name,
|
||||
size_t workers_count = std::thread::hardware_concurrency())
|
||||
: alive_(false),
|
||||
endpoint_(endpoint),
|
||||
endpoint_(std::move(endpoint)),
|
||||
listener_(session_context, context, inactivity_timeout_sec, service_name, workers_count),
|
||||
service_name_(service_name) {}
|
||||
|
||||
|
||||
@@ -73,40 +73,19 @@ class Server final {
|
||||
* invokes workers_count workers
|
||||
*/
|
||||
Server(ServerEndpoint &endpoint, TSessionContext *session_context, ServerContext *server_context,
|
||||
const int inactivity_timeout_sec, const std::string_view service_name,
|
||||
size_t workers_count = std::thread::hardware_concurrency())
|
||||
: endpoint_{endpoint},
|
||||
service_name_{service_name},
|
||||
context_thread_pool_{workers_count},
|
||||
listener_{Listener<TSession, TSessionContext>::Create(context_thread_pool_.GetIOContext(), session_context,
|
||||
server_context, endpoint_, service_name_,
|
||||
inactivity_timeout_sec)} {}
|
||||
int inactivity_timeout_sec, std::string_view service_name,
|
||||
size_t workers_count = std::thread::hardware_concurrency());
|
||||
|
||||
~Server() { MG_ASSERT(!IsRunning(), "Server wasn't shutdown properly"); }
|
||||
~Server();
|
||||
|
||||
Server(const Server &) = delete;
|
||||
Server(Server &&) = delete;
|
||||
Server &operator=(const Server &) = delete;
|
||||
Server &operator=(Server &&) = delete;
|
||||
|
||||
const auto &Endpoint() const {
|
||||
MG_ASSERT(IsRunning(), "You can't get the server endpoint when it's not running!");
|
||||
return endpoint_;
|
||||
}
|
||||
const auto &Endpoint() const;
|
||||
|
||||
bool Start() {
|
||||
if (IsRunning()) {
|
||||
spdlog::error("The server is already running");
|
||||
return false;
|
||||
}
|
||||
listener_->Start();
|
||||
|
||||
spdlog::info("{} server is fully armed and operational", service_name_);
|
||||
spdlog::info("{} listening on {}", service_name_, endpoint_.address());
|
||||
context_thread_pool_.Run();
|
||||
|
||||
return true;
|
||||
}
|
||||
bool Start();
|
||||
|
||||
void Shutdown() {
|
||||
context_thread_pool_.Shutdown();
|
||||
@@ -115,7 +94,7 @@ class Server final {
|
||||
|
||||
void AwaitShutdown() { context_thread_pool_.AwaitShutdown(); }
|
||||
|
||||
bool IsRunning() const noexcept { return context_thread_pool_.IsRunning() && listener_->IsRunning(); }
|
||||
bool IsRunning() const noexcept;
|
||||
|
||||
private:
|
||||
ServerEndpoint endpoint_;
|
||||
@@ -125,4 +104,46 @@ class Server final {
|
||||
std::shared_ptr<Listener<TSession, TSessionContext>> listener_;
|
||||
};
|
||||
|
||||
template <typename TSession, typename TSessionContext>
|
||||
Server<TSession, TSessionContext>::~Server() {
|
||||
MG_ASSERT(!IsRunning(), "Server wasn't shutdown properly");
|
||||
}
|
||||
|
||||
template <typename TSession, typename TSessionContext>
|
||||
Server<TSession, TSessionContext>::Server(ServerEndpoint &endpoint, TSessionContext *session_context,
|
||||
ServerContext *server_context, const int inactivity_timeout_sec,
|
||||
const std::string_view service_name, size_t workers_count)
|
||||
: endpoint_{endpoint},
|
||||
service_name_{service_name},
|
||||
context_thread_pool_{workers_count},
|
||||
listener_{Listener<TSession, TSessionContext>::Create(context_thread_pool_.GetIOContext(), session_context,
|
||||
server_context, endpoint_, service_name_,
|
||||
inactivity_timeout_sec)} {}
|
||||
|
||||
template <typename TSession, typename TSessionContext>
|
||||
bool Server<TSession, TSessionContext>::Start() {
|
||||
if (IsRunning()) {
|
||||
spdlog::error("The server is already running");
|
||||
return false;
|
||||
}
|
||||
listener_->Start();
|
||||
|
||||
spdlog::info("{} server is fully armed and operational", service_name_);
|
||||
spdlog::info("{} listening on {}", service_name_, endpoint_.address());
|
||||
context_thread_pool_.Run();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename TSession, typename TSessionContext>
|
||||
const auto &Server<TSession, TSessionContext>::Endpoint() const {
|
||||
MG_ASSERT(IsRunning(), "You can't get the server endpoint when it's not running!");
|
||||
return endpoint_;
|
||||
}
|
||||
|
||||
template <typename TSession, typename TSessionContext>
|
||||
bool Server<TSession, TSessionContext>::IsRunning() const noexcept {
|
||||
return context_thread_pool_.IsRunning() && listener_->IsRunning();
|
||||
}
|
||||
|
||||
} // namespace memgraph::communication::v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -31,7 +31,7 @@
|
||||
template <typename TElement>
|
||||
class RingBuffer {
|
||||
public:
|
||||
explicit RingBuffer(int capacity) : capacity_(capacity) { buffer_ = std::make_unique<TElement[]>(capacity_); }
|
||||
explicit RingBuffer(int capacity) : capacity_(capacity), buffer_{std::make_unique<TElement[]>(capacity_)} {}
|
||||
|
||||
RingBuffer(const RingBuffer &) = delete;
|
||||
RingBuffer(RingBuffer &&) = delete;
|
||||
|
||||
18
src/flags/all.hpp
Normal file
18
src/flags/all.hpp
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "flags/audit.hpp"
|
||||
#include "flags/bolt.hpp"
|
||||
#include "flags/general.hpp"
|
||||
#include "flags/isolation_level.hpp"
|
||||
#include "flags/log_level.hpp"
|
||||
#include "flags/memory_limit.hpp"
|
||||
28
src/flags/audit.cpp
Normal file
28
src/flags/audit.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright 2023 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.
|
||||
#include "flags/audit.hpp"
|
||||
|
||||
#include "audit/log.hpp"
|
||||
|
||||
#include "utils/flag_validation.hpp"
|
||||
|
||||
// Audit logging flags.
|
||||
#ifdef MG_ENTERPRISE
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(audit_enabled, false, "Set to true to enable audit logging.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(audit_buffer_size, memgraph::audit::kBufferSizeDefault,
|
||||
"Maximum number of items in the audit log buffer.", FLAG_IN_RANGE(1, INT32_MAX));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(audit_buffer_flush_interval_ms, memgraph::audit::kBufferFlushIntervalMillisDefault,
|
||||
"Interval (in milliseconds) used for flushing the audit log buffer.",
|
||||
FLAG_IN_RANGE(10, INT32_MAX));
|
||||
#endif
|
||||
23
src/flags/audit.hpp
Normal file
23
src/flags/audit.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
|
||||
// Audit logging flags.
|
||||
#ifdef MG_ENTERPRISE
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(audit_enabled);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_int32(audit_buffer_size);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_int32(audit_buffer_flush_interval_ms);
|
||||
#endif
|
||||
38
src/flags/bolt.cpp
Normal file
38
src/flags/bolt.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2023 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.
|
||||
#include "flags/bolt.hpp"
|
||||
|
||||
#include "utils/flag_validation.hpp"
|
||||
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
|
||||
// Bolt server flags.
|
||||
DEFINE_string(bolt_address, "0.0.0.0", "IP address on which the Bolt server should listen.");
|
||||
|
||||
DEFINE_VALIDATED_int32(bolt_port, 7687, "Port on which the Bolt server should listen.",
|
||||
FLAG_IN_RANGE(0, std::numeric_limits<uint16_t>::max()));
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(bolt_num_workers, std::max(std::thread::hardware_concurrency(), 1U),
|
||||
"Number of workers used by the Bolt server. By default, this will be the "
|
||||
"number of processing units available on the machine.",
|
||||
FLAG_IN_RANGE(1, INT32_MAX));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(bolt_session_inactivity_timeout, 1800,
|
||||
"Time in seconds after which inactive Bolt sessions will be "
|
||||
"closed.",
|
||||
FLAG_IN_RANGE(1, INT32_MAX));
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(bolt_cert_file, "", "Certificate file which should be used for the Bolt server.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(bolt_key_file, "", "Key file which should be used for the Bolt server.");
|
||||
27
src/flags/bolt.hpp
Normal file
27
src/flags/bolt.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
|
||||
// Bolt server flags.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(bolt_address);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_int32(bolt_port);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_int32(bolt_num_workers);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_int32(bolt_session_inactivity_timeout);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(bolt_cert_file);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(bolt_key_file);
|
||||
194
src/flags/general.cpp
Normal file
194
src/flags/general.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#include "general.hpp"
|
||||
|
||||
#include "utils/flag_validation.hpp"
|
||||
|
||||
#include "glue/auth_handler.hpp"
|
||||
|
||||
// Short help flag.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_bool(h, false, "Print usage and exit.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(monitoring_address, "0.0.0.0",
|
||||
"IP address on which the websocket server for Memgraph monitoring should listen.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(metrics_address, "0.0.0.0",
|
||||
"IP address on which the Memgraph server for exposing metrics should listen.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(monitoring_port, 7444,
|
||||
"Port on which the websocket server for Memgraph monitoring should listen.",
|
||||
FLAG_IN_RANGE(0, std::numeric_limits<uint16_t>::max()));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(metrics_port, 9091, "Port on which the Memgraph server for exposing metrics should listen.",
|
||||
FLAG_IN_RANGE(0, std::numeric_limits<uint16_t>::max()));
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(init_file, "",
|
||||
"Path to cypherl file that is used for configuring users and database schema before server starts.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(init_data_file, "", "Path to cypherl file that is used for creating data after server starts.");
|
||||
|
||||
// General purpose flags.
|
||||
// NOTE: The `data_directory` flag must be the same here and in
|
||||
// `mg_import_csv`. If you change it, make sure to change it there as well.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(data_directory, "mg_data", "Path to directory in which to save all permanent data.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(data_recovery_on_startup, false, "Controls whether the database recovers persisted data on startup.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(memory_warning_threshold, 1024,
|
||||
"Memory warning threshold, in MB. If Memgraph detects there is "
|
||||
"less available RAM it will log a warning. Set to 0 to "
|
||||
"disable.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(allow_load_csv, true, "Controls whether LOAD CSV clause is allowed in queries.");
|
||||
|
||||
// Storage flags.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_gc_cycle_sec, 30, "Storage garbage collector interval (in seconds).",
|
||||
FLAG_IN_RANGE(1, 24 * 3600));
|
||||
// NOTE: The `storage_properties_on_edges` flag must be the same here and in
|
||||
// `mg_import_csv`. If you change it, make sure to change it there as well.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_properties_on_edges, false, "Controls whether edges have properties.");
|
||||
|
||||
// storage_recover_on_startup deprecated; use data_recovery_on_startup instead
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_bool(storage_recover_on_startup, false,
|
||||
"Controls whether the storage recovers persisted data on startup.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_snapshot_interval_sec, 0,
|
||||
"Storage snapshot creation interval (in seconds). Set "
|
||||
"to 0 to disable periodic snapshot creation.",
|
||||
FLAG_IN_RANGE(0, 7 * 24 * 3600));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_wal_enabled, false,
|
||||
"Controls whether the storage uses write-ahead-logging. To enable "
|
||||
"WAL periodic snapshots must be enabled.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_snapshot_retention_count, 3, "The number of snapshots that should always be kept.",
|
||||
FLAG_IN_RANGE(1, 1000000));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_wal_file_size_kib, memgraph::storage::Config::Durability().wal_file_size_kibibytes,
|
||||
"Minimum file size of each WAL file.",
|
||||
FLAG_IN_RANGE(1, static_cast<unsigned long>(1000) * 1024));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_wal_file_flush_every_n_tx,
|
||||
memgraph::storage::Config::Durability().wal_file_flush_every_n_tx,
|
||||
"Issue a 'fsync' call after this amount of transactions are written to the "
|
||||
"WAL file. Set to 1 for fully synchronous operation.",
|
||||
FLAG_IN_RANGE(1, 1000000));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_snapshot_on_exit, false, "Controls whether the storage creates another snapshot on exit.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(storage_items_per_batch, memgraph::storage::Config::Durability().items_per_batch,
|
||||
"The number of edges and vertices stored in a batch in a snapshot file.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_parallel_index_recovery, false,
|
||||
"Controls whether the index creation can be done in a multithreaded fashion.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(storage_recovery_thread_count,
|
||||
std::max(static_cast<uint64_t>(std::thread::hardware_concurrency()),
|
||||
memgraph::storage::Config::Durability().recovery_thread_count),
|
||||
"The number of threads used to recover persisted data from disk.");
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_delete_on_drop, true,
|
||||
"If set to true the query 'DROP DATABASE x' will delete the underlying storage as well.");
|
||||
#endif
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(telemetry_enabled, false,
|
||||
"Set to true to enable telemetry. We collect information about the "
|
||||
"running system (CPU and memory information) and information about "
|
||||
"the database runtime (vertex and edge counts and resource usage) "
|
||||
"to allow for easier improvement of the product.");
|
||||
|
||||
// Streams flags
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint32(
|
||||
stream_transaction_conflict_retries, 30,
|
||||
"Number of times to retry when a stream transformation fails to commit because of conflicting transactions");
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint32(
|
||||
stream_transaction_retry_interval, 500,
|
||||
"Retry interval in milliseconds when a stream transformation fails to commit because of conflicting transactions");
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(kafka_bootstrap_servers, "",
|
||||
"List of default Kafka brokers as a comma separated list of broker host or host:port.");
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(pulsar_service_url, "", "Default URL used while connecting to Pulsar brokers.");
|
||||
|
||||
// Query flags.
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_double(query_execution_timeout_sec, 600,
|
||||
"Maximum allowed query execution time. Queries exceeding this "
|
||||
"limit will be aborted. Value of 0 means no limit.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(replication_replica_check_frequency_sec, 1,
|
||||
"The time duration between two replica checks/pings. If < 1, replicas will NOT be checked at all. NOTE: "
|
||||
"The MAIN instance allocates a new thread for each REPLICA.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(replication_restore_state_on_startup, false, "Restore replication state on startup, e.g. recover replica");
|
||||
|
||||
DEFINE_VALIDATED_string(query_modules_directory, "",
|
||||
"Directory where modules with custom query procedures are stored. "
|
||||
"NOTE: Multiple comma-separated directories can be defined.",
|
||||
{
|
||||
if (value.empty()) return true;
|
||||
const auto directories = memgraph::utils::Split(value, ",");
|
||||
for (const auto &dir : directories) {
|
||||
if (!memgraph::utils::DirExists(dir)) {
|
||||
std::cout << "Expected --" << flagname << " to point to directories." << std::endl;
|
||||
std::cout << dir << " is not a directory." << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
auto memgraph::flags::ParseQueryModulesDirectory() -> std::vector<std::filesystem::path> {
|
||||
const auto directories = memgraph::utils::Split(FLAGS_query_modules_directory, ",");
|
||||
std::vector<std::filesystem::path> query_modules_directories;
|
||||
query_modules_directories.reserve(directories.size());
|
||||
std::transform(directories.begin(), directories.end(), std::back_inserter(query_modules_directories),
|
||||
[](const auto &dir) { return dir; });
|
||||
|
||||
return query_modules_directories;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(query_callable_mappings_path, "",
|
||||
"The path to mappings that describes aliases to callables in cypher queries in the form of key-value "
|
||||
"pairs in a json file. With this option query module procedures that do not exist in memgraph can be "
|
||||
"mapped to ones that exist.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_string(license_key, "", "License key for Memgraph Enterprise.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_string(organization_name, "", "Organization name.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(auth_user_or_role_name_regex, memgraph::glue::kDefaultUserRoleRegex.data(),
|
||||
"Set to the regular expression that each user or role name must fulfill.");
|
||||
122
src/flags/general.hpp
Normal file
122
src/flags/general.hpp
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
// Short help flag.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(h);
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(monitoring_address);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_int32(monitoring_port);
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(metrics_address);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_int32(metrics_port);
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(init_file);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(init_data_file);
|
||||
|
||||
// General purpose flags.
|
||||
// NOTE: The `data_directory` flag must be the same here and in
|
||||
// `mg_import_csv`. If you change it, make sure to change it there as well.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(data_directory);
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(data_recovery_on_startup);
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(memory_warning_threshold);
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(allow_load_csv);
|
||||
|
||||
// Storage flags.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(storage_gc_cycle_sec);
|
||||
// NOTE: The `storage_properties_on_edges` flag must be the same here and in
|
||||
// `mg_import_csv`. If you change it, make sure to change it there as well.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(storage_properties_on_edges);
|
||||
// storage_recover_on_startup deprecated; use data_recovery_on_startup instead
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(storage_recover_on_startup);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(storage_snapshot_interval_sec);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(storage_wal_enabled);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(storage_snapshot_retention_count);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(storage_wal_file_size_kib);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(storage_wal_file_flush_every_n_tx);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(storage_snapshot_on_exit);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(storage_items_per_batch);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(storage_parallel_index_recovery);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(storage_recovery_thread_count);
|
||||
#ifdef MG_ENTERPRISE
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(storage_delete_on_drop);
|
||||
#endif
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(telemetry_enabled);
|
||||
|
||||
// Streams flags
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint32(stream_transaction_conflict_retries);
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint32(stream_transaction_retry_interval);
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(kafka_bootstrap_servers);
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(pulsar_service_url);
|
||||
|
||||
// Query flags.
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_double(query_execution_timeout_sec);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(query_modules_directory);
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(query_callable_mappings_path);
|
||||
namespace memgraph::flags {
|
||||
auto ParseQueryModulesDirectory() -> std::vector<std::filesystem::path>;
|
||||
} // namespace memgraph::flags
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_uint64(replication_replica_check_frequency_sec);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_bool(replication_restore_state_on_startup);
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(license_key);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(organization_name);
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DECLARE_string(auth_user_or_role_name_regex);
|
||||
54
src/flags/isolation_level.cpp
Normal file
54
src/flags/isolation_level.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2023 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.
|
||||
#include "flags/isolation_level.hpp"
|
||||
|
||||
#include "utils/enum.hpp"
|
||||
#include "utils/flag_validation.hpp"
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
|
||||
inline constexpr std::array isolation_level_mappings{
|
||||
std::pair{std::string_view{"SNAPSHOT_ISOLATION"}, memgraph::storage::IsolationLevel::SNAPSHOT_ISOLATION},
|
||||
std::pair{std::string_view{"READ_COMMITTED"}, memgraph::storage::IsolationLevel::READ_COMMITTED},
|
||||
std::pair{std::string_view{"READ_UNCOMMITTED"}, memgraph::storage::IsolationLevel::READ_UNCOMMITTED}};
|
||||
|
||||
const std::string isolation_level_help_string =
|
||||
fmt::format("Default isolation level used for the transactions. Allowed values: {}",
|
||||
memgraph::utils::GetAllowedEnumValuesString(isolation_level_mappings));
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_string(isolation_level, "SNAPSHOT_ISOLATION", isolation_level_help_string.c_str(), {
|
||||
if (const auto result = memgraph::utils::IsValidEnumValueString(value, isolation_level_mappings); result.HasError()) {
|
||||
switch (result.GetError()) {
|
||||
case memgraph::utils::ValidationError::EmptyValue: {
|
||||
std::cout << "Isolation level cannot be empty." << std::endl;
|
||||
break;
|
||||
}
|
||||
case memgraph::utils::ValidationError::InvalidValue: {
|
||||
std::cout << "Invalid value for isolation level. Allowed values: "
|
||||
<< memgraph::utils::GetAllowedEnumValuesString(isolation_level_mappings) << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
memgraph::storage::IsolationLevel memgraph::flags::ParseIsolationLevel() {
|
||||
const auto isolation_level =
|
||||
memgraph::utils::StringToEnum<memgraph::storage::IsolationLevel>(FLAGS_isolation_level, isolation_level_mappings);
|
||||
MG_ASSERT(isolation_level, "Invalid isolation level");
|
||||
return *isolation_level;
|
||||
}
|
||||
19
src/flags/isolation_level.hpp
Normal file
19
src/flags/isolation_level.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "storage/v2/isolation_level.hpp"
|
||||
|
||||
namespace memgraph::flags {
|
||||
|
||||
memgraph::storage::IsolationLevel ParseIsolationLevel();
|
||||
|
||||
} // namespace memgraph::flags
|
||||
101
src/flags/log_level.cpp
Normal file
101
src/flags/log_level.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright 2023 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.
|
||||
#include "flags/log_level.hpp"
|
||||
|
||||
#include "utils/enum.hpp"
|
||||
#include "utils/flag_validation.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
#include "spdlog/common.h"
|
||||
#include "spdlog/sinks/daily_file_sink.h"
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
// Logging flags
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_bool(also_log_to_stderr, false, "Log messages go to stderr in addition to logfiles");
|
||||
DEFINE_string(log_file, "", "Path to where the log should be stored.");
|
||||
|
||||
inline constexpr std::array log_level_mappings{
|
||||
std::pair{"TRACE"sv, spdlog::level::trace}, std::pair{"DEBUG"sv, spdlog::level::debug},
|
||||
std::pair{"INFO"sv, spdlog::level::info}, std::pair{"WARNING"sv, spdlog::level::warn},
|
||||
std::pair{"ERROR"sv, spdlog::level::err}, std::pair{"CRITICAL"sv, spdlog::level::critical}};
|
||||
|
||||
const std::string log_level_help_string = fmt::format("Minimum log level. Allowed values: {}",
|
||||
memgraph::utils::GetAllowedEnumValuesString(log_level_mappings));
|
||||
|
||||
DEFINE_VALIDATED_string(log_level, "WARNING", log_level_help_string.c_str(), {
|
||||
if (const auto result = memgraph::utils::IsValidEnumValueString(value, log_level_mappings); result.HasError()) {
|
||||
const auto error = result.GetError();
|
||||
switch (error) {
|
||||
case memgraph::utils::ValidationError::EmptyValue: {
|
||||
std::cout << "Log level cannot be empty." << std::endl;
|
||||
break;
|
||||
}
|
||||
case memgraph::utils::ValidationError::InvalidValue: {
|
||||
std::cout << "Invalid value for log level. Allowed values: "
|
||||
<< memgraph::utils::GetAllowedEnumValuesString(log_level_mappings) << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
spdlog::level::level_enum ParseLogLevel() {
|
||||
const auto log_level = memgraph::utils::StringToEnum<spdlog::level::level_enum>(FLAGS_log_level, log_level_mappings);
|
||||
MG_ASSERT(log_level, "Invalid log level");
|
||||
return *log_level;
|
||||
}
|
||||
|
||||
// 5 weeks * 7 days
|
||||
inline constexpr auto log_retention_count = 35;
|
||||
void CreateLoggerFromSink(const auto &sinks, const auto log_level) {
|
||||
auto logger = std::make_shared<spdlog::logger>("memgraph_log", sinks.begin(), sinks.end());
|
||||
logger->set_level(log_level);
|
||||
logger->flush_on(spdlog::level::trace);
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
}
|
||||
|
||||
void memgraph::flags::InitializeLogger() {
|
||||
std::vector<spdlog::sink_ptr> sinks;
|
||||
|
||||
if (FLAGS_also_log_to_stderr) {
|
||||
sinks.emplace_back(std::make_shared<spdlog::sinks::stderr_color_sink_mt>());
|
||||
}
|
||||
|
||||
if (!FLAGS_log_file.empty()) {
|
||||
// get local time
|
||||
time_t current_time{0};
|
||||
struct tm *local_time{nullptr};
|
||||
|
||||
time(¤t_time);
|
||||
local_time = localtime(¤t_time);
|
||||
|
||||
sinks.emplace_back(std::make_shared<spdlog::sinks::daily_file_sink_mt>(
|
||||
FLAGS_log_file, local_time->tm_hour, local_time->tm_min, false, log_retention_count));
|
||||
}
|
||||
CreateLoggerFromSink(sinks, ParseLogLevel());
|
||||
}
|
||||
|
||||
void memgraph::flags::AddLoggerSink(spdlog::sink_ptr new_sink) {
|
||||
auto default_logger = spdlog::default_logger();
|
||||
auto sinks = default_logger->sinks();
|
||||
sinks.push_back(new_sink);
|
||||
CreateLoggerFromSink(sinks, default_logger->level());
|
||||
}
|
||||
18
src/flags/log_level.hpp
Normal file
18
src/flags/log_level.hpp
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include <spdlog/sinks/sink.h>
|
||||
|
||||
namespace memgraph::flags {
|
||||
void InitializeLogger();
|
||||
void AddLoggerSink(spdlog::sink_ptr new_sink);
|
||||
} // namespace memgraph::flags
|
||||
41
src/flags/memory_limit.cpp
Normal file
41
src/flags/memory_limit.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2023 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.
|
||||
#include "flags/memory_limit.hpp"
|
||||
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/sysinfo/memory.hpp"
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(
|
||||
memory_limit, 0,
|
||||
"Total memory limit in MiB. Set to 0 to use the default values which are 100\% of the phyisical memory if the swap "
|
||||
"is enabled and 90\% of the physical memory otherwise.");
|
||||
|
||||
int64_t memgraph::flags::GetMemoryLimit() {
|
||||
if (FLAGS_memory_limit == 0) {
|
||||
auto maybe_total_memory = memgraph::utils::sysinfo::TotalMemory();
|
||||
MG_ASSERT(maybe_total_memory, "Failed to fetch the total physical memory");
|
||||
const auto maybe_swap_memory = memgraph::utils::sysinfo::SwapTotalMemory();
|
||||
MG_ASSERT(maybe_swap_memory, "Failed to fetch the total swap memory");
|
||||
|
||||
if (*maybe_swap_memory == 0) {
|
||||
// take only 90% of the total memory
|
||||
*maybe_total_memory *= 9;
|
||||
*maybe_total_memory /= 10;
|
||||
}
|
||||
return *maybe_total_memory * 1024;
|
||||
}
|
||||
|
||||
// We parse the memory as MiB every time
|
||||
return FLAGS_memory_limit * 1024 * 1024;
|
||||
}
|
||||
17
src/flags/memory_limit.hpp
Normal file
17
src/flags/memory_limit.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace memgraph::flags {
|
||||
int64_t GetMemoryLimit();
|
||||
} // namespace memgraph::flags
|
||||
@@ -1,4 +1,4 @@
|
||||
set(mg_glue_sources auth.cpp auth_checker.cpp auth_handler.cpp communication.cpp)
|
||||
|
||||
add_library(mg-glue STATIC ${mg_glue_sources})
|
||||
target_link_libraries(mg-glue mg-query mg-auth)
|
||||
add_library(mg-glue STATIC )
|
||||
target_sources(mg-glue PRIVATE auth.cpp auth_checker.cpp auth_handler.cpp communication.cpp SessionHL.cpp ServerT.cpp MonitoringServerT.cpp)
|
||||
target_link_libraries(mg-glue mg-query mg-auth mg-audit)
|
||||
target_precompile_headers(mg-glue INTERFACE auth_checker.hpp auth_handler.hpp)
|
||||
|
||||
14
src/glue/MonitoringServerT.cpp
Normal file
14
src/glue/MonitoringServerT.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright 2023 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.
|
||||
#include "glue/MonitoringServerT.hpp"
|
||||
|
||||
template class memgraph::communication::http::Server<
|
||||
memgraph::http::MetricsRequestHandler<memgraph::dbms::SessionContext>, memgraph::dbms::SessionContext>;
|
||||
25
src/glue/MonitoringServerT.hpp
Normal file
25
src/glue/MonitoringServerT.hpp
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "communication/http/server.hpp"
|
||||
#include "dbms/session_context.hpp"
|
||||
#include "http_handlers/metrics.hpp"
|
||||
|
||||
extern template class memgraph::communication::http::Server<
|
||||
memgraph::http::MetricsRequestHandler<memgraph::dbms::SessionContext>, memgraph::dbms::SessionContext>;
|
||||
|
||||
namespace memgraph::glue {
|
||||
|
||||
using MonitoringServerT =
|
||||
memgraph::communication::http::Server<memgraph::http::MetricsRequestHandler<memgraph::dbms::SessionContext>,
|
||||
memgraph::dbms::SessionContext>;
|
||||
} // namespace memgraph::glue
|
||||
17
src/glue/ServerT.cpp
Normal file
17
src/glue/ServerT.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2023 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.
|
||||
#include "glue/ServerT.hpp"
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
template class memgraph::communication::v2::Server<memgraph::glue::SessionHL, memgraph::dbms::SessionContextHandler>;
|
||||
#else
|
||||
template class memgraph::communication::v2::Server<memgraph::glue::SessionHL, memgraph::dbms::SessionContext>;
|
||||
#endif
|
||||
35
src/glue/ServerT.hpp
Normal file
35
src/glue/ServerT.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "communication/v2/server.hpp"
|
||||
#include "glue/SessionHL.hpp"
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
#include "dbms/session_context_handler.hpp"
|
||||
#else
|
||||
#include "dbms/session_context.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
extern template class memgraph::communication::v2::Server<memgraph::glue::SessionHL,
|
||||
memgraph::dbms::SessionContextHandler>;
|
||||
#else
|
||||
extern template class memgraph::communication::v2::Server<memgraph::glue::SessionHL, memgraph::dbms::SessionContext>;
|
||||
#endif
|
||||
|
||||
namespace memgraph::glue {
|
||||
#ifdef MG_ENTERPRISE
|
||||
using ServerT = memgraph::communication::v2::Server<memgraph::glue::SessionHL, memgraph::dbms::SessionContextHandler>;
|
||||
#else
|
||||
using ServerT = memgraph::communication::v2::Server<memgraph::glue::SessionHL, memgraph::dbms::SessionContext>;
|
||||
#endif
|
||||
} // namespace memgraph::glue
|
||||
386
src/glue/SessionHL.cpp
Normal file
386
src/glue/SessionHL.cpp
Normal file
@@ -0,0 +1,386 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#include "glue/SessionHL.hpp"
|
||||
|
||||
#include "audit/log.hpp"
|
||||
#include "glue/auth_checker.hpp"
|
||||
#include "glue/communication.hpp"
|
||||
#include "license/license.hpp"
|
||||
#include "query/discard_value_stream.hpp"
|
||||
|
||||
#include "gflags/gflags.h"
|
||||
|
||||
namespace memgraph::metrics {
|
||||
extern const Event ActiveBoltSessions;
|
||||
} // namespace memgraph::metrics
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(bolt_server_name_for_init, "",
|
||||
"Server name which the database should send to the client in the "
|
||||
"Bolt INIT message.");
|
||||
|
||||
auto ToQueryExtras(const memgraph::communication::bolt::Value &extra) -> memgraph::query::QueryExtras {
|
||||
auto const &as_map = extra.ValueMap();
|
||||
|
||||
auto metadata_pv = std::map<std::string, memgraph::storage::PropertyValue>{};
|
||||
|
||||
if (auto const it = as_map.find("tx_metadata"); it != as_map.cend() && it->second.IsMap()) {
|
||||
for (const auto &[key, bolt_md] : it->second.ValueMap()) {
|
||||
metadata_pv.emplace(key, memgraph::glue::ToPropertyValue(bolt_md));
|
||||
}
|
||||
}
|
||||
|
||||
auto tx_timeout = std::optional<int64_t>{};
|
||||
if (auto const it = as_map.find("tx_timeout"); it != as_map.cend() && it->second.IsInt()) {
|
||||
tx_timeout = it->second.ValueInt();
|
||||
}
|
||||
|
||||
return memgraph::query::QueryExtras{std::move(metadata_pv), tx_timeout};
|
||||
}
|
||||
|
||||
class TypedValueResultStreamBase {
|
||||
public:
|
||||
explicit TypedValueResultStreamBase(memgraph::query::InterpreterContext *interpreterContext);
|
||||
|
||||
std::vector<memgraph::communication::bolt::Value> DecodeValues(
|
||||
const std::vector<memgraph::query::TypedValue> &values) const;
|
||||
|
||||
private:
|
||||
// NOTE: Needed only for ToBoltValue conversions
|
||||
memgraph::query::InterpreterContext *interpreter_context_;
|
||||
};
|
||||
|
||||
/// Wrapper around TEncoder which converts TypedValue to Value
|
||||
/// before forwarding the calls to original TEncoder.
|
||||
template <typename TEncoder>
|
||||
class TypedValueResultStream : public TypedValueResultStreamBase {
|
||||
public:
|
||||
TypedValueResultStream(TEncoder *encoder, memgraph::query::InterpreterContext *ic)
|
||||
: TypedValueResultStreamBase{ic}, encoder_(encoder) {}
|
||||
|
||||
void Result(const std::vector<memgraph::query::TypedValue> &values) { encoder_->MessageRecord(DecodeValues(values)); }
|
||||
|
||||
private:
|
||||
TEncoder *encoder_;
|
||||
};
|
||||
|
||||
std::vector<memgraph::communication::bolt::Value> TypedValueResultStreamBase::DecodeValues(
|
||||
const std::vector<memgraph::query::TypedValue> &values) const {
|
||||
std::vector<memgraph::communication::bolt::Value> decoded_values;
|
||||
decoded_values.reserve(values.size());
|
||||
for (const auto &v : values) {
|
||||
auto maybe_value = memgraph::glue::ToBoltValue(v, *interpreter_context_->db, memgraph::storage::View::NEW);
|
||||
if (maybe_value.HasError()) {
|
||||
switch (maybe_value.GetError()) {
|
||||
case memgraph::storage::Error::DELETED_OBJECT:
|
||||
throw memgraph::communication::bolt::ClientError("Returning a deleted object as a result.");
|
||||
case memgraph::storage::Error::NONEXISTENT_OBJECT:
|
||||
throw memgraph::communication::bolt::ClientError("Returning a nonexistent object as a result.");
|
||||
case memgraph::storage::Error::VERTEX_HAS_EDGES:
|
||||
case memgraph::storage::Error::SERIALIZATION_ERROR:
|
||||
case memgraph::storage::Error::PROPERTIES_DISABLED:
|
||||
throw memgraph::communication::bolt::ClientError("Unexpected storage error when streaming results.");
|
||||
}
|
||||
}
|
||||
decoded_values.emplace_back(std::move(*maybe_value));
|
||||
}
|
||||
return decoded_values;
|
||||
}
|
||||
TypedValueResultStreamBase::TypedValueResultStreamBase(memgraph::query::InterpreterContext *interpreterContext)
|
||||
: interpreter_context_(interpreterContext) {}
|
||||
|
||||
namespace memgraph::glue {
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
|
||||
void SessionHL::UpdateAndDefunct(const std::string &db_name) {
|
||||
UpdateAndDefunct(ContextWrapper(sc_handler_.Get(db_name)));
|
||||
}
|
||||
void SessionHL::UpdateAndDefunct(ContextWrapper &&cntxt) {
|
||||
defunct_.emplace(std::move(current_));
|
||||
Update(std::forward<ContextWrapper>(cntxt));
|
||||
defunct_->Defunct();
|
||||
}
|
||||
void SessionHL::Update(const std::string &db_name) {
|
||||
ContextWrapper tmp(sc_handler_.Get(db_name));
|
||||
Update(std::move(tmp));
|
||||
}
|
||||
void SessionHL::Update(ContextWrapper &&cntxt) {
|
||||
current_ = std::move(cntxt);
|
||||
interpreter_ = current_.interp();
|
||||
interpreter_->in_explicit_db_ = in_explicit_db_;
|
||||
interpreter_context_ = current_.interpreter_context();
|
||||
}
|
||||
void SessionHL::MultiDatabaseAuth(const std::string &db) {
|
||||
if (user_ && !AuthChecker::IsUserAuthorized(*user_, {}, db)) {
|
||||
throw memgraph::communication::bolt::ClientError(
|
||||
"You are not authorized on the database \"{}\"! Please contact your database administrator.", db);
|
||||
}
|
||||
}
|
||||
std::string SessionHL::GetDefaultDB() {
|
||||
if (user_.has_value()) {
|
||||
return user_->db_access().GetDefault();
|
||||
}
|
||||
return memgraph::dbms::kDefaultDB;
|
||||
}
|
||||
|
||||
bool SessionHL::OnDelete(const std::string &db_name) {
|
||||
MG_ASSERT(current_.interpreter_context()->db->id() != db_name && (!defunct_ || defunct_->defunct()),
|
||||
"Trying to delete a database while still in use.");
|
||||
return true;
|
||||
}
|
||||
memgraph::dbms::SetForResult SessionHL::OnChange(const std::string &db_name) {
|
||||
MultiDatabaseAuth(db_name);
|
||||
if (db_name != current_.interpreter_context()->db->id()) {
|
||||
UpdateAndDefunct(db_name); // Done during Pull, so we cannot just replace the current db
|
||||
return memgraph::dbms::SetForResult::SUCCESS;
|
||||
}
|
||||
return memgraph::dbms::SetForResult::ALREADY_SET;
|
||||
}
|
||||
|
||||
#endif
|
||||
std::string SessionHL::GetDatabaseName() const { return interpreter_context_->db->id(); }
|
||||
|
||||
std::optional<std::string> SessionHL::GetServerNameForInit() {
|
||||
if (FLAGS_bolt_server_name_for_init.empty()) return std::nullopt;
|
||||
return FLAGS_bolt_server_name_for_init;
|
||||
}
|
||||
bool SessionHL::Authenticate(const std::string &username, const std::string &password) {
|
||||
auto locked_auth = auth_->Lock();
|
||||
if (!locked_auth->HasUsers()) {
|
||||
return true;
|
||||
}
|
||||
user_ = locked_auth->Authenticate(username, password);
|
||||
#ifdef MG_ENTERPRISE
|
||||
if (user_.has_value()) {
|
||||
const auto &db = user_->db_access().GetDefault();
|
||||
// Check if the underlying database needs to be updated
|
||||
if (db != current_.interpreter_context()->db->id()) {
|
||||
const auto &res = sc_handler_.SetFor(UUID(), db);
|
||||
return res == memgraph::dbms::SetForResult::SUCCESS || res == memgraph::dbms::SetForResult::ALREADY_SET;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return user_.has_value();
|
||||
}
|
||||
void SessionHL::Abort() { interpreter_->Abort(); }
|
||||
|
||||
std::map<std::string, memgraph::communication::bolt::Value> SessionHL::Discard(std::optional<int> n,
|
||||
std::optional<int> qid) {
|
||||
try {
|
||||
memgraph::query::DiscardValueResultStream stream;
|
||||
return DecodeSummary(interpreter_->Pull(&stream, n, qid));
|
||||
} catch (const memgraph::query::QueryException &e) {
|
||||
// Wrap QueryException into ClientError, because we want to allow the
|
||||
// client to fix their query.
|
||||
throw memgraph::communication::bolt::ClientError(e.what());
|
||||
}
|
||||
}
|
||||
std::map<std::string, memgraph::communication::bolt::Value> SessionHL::Pull(SessionHL::TEncoder *encoder,
|
||||
std::optional<int> n,
|
||||
std::optional<int> qid) {
|
||||
try {
|
||||
TypedValueResultStream<TEncoder> stream(encoder, interpreter_context_);
|
||||
return DecodeSummary(interpreter_->Pull(&stream, n, qid));
|
||||
} catch (const memgraph::query::QueryException &e) {
|
||||
// Wrap QueryException into ClientError, because we want to allow the
|
||||
// client to fix their query.
|
||||
throw memgraph::communication::bolt::ClientError(e.what());
|
||||
}
|
||||
}
|
||||
std::pair<std::vector<std::string>, std::optional<int>> SessionHL::Interpret(
|
||||
const std::string &query, const std::map<std::string, memgraph::communication::bolt::Value> ¶ms,
|
||||
const std::map<std::string, memgraph::communication::bolt::Value> &extra) {
|
||||
std::map<std::string, memgraph::storage::PropertyValue> params_pv;
|
||||
for (const auto &[key, bolt_param] : params) {
|
||||
params_pv.emplace(key, ToPropertyValue(bolt_param));
|
||||
}
|
||||
const std::string *username{nullptr};
|
||||
if (user_) {
|
||||
username = &user_->username();
|
||||
}
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
if (memgraph::license::global_license_checker.IsEnterpriseValidFast()) {
|
||||
audit_log_->Record(endpoint_.address().to_string(), user_ ? *username : "", query,
|
||||
memgraph::storage::PropertyValue(params_pv), interpreter_context_->db->id());
|
||||
}
|
||||
#endif
|
||||
try {
|
||||
auto result = interpreter_->Prepare(query, params_pv, username, ToQueryExtras(extra), UUID());
|
||||
const std::string db_name = result.db ? *result.db : "";
|
||||
if (user_ && !AuthChecker::IsUserAuthorized(*user_, result.privileges, db_name)) {
|
||||
interpreter_->Abort();
|
||||
if (db_name.empty()) {
|
||||
throw memgraph::communication::bolt::ClientError(
|
||||
"You are not authorized to execute this query! Please contact your database administrator.");
|
||||
}
|
||||
throw memgraph::communication::bolt::ClientError(
|
||||
"You are not authorized to execute this query on database \"{}\"! Please contact your database "
|
||||
"administrator.",
|
||||
db_name);
|
||||
}
|
||||
return {std::move(result.headers), result.qid};
|
||||
|
||||
} catch (const memgraph::query::QueryException &e) {
|
||||
// Wrap QueryException into ClientError, because we want to allow the
|
||||
// client to fix their query.
|
||||
throw memgraph::communication::bolt::ClientError(e.what());
|
||||
} catch (const memgraph::query::ReplicationException &e) {
|
||||
throw memgraph::communication::bolt::ClientError(e.what());
|
||||
}
|
||||
}
|
||||
void SessionHL::RollbackTransaction() { interpreter_->RollbackTransaction(); }
|
||||
void SessionHL::CommitTransaction() { interpreter_->CommitTransaction(); }
|
||||
void SessionHL::BeginTransaction(const std::map<std::string, memgraph::communication::bolt::Value> &extra) {
|
||||
interpreter_->BeginTransaction(ToQueryExtras(extra));
|
||||
}
|
||||
void SessionHL::Configure(const std::map<std::string, memgraph::communication::bolt::Value> &run_time_info) {
|
||||
#ifdef MG_ENTERPRISE
|
||||
std::string db;
|
||||
bool update = false;
|
||||
// Check if user explicitly defined the database to use
|
||||
if (run_time_info.contains("db")) {
|
||||
const auto &db_info = run_time_info.at("db");
|
||||
if (!db_info.IsString()) {
|
||||
throw memgraph::communication::bolt::ClientError("Malformed database name.");
|
||||
}
|
||||
db = db_info.ValueString();
|
||||
update = db != current_.interpreter_context()->db->id();
|
||||
in_explicit_db_ = true;
|
||||
// NOTE: Once in a transaction, the drivers stop explicitly sending the db and count on using it until commit
|
||||
} else if (in_explicit_db_ && !interpreter_->in_explicit_transaction_) { // Just on a switch
|
||||
db = GetDefaultDB();
|
||||
update = db != current_.interpreter_context()->db->id();
|
||||
in_explicit_db_ = false;
|
||||
}
|
||||
|
||||
// Check if the underlying database needs to be updated
|
||||
if (update) {
|
||||
sc_handler_.SetInPlace(db, [this](auto new_sc) mutable {
|
||||
const auto &db_name = new_sc.interpreter_context->db->id();
|
||||
MultiDatabaseAuth(db_name);
|
||||
try {
|
||||
Update(ContextWrapper(new_sc));
|
||||
return memgraph::dbms::SetForResult::SUCCESS;
|
||||
} catch (memgraph::dbms::UnknownDatabaseException &e) {
|
||||
throw memgraph::communication::bolt::ClientError("No database named \"{}\" found!", db_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
SessionHL::~SessionHL() { memgraph::metrics::DecrementCounter(memgraph::metrics::ActiveBoltSessions); }
|
||||
SessionHL::SessionHL(
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::dbms::SessionContextHandler &sc_handler,
|
||||
#else
|
||||
memgraph::dbms::SessionContext sc,
|
||||
#endif
|
||||
const memgraph::communication::v2::ServerEndpoint &endpoint, memgraph::communication::v2::InputStream *input_stream,
|
||||
memgraph::communication::v2::OutputStream *output_stream, const std::string &default_db) // NOLINT
|
||||
: Session<memgraph::communication::v2::InputStream, memgraph::communication::v2::OutputStream>(input_stream,
|
||||
output_stream),
|
||||
#ifdef MG_ENTERPRISE
|
||||
sc_handler_(sc_handler),
|
||||
current_(sc_handler_.Get(default_db)),
|
||||
#else
|
||||
current_(sc),
|
||||
#endif
|
||||
interpreter_context_(current_.interpreter_context()),
|
||||
interpreter_(current_.interp()),
|
||||
auth_(current_.auth()),
|
||||
#ifdef MG_ENTERPRISE
|
||||
audit_log_(current_.audit_log()),
|
||||
#endif
|
||||
endpoint_(endpoint),
|
||||
run_id_(current_.run_id()) {
|
||||
memgraph::metrics::IncrementCounter(memgraph::metrics::ActiveBoltSessions);
|
||||
}
|
||||
|
||||
/// ContextWrapper
|
||||
ContextWrapper::ContextWrapper(memgraph::dbms::SessionContext sc)
|
||||
: session_context(sc),
|
||||
interpreter(std::make_unique<memgraph::query::Interpreter>(session_context.interpreter_context.get())),
|
||||
defunct_(false) {
|
||||
session_context.interpreter_context->interpreters.WithLock(
|
||||
[this](auto &interpreters) { interpreters.insert(interpreter.get()); });
|
||||
}
|
||||
ContextWrapper::~ContextWrapper() { Defunct(); }
|
||||
void ContextWrapper::Defunct() {
|
||||
if (!defunct_) {
|
||||
session_context.interpreter_context->interpreters.WithLock(
|
||||
[this](auto &interpreters) { interpreters.erase(interpreter.get()); });
|
||||
defunct_ = true;
|
||||
}
|
||||
}
|
||||
ContextWrapper::ContextWrapper(ContextWrapper &&in) noexcept
|
||||
: session_context(std::move(in.session_context)), interpreter(std::move(in.interpreter)), defunct_(in.defunct_) {
|
||||
in.defunct_ = true;
|
||||
}
|
||||
ContextWrapper &ContextWrapper::operator=(ContextWrapper &&in) noexcept {
|
||||
if (this != &in) {
|
||||
Defunct();
|
||||
session_context = std::move(in.session_context);
|
||||
interpreter = std::move(in.interpreter);
|
||||
defunct_ = in.defunct_;
|
||||
in.defunct_ = true;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
memgraph::query::InterpreterContext *ContextWrapper::interpreter_context() {
|
||||
return session_context.interpreter_context.get();
|
||||
}
|
||||
memgraph::query::Interpreter *ContextWrapper::interp() { return interpreter.get(); }
|
||||
memgraph::utils::Synchronized<memgraph::auth::Auth, memgraph::utils::WritePrioritizedRWLock> *ContextWrapper::auth()
|
||||
const {
|
||||
return session_context.auth;
|
||||
}
|
||||
std::string ContextWrapper::run_id() const { return session_context.run_id; }
|
||||
bool ContextWrapper::defunct() const { return defunct_; }
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::audit::Log *ContextWrapper::audit_log() const { return session_context.audit_log; }
|
||||
#endif
|
||||
|
||||
std::map<std::string, memgraph::communication::bolt::Value> SessionHL::DecodeSummary(
|
||||
const std::map<std::string, memgraph::query::TypedValue> &summary) {
|
||||
std::map<std::string, memgraph::communication::bolt::Value> decoded_summary;
|
||||
for (const auto &kv : summary) {
|
||||
auto maybe_value = ToBoltValue(kv.second, *interpreter_context_->db, memgraph::storage::View::NEW);
|
||||
if (maybe_value.HasError()) {
|
||||
switch (maybe_value.GetError()) {
|
||||
case memgraph::storage::Error::DELETED_OBJECT:
|
||||
case memgraph::storage::Error::SERIALIZATION_ERROR:
|
||||
case memgraph::storage::Error::VERTEX_HAS_EDGES:
|
||||
case memgraph::storage::Error::PROPERTIES_DISABLED:
|
||||
case memgraph::storage::Error::NONEXISTENT_OBJECT:
|
||||
throw memgraph::communication::bolt::ClientError("Unexpected storage error when streaming summary.");
|
||||
}
|
||||
}
|
||||
decoded_summary.emplace(kv.first, std::move(*maybe_value));
|
||||
}
|
||||
// Add this memgraph instance run_id, received from telemetry
|
||||
// This is sent with every query, instead of only on bolt init inside
|
||||
// communication/bolt/v1/states/init.hpp because neo4jdriver does not
|
||||
// read the init message.
|
||||
if (auto run_id = run_id_; run_id) {
|
||||
decoded_summary.emplace("run_id", *run_id);
|
||||
}
|
||||
|
||||
// Clean up previous session (session gets defunct when switching between databases)
|
||||
if (defunct_) {
|
||||
defunct_.reset();
|
||||
}
|
||||
|
||||
return decoded_summary;
|
||||
}
|
||||
} // namespace memgraph::glue
|
||||
161
src/glue/SessionHL.hpp
Normal file
161
src/glue/SessionHL.hpp
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "communication/v2/server.hpp"
|
||||
#include "communication/v2/session.hpp"
|
||||
#include "dbms/session_context.hpp"
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
#include "dbms/session_context_handler.hpp"
|
||||
#else
|
||||
#include "dbms/session_context.hpp"
|
||||
#endif
|
||||
|
||||
namespace memgraph::glue {
|
||||
|
||||
struct ContextWrapper {
|
||||
explicit ContextWrapper(memgraph::dbms::SessionContext sc);
|
||||
~ContextWrapper();
|
||||
|
||||
ContextWrapper(const ContextWrapper &) = delete;
|
||||
ContextWrapper &operator=(const ContextWrapper &) = delete;
|
||||
|
||||
ContextWrapper(ContextWrapper &&in) noexcept;
|
||||
ContextWrapper &operator=(ContextWrapper &&in) noexcept;
|
||||
|
||||
void Defunct();
|
||||
memgraph::query::InterpreterContext *interpreter_context();
|
||||
memgraph::query::Interpreter *interp();
|
||||
memgraph::utils::Synchronized<memgraph::auth::Auth, memgraph::utils::WritePrioritizedRWLock> *auth() const;
|
||||
std::string run_id() const;
|
||||
bool defunct() const;
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::audit::Log *audit_log() const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
memgraph::dbms::SessionContext session_context;
|
||||
std::unique_ptr<memgraph::query::Interpreter> interpreter;
|
||||
bool defunct_;
|
||||
};
|
||||
|
||||
class SessionHL final : public memgraph::communication::bolt::Session<memgraph::communication::v2::InputStream,
|
||||
memgraph::communication::v2::OutputStream> {
|
||||
public:
|
||||
SessionHL(
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::dbms::SessionContextHandler &sc_handler,
|
||||
#else
|
||||
memgraph::dbms::SessionContext sc,
|
||||
#endif
|
||||
const memgraph::communication::v2::ServerEndpoint &endpoint,
|
||||
memgraph::communication::v2::InputStream *input_stream, memgraph::communication::v2::OutputStream *output_stream,
|
||||
const std::string &default_db = memgraph::dbms::kDefaultDB);
|
||||
|
||||
~SessionHL() override;
|
||||
|
||||
SessionHL(const SessionHL &) = delete;
|
||||
SessionHL &operator=(const SessionHL &) = delete;
|
||||
SessionHL(SessionHL &&) = delete;
|
||||
SessionHL &operator=(SessionHL &&) = delete;
|
||||
|
||||
void Configure(const std::map<std::string, memgraph::communication::bolt::Value> &run_time_info) override;
|
||||
|
||||
using TEncoder = memgraph::communication::bolt::Encoder<
|
||||
memgraph::communication::bolt::ChunkedEncoderBuffer<memgraph::communication::v2::OutputStream>>;
|
||||
|
||||
void BeginTransaction(const std::map<std::string, memgraph::communication::bolt::Value> &extra) override;
|
||||
|
||||
void CommitTransaction() override;
|
||||
|
||||
void RollbackTransaction() override;
|
||||
|
||||
std::pair<std::vector<std::string>, std::optional<int>> Interpret(
|
||||
const std::string &query, const std::map<std::string, memgraph::communication::bolt::Value> ¶ms,
|
||||
const std::map<std::string, memgraph::communication::bolt::Value> &extra) override;
|
||||
|
||||
std::map<std::string, memgraph::communication::bolt::Value> Pull(TEncoder *encoder, std::optional<int> n,
|
||||
std::optional<int> qid) override;
|
||||
|
||||
std::map<std::string, memgraph::communication::bolt::Value> Discard(std::optional<int> n,
|
||||
std::optional<int> qid) override;
|
||||
|
||||
void Abort() override;
|
||||
|
||||
// Called during Init
|
||||
// During Init, the user cannot choose the landing DB (switch is done during query execution)
|
||||
bool Authenticate(const std::string &username, const std::string &password) override;
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::dbms::SetForResult OnChange(const std::string &db_name) override;
|
||||
|
||||
bool OnDelete(const std::string &db_name) override;
|
||||
#endif
|
||||
std::optional<std::string> GetServerNameForInit() override;
|
||||
|
||||
std::string GetDatabaseName() const override;
|
||||
|
||||
private:
|
||||
std::map<std::string, memgraph::communication::bolt::Value> DecodeSummary(
|
||||
const std::map<std::string, memgraph::query::TypedValue> &summary);
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
/**
|
||||
* @brief Update setup to the new database.
|
||||
*
|
||||
* @param db_name name of the target database
|
||||
* @throws UnknownDatabaseException if handler cannot get it
|
||||
*/
|
||||
void UpdateAndDefunct(const std::string &db_name);
|
||||
|
||||
void UpdateAndDefunct(ContextWrapper &&cntxt);
|
||||
|
||||
void Update(const std::string &db_name);
|
||||
|
||||
void Update(ContextWrapper &&cntxt);
|
||||
|
||||
/**
|
||||
* @brief Authenticate user on passed database.
|
||||
*
|
||||
* @param db database to check against
|
||||
* @throws bolt::ClientError when user is not authorized
|
||||
*/
|
||||
void MultiDatabaseAuth(const std::string &db);
|
||||
|
||||
/**
|
||||
* @brief Get the user's default database
|
||||
*
|
||||
* @return std::string
|
||||
*/
|
||||
std::string GetDefaultDB();
|
||||
#endif
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::dbms::SessionContextHandler &sc_handler_;
|
||||
#endif
|
||||
ContextWrapper current_;
|
||||
std::optional<ContextWrapper> defunct_;
|
||||
|
||||
memgraph::query::InterpreterContext *interpreter_context_;
|
||||
memgraph::query::Interpreter *interpreter_;
|
||||
memgraph::utils::Synchronized<memgraph::auth::Auth, memgraph::utils::WritePrioritizedRWLock> *auth_;
|
||||
std::optional<memgraph::auth::User> user_;
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::audit::Log *audit_log_;
|
||||
bool in_explicit_db_{false}; //!< If true, the user has defined the database to use via metadata
|
||||
#endif
|
||||
memgraph::communication::v2::ServerEndpoint endpoint_;
|
||||
// NOTE: run_id should be const but that complicates code a lot.
|
||||
std::optional<std::string> run_id_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::glue
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -63,7 +63,7 @@ inline void LoadConfig(const std::string &product_name) {
|
||||
delete[] custom_argv;
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> LoadUsernameAndPassword(const std::string &pass_file) {
|
||||
inline std::pair<std::string, std::string> LoadUsernameAndPassword(const std::string &pass_file) {
|
||||
std::ifstream file(pass_file);
|
||||
if (file.fail()) {
|
||||
spdlog::warn("Problem with opening MG_PASSFILE, memgraph server will start without user");
|
||||
|
||||
@@ -101,6 +101,7 @@ class MetricsService {
|
||||
auto GetEventCounters() {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-init-variables)
|
||||
std::vector<std::tuple<std::string, std::string, uint64_t>> event_counters{};
|
||||
event_counters.reserve(memgraph::metrics::CounterEnd());
|
||||
|
||||
for (auto i = 0; i < memgraph::metrics::CounterEnd(); i++) {
|
||||
event_counters.emplace_back(memgraph::metrics::GetCounterName(i), memgraph::metrics::GetCounterType(i),
|
||||
@@ -113,6 +114,7 @@ class MetricsService {
|
||||
auto GetEventGauges() {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-init-variables)
|
||||
std::vector<std::tuple<std::string, std::string, uint64_t>> event_gauges{};
|
||||
event_gauges.reserve(memgraph::metrics::GaugeEnd());
|
||||
|
||||
for (auto i = 0; i < memgraph::metrics::GaugeEnd(); i++) {
|
||||
event_gauges.emplace_back(memgraph::metrics::GetGaugeName(i), memgraph::metrics::GetGaugeType(i),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -91,7 +91,6 @@ std::string Endpoint::SocketAddress() const {
|
||||
return ip_address + ":" + std::to_string(port);
|
||||
}
|
||||
|
||||
Endpoint::Endpoint() {}
|
||||
Endpoint::Endpoint(std::string ip_address, uint16_t port) : address(std::move(ip_address)), port(port) {
|
||||
IpFamily ip_family = GetIpFamily(address);
|
||||
if (ip_family == IpFamily::NONE) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -25,8 +25,13 @@ namespace memgraph::io::network {
|
||||
* connection address.
|
||||
*/
|
||||
struct Endpoint {
|
||||
Endpoint();
|
||||
Endpoint() = default;
|
||||
Endpoint(std::string ip_address, uint16_t port);
|
||||
Endpoint(Endpoint const &) = default;
|
||||
Endpoint(Endpoint &&) noexcept = default;
|
||||
Endpoint &operator=(Endpoint const &) = default;
|
||||
Endpoint &operator=(Endpoint &&) noexcept = default;
|
||||
~Endpoint() = default;
|
||||
|
||||
enum class IpFamily : std::uint8_t { NONE, IP4, IP6 };
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ std::optional<std::string> KVStore::Get(const std::string &key) const noexcept {
|
||||
return value;
|
||||
}
|
||||
|
||||
bool KVStore::Delete(const std::string &key) {
|
||||
bool KVStore::Delete(std::string_view key) {
|
||||
auto s = pimpl_->db->Delete(rocksdb::WriteOptions(), key);
|
||||
return s.ok();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -91,7 +91,7 @@ class KVStore final {
|
||||
* true if the key doesn't exist and underlying storage
|
||||
* didn't encounter any error.
|
||||
*/
|
||||
bool Delete(const std::string &key);
|
||||
bool Delete(std::string_view key);
|
||||
|
||||
/**
|
||||
* Deletes the keys and corresponding values from storage.
|
||||
|
||||
887
src/memgraph.cpp
887
src/memgraph.cpp
@@ -9,465 +9,36 @@
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <fmt/format.h>
|
||||
#include <gflags/gflags.h>
|
||||
#include <spdlog/common.h>
|
||||
#include <spdlog/sinks/daily_file_sink.h>
|
||||
#include <spdlog/sinks/dist_sink.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#ifndef MG_ENTERPRISE
|
||||
#include "dbms/session_context_handler.hpp"
|
||||
#endif
|
||||
|
||||
#include "audit/log.hpp"
|
||||
#include "auth/models.hpp"
|
||||
#include "communication/bolt/v1/constants.hpp"
|
||||
#include "communication/http/server.hpp"
|
||||
#include "communication/websocket/auth.hpp"
|
||||
#include "communication/websocket/server.hpp"
|
||||
#include "dbms/constants.hpp"
|
||||
#include "dbms/global.hpp"
|
||||
#include "dbms/session_context.hpp"
|
||||
#include "flags/all.hpp"
|
||||
#include "glue/MonitoringServerT.hpp"
|
||||
#include "glue/ServerT.hpp"
|
||||
#include "glue/auth_checker.hpp"
|
||||
#include "glue/auth_handler.hpp"
|
||||
#include "helpers.hpp"
|
||||
#include "http_handlers/metrics.hpp"
|
||||
#include "license/license.hpp"
|
||||
#include "license/license_sender.hpp"
|
||||
#include "py/py.hpp"
|
||||
#include "query/auth_checker.hpp"
|
||||
#include "query/discard_value_stream.hpp"
|
||||
#include "query/exceptions.hpp"
|
||||
#include "query/frontend/ast/ast.hpp"
|
||||
#include "query/interpreter.hpp"
|
||||
#include "query/plan/operator.hpp"
|
||||
#include "query/procedure/callable_alias_mapper.hpp"
|
||||
#include "query/procedure/module.hpp"
|
||||
#include "query/procedure/py_module.hpp"
|
||||
#include "requests/requests.hpp"
|
||||
#include "storage/v2/config.hpp"
|
||||
#include "storage/v2/disk/storage.hpp"
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
#include "storage/v2/isolation_level.hpp"
|
||||
#include "storage/v2/storage.hpp"
|
||||
#include "storage/v2/view.hpp"
|
||||
#include "telemetry/telemetry.hpp"
|
||||
#include "utils/enum.hpp"
|
||||
#include "utils/event_counter.hpp"
|
||||
#include "utils/file.hpp"
|
||||
#include "utils/flag_validation.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/memory_tracker.hpp"
|
||||
#include "utils/message.hpp"
|
||||
#include "utils/readable_size.hpp"
|
||||
#include "utils/rw_lock.hpp"
|
||||
#include "utils/settings.hpp"
|
||||
#include "utils/signals.hpp"
|
||||
#include "utils/string.hpp"
|
||||
#include "utils/synchronized.hpp"
|
||||
#include "utils/sysinfo/memory.hpp"
|
||||
#include "utils/system_info.hpp"
|
||||
#include "utils/terminate_handler.hpp"
|
||||
#include "version.hpp"
|
||||
|
||||
// Communication libraries must be included after query libraries are included.
|
||||
// This is to enable compilation of the binary when linking with old OpenSSL
|
||||
// libraries (as on CentOS 7).
|
||||
//
|
||||
// The OpenSSL library available on CentOS 7 is v1.0.0, that version includes
|
||||
// `libkrb5` in its public API headers (that we include in our communication
|
||||
// stack). The `libkrb5` library has `#define`s for `TRUE` and `FALSE`. Those
|
||||
// defines clash with Antlr's usage of `TRUE` and `FALSE` as enumeration keys.
|
||||
// Because of that the definitions of `TRUE` and `FALSE` that are inherited
|
||||
// from `libkrb5` must be included after the Antlr includes. Hence,
|
||||
// communication headers must be included after query headers.
|
||||
#include "communication/bolt/v1/exceptions.hpp"
|
||||
#include "communication/bolt/v1/session.hpp"
|
||||
#include "communication/init.hpp"
|
||||
#include "communication/v2/server.hpp"
|
||||
#include "communication/v2/session.hpp"
|
||||
#include "dbms/session_context_handler.hpp"
|
||||
#include "glue/communication.hpp"
|
||||
|
||||
#include "auth/auth.hpp"
|
||||
#include "glue/auth.hpp"
|
||||
|
||||
constexpr const char *kMgUser = "MEMGRAPH_USER";
|
||||
constexpr const char *kMgPassword = "MEMGRAPH_PASSWORD";
|
||||
constexpr const char *kMgPassfile = "MEMGRAPH_PASSFILE";
|
||||
|
||||
// Short help flag.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_bool(h, false, "Print usage and exit.");
|
||||
|
||||
// Bolt server flags.
|
||||
DEFINE_string(bolt_address, "0.0.0.0", "IP address on which the Bolt server should listen.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(monitoring_address, "0.0.0.0",
|
||||
"IP address on which the websocket server for Memgraph monitoring should listen.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(metrics_address, "0.0.0.0",
|
||||
"IP address on which the Memgraph server for exposing metrics should listen.");
|
||||
DEFINE_VALIDATED_int32(bolt_port, 7687, "Port on which the Bolt server should listen.",
|
||||
FLAG_IN_RANGE(0, std::numeric_limits<uint16_t>::max()));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(monitoring_port, 7444,
|
||||
"Port on which the websocket server for Memgraph monitoring should listen.",
|
||||
FLAG_IN_RANGE(0, std::numeric_limits<uint16_t>::max()));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(metrics_port, 9091, "Port on which the Memgraph server for exposing metrics should listen.",
|
||||
FLAG_IN_RANGE(0, std::numeric_limits<uint16_t>::max()));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(bolt_num_workers, std::max(std::thread::hardware_concurrency(), 1U),
|
||||
"Number of workers used by the Bolt server. By default, this will be the "
|
||||
"number of processing units available on the machine.",
|
||||
FLAG_IN_RANGE(1, INT32_MAX));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(bolt_session_inactivity_timeout, 1800,
|
||||
"Time in seconds after which inactive Bolt sessions will be "
|
||||
"closed.",
|
||||
FLAG_IN_RANGE(1, INT32_MAX));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(bolt_cert_file, "", "Certificate file which should be used for the Bolt server.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(bolt_key_file, "", "Key file which should be used for the Bolt server.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(bolt_server_name_for_init, "",
|
||||
"Server name which the database should send to the client in the "
|
||||
"Bolt INIT message.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(init_file, "",
|
||||
"Path to cypherl file that is used for configuring users and database schema before server starts.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(init_data_file, "", "Path to cypherl file that is used for creating data after server starts.");
|
||||
|
||||
// General purpose flags.
|
||||
// NOTE: The `data_directory` flag must be the same here and in
|
||||
// `mg_import_csv`. If you change it, make sure to change it there as well.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(data_directory, "mg_data", "Path to directory in which to save all permanent data.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(data_recovery_on_startup, false, "Controls whether the database recovers persisted data on startup.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(memory_warning_threshold, 1024,
|
||||
"Memory warning threshold, in MB. If Memgraph detects there is "
|
||||
"less available RAM it will log a warning. Set to 0 to "
|
||||
"disable.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(allow_load_csv, true, "Controls whether LOAD CSV clause is allowed in queries.");
|
||||
|
||||
// Storage flags.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_gc_cycle_sec, 30, "Storage garbage collector interval (in seconds).",
|
||||
FLAG_IN_RANGE(1, 24 * 3600));
|
||||
// NOTE: The `storage_properties_on_edges` flag must be the same here and in
|
||||
// `mg_import_csv`. If you change it, make sure to change it there as well.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_properties_on_edges, false, "Controls whether edges have properties.");
|
||||
|
||||
// storage_recover_on_startup deprecated; use data_recovery_on_startup instead
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_bool(storage_recover_on_startup, false,
|
||||
"Controls whether the storage recovers persisted data on startup.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_snapshot_interval_sec, 0,
|
||||
"Storage snapshot creation interval (in seconds). Set "
|
||||
"to 0 to disable periodic snapshot creation.",
|
||||
FLAG_IN_RANGE(0, 7 * 24 * 3600));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_wal_enabled, false,
|
||||
"Controls whether the storage uses write-ahead-logging. To enable "
|
||||
"WAL periodic snapshots must be enabled.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_snapshot_retention_count, 3, "The number of snapshots that should always be kept.",
|
||||
FLAG_IN_RANGE(1, 1000000));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_wal_file_size_kib, memgraph::storage::Config::Durability().wal_file_size_kibibytes,
|
||||
"Minimum file size of each WAL file.",
|
||||
FLAG_IN_RANGE(1, static_cast<unsigned long>(1000) * 1024));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(storage_wal_file_flush_every_n_tx,
|
||||
memgraph::storage::Config::Durability().wal_file_flush_every_n_tx,
|
||||
"Issue a 'fsync' call after this amount of transactions are written to the "
|
||||
"WAL file. Set to 1 for fully synchronous operation.",
|
||||
FLAG_IN_RANGE(1, 1000000));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_snapshot_on_exit, false, "Controls whether the storage creates another snapshot on exit.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(storage_items_per_batch, memgraph::storage::Config::Durability().items_per_batch,
|
||||
"The number of edges and vertices stored in a batch in a snapshot file.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_parallel_index_recovery, false,
|
||||
"Controls whether the index creation can be done in a multithreaded fashion.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(storage_recovery_thread_count,
|
||||
std::max(static_cast<uint64_t>(std::thread::hardware_concurrency()),
|
||||
memgraph::storage::Config::Durability().recovery_thread_count),
|
||||
"The number of threads used to recover persisted data from disk.");
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_delete_on_drop, true,
|
||||
"If set to true the query 'DROP DATABASE x' will delete the underlying storage as well.");
|
||||
#endif
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(telemetry_enabled, false,
|
||||
"Set to true to enable telemetry. We collect information about the "
|
||||
"running system (CPU and memory information) and information about "
|
||||
"the database runtime (vertex and edge counts and resource usage) "
|
||||
"to allow for easier improvement of the product.");
|
||||
|
||||
// Streams flags
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint32(
|
||||
stream_transaction_conflict_retries, 30,
|
||||
"Number of times to retry when a stream transformation fails to commit because of conflicting transactions");
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint32(
|
||||
stream_transaction_retry_interval, 500,
|
||||
"Retry interval in milliseconds when a stream transformation fails to commit because of conflicting transactions");
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(kafka_bootstrap_servers, "",
|
||||
"List of default Kafka brokers as a comma separated list of broker host or host:port.");
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(pulsar_service_url, "", "Default URL used while connecting to Pulsar brokers.");
|
||||
|
||||
// Audit logging flags.
|
||||
#ifdef MG_ENTERPRISE
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(audit_enabled, false, "Set to true to enable audit logging.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(audit_buffer_size, memgraph::audit::kBufferSizeDefault,
|
||||
"Maximum number of items in the audit log buffer.", FLAG_IN_RANGE(1, INT32_MAX));
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_int32(audit_buffer_flush_interval_ms, memgraph::audit::kBufferFlushIntervalMillisDefault,
|
||||
"Interval (in milliseconds) used for flushing the audit log buffer.",
|
||||
FLAG_IN_RANGE(10, INT32_MAX));
|
||||
#endif
|
||||
|
||||
// Query flags.
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_double(query_execution_timeout_sec, 600,
|
||||
"Maximum allowed query execution time. Queries exceeding this "
|
||||
"limit will be aborted. Value of 0 means no limit.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(replication_replica_check_frequency_sec, 1,
|
||||
"The time duration between two replica checks/pings. If < 1, replicas will NOT be checked at all. NOTE: "
|
||||
"The MAIN instance allocates a new thread for each REPLICA.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(replication_restore_state_on_startup, false, "Restore replication state on startup, e.g. recover replica");
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint64(
|
||||
memory_limit, 0,
|
||||
"Total memory limit in MiB. Set to 0 to use the default values which are 100\% of the phyisical memory if the swap "
|
||||
"is enabled and 90\% of the physical memory otherwise.");
|
||||
|
||||
namespace {
|
||||
using namespace std::literals;
|
||||
inline constexpr std::array isolation_level_mappings{
|
||||
std::pair{"SNAPSHOT_ISOLATION"sv, memgraph::storage::IsolationLevel::SNAPSHOT_ISOLATION},
|
||||
std::pair{"READ_COMMITTED"sv, memgraph::storage::IsolationLevel::READ_COMMITTED},
|
||||
std::pair{"READ_UNCOMMITTED"sv, memgraph::storage::IsolationLevel::READ_UNCOMMITTED}};
|
||||
|
||||
const std::string isolation_level_help_string =
|
||||
fmt::format("Default isolation level used for the transactions. Allowed values: {}",
|
||||
memgraph::utils::GetAllowedEnumValuesString(isolation_level_mappings));
|
||||
} // namespace
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_string(isolation_level, "SNAPSHOT_ISOLATION", isolation_level_help_string.c_str(), {
|
||||
if (const auto result = memgraph::utils::IsValidEnumValueString(value, isolation_level_mappings); result.HasError()) {
|
||||
const auto error = result.GetError();
|
||||
switch (error) {
|
||||
case memgraph::utils::ValidationError::EmptyValue: {
|
||||
std::cout << "Isolation level cannot be empty." << std::endl;
|
||||
break;
|
||||
}
|
||||
case memgraph::utils::ValidationError::InvalidValue: {
|
||||
std::cout << "Invalid value for isolation level. Allowed values: "
|
||||
<< memgraph::utils::GetAllowedEnumValuesString(isolation_level_mappings) << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
namespace {
|
||||
memgraph::storage::IsolationLevel ParseIsolationLevel() {
|
||||
const auto isolation_level =
|
||||
memgraph::utils::StringToEnum<memgraph::storage::IsolationLevel>(FLAGS_isolation_level, isolation_level_mappings);
|
||||
MG_ASSERT(isolation_level, "Invalid isolation level");
|
||||
return *isolation_level;
|
||||
}
|
||||
|
||||
int64_t GetMemoryLimit() {
|
||||
if (FLAGS_memory_limit == 0) {
|
||||
auto maybe_total_memory = memgraph::utils::sysinfo::TotalMemory();
|
||||
MG_ASSERT(maybe_total_memory, "Failed to fetch the total physical memory");
|
||||
const auto maybe_swap_memory = memgraph::utils::sysinfo::SwapTotalMemory();
|
||||
MG_ASSERT(maybe_swap_memory, "Failed to fetch the total swap memory");
|
||||
|
||||
if (*maybe_swap_memory == 0) {
|
||||
// take only 90% of the total memory
|
||||
*maybe_total_memory *= 9;
|
||||
*maybe_total_memory /= 10;
|
||||
}
|
||||
return *maybe_total_memory * 1024;
|
||||
}
|
||||
|
||||
// We parse the memory as MiB every time
|
||||
return FLAGS_memory_limit * 1024 * 1024;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
std::vector<std::filesystem::path> query_modules_directories;
|
||||
} // namespace
|
||||
DEFINE_VALIDATED_string(query_modules_directory, "",
|
||||
"Directory where modules with custom query procedures are stored. "
|
||||
"NOTE: Multiple comma-separated directories can be defined.",
|
||||
{
|
||||
query_modules_directories.clear();
|
||||
if (value.empty()) return true;
|
||||
const auto directories = memgraph::utils::Split(value, ",");
|
||||
for (const auto &dir : directories) {
|
||||
if (!memgraph::utils::DirExists(dir)) {
|
||||
std::cout << "Expected --" << flagname << " to point to directories." << std::endl;
|
||||
std::cout << dir << " is not a directory." << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
query_modules_directories.reserve(directories.size());
|
||||
std::transform(directories.begin(), directories.end(),
|
||||
std::back_inserter(query_modules_directories),
|
||||
[](const auto &dir) { return dir; });
|
||||
return true;
|
||||
});
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(query_callable_mappings_path, "",
|
||||
"The path to mappings that describes aliases to callables in cypher queries in the form of key-value "
|
||||
"pairs in a json file. With this option query module procedures that do not exist in memgraph can be "
|
||||
"mapped to ones that exist.");
|
||||
|
||||
// Logging flags
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_bool(also_log_to_stderr, false, "Log messages go to stderr in addition to logfiles");
|
||||
DEFINE_string(log_file, "", "Path to where the log should be stored.");
|
||||
|
||||
namespace {
|
||||
inline constexpr std::array log_level_mappings{
|
||||
std::pair{"TRACE"sv, spdlog::level::trace}, std::pair{"DEBUG"sv, spdlog::level::debug},
|
||||
std::pair{"INFO"sv, spdlog::level::info}, std::pair{"WARNING"sv, spdlog::level::warn},
|
||||
std::pair{"ERROR"sv, spdlog::level::err}, std::pair{"CRITICAL"sv, spdlog::level::critical}};
|
||||
|
||||
const std::string log_level_help_string = fmt::format("Minimum log level. Allowed values: {}",
|
||||
memgraph::utils::GetAllowedEnumValuesString(log_level_mappings));
|
||||
} // namespace
|
||||
|
||||
DEFINE_VALIDATED_string(log_level, "WARNING", log_level_help_string.c_str(), {
|
||||
if (const auto result = memgraph::utils::IsValidEnumValueString(value, log_level_mappings); result.HasError()) {
|
||||
const auto error = result.GetError();
|
||||
switch (error) {
|
||||
case memgraph::utils::ValidationError::EmptyValue: {
|
||||
std::cout << "Log level cannot be empty." << std::endl;
|
||||
break;
|
||||
}
|
||||
case memgraph::utils::ValidationError::InvalidValue: {
|
||||
std::cout << "Invalid value for log level. Allowed values: "
|
||||
<< memgraph::utils::GetAllowedEnumValuesString(log_level_mappings) << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
namespace {
|
||||
spdlog::level::level_enum ParseLogLevel() {
|
||||
const auto log_level = memgraph::utils::StringToEnum<spdlog::level::level_enum>(FLAGS_log_level, log_level_mappings);
|
||||
MG_ASSERT(log_level, "Invalid log level");
|
||||
return *log_level;
|
||||
}
|
||||
|
||||
// 5 weeks * 7 days
|
||||
inline constexpr auto log_retention_count = 35;
|
||||
void CreateLoggerFromSink(const auto &sinks, const auto log_level) {
|
||||
auto logger = std::make_shared<spdlog::logger>("memgraph_log", sinks.begin(), sinks.end());
|
||||
logger->set_level(log_level);
|
||||
logger->flush_on(spdlog::level::trace);
|
||||
spdlog::set_default_logger(std::move(logger));
|
||||
}
|
||||
|
||||
void InitializeLogger() {
|
||||
std::vector<spdlog::sink_ptr> sinks;
|
||||
|
||||
if (FLAGS_also_log_to_stderr) {
|
||||
sinks.emplace_back(std::make_shared<spdlog::sinks::stderr_color_sink_mt>());
|
||||
}
|
||||
|
||||
if (!FLAGS_log_file.empty()) {
|
||||
// get local time
|
||||
time_t current_time{0};
|
||||
struct tm *local_time{nullptr};
|
||||
|
||||
time(¤t_time);
|
||||
local_time = localtime(¤t_time);
|
||||
|
||||
sinks.emplace_back(std::make_shared<spdlog::sinks::daily_file_sink_mt>(
|
||||
FLAGS_log_file, local_time->tm_hour, local_time->tm_min, false, log_retention_count));
|
||||
}
|
||||
CreateLoggerFromSink(sinks, ParseLogLevel());
|
||||
}
|
||||
|
||||
void AddLoggerSink(spdlog::sink_ptr new_sink) {
|
||||
auto default_logger = spdlog::default_logger();
|
||||
auto sinks = default_logger->sinks();
|
||||
sinks.push_back(new_sink);
|
||||
CreateLoggerFromSink(sinks, default_logger->level());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_string(license_key, "", "License key for Memgraph Enterprise.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_string(organization_name, "", "Organization name.");
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(auth_user_or_role_name_regex, memgraph::glue::kDefaultUserRoleRegex.data(),
|
||||
"Set to the regular expression that each user or role name must fulfill.");
|
||||
|
||||
void InitFromCypherlFile(memgraph::query::InterpreterContext &ctx, std::string cypherl_file_path,
|
||||
memgraph::audit::Log *audit_log = nullptr) {
|
||||
memgraph::query::Interpreter interpreter(&ctx);
|
||||
@@ -494,430 +65,6 @@ void InitFromCypherlFile(memgraph::query::InterpreterContext &ctx, std::string c
|
||||
file.close();
|
||||
}
|
||||
|
||||
namespace memgraph::metrics {
|
||||
extern const Event ActiveBoltSessions;
|
||||
} // namespace memgraph::metrics
|
||||
|
||||
auto ToQueryExtras(memgraph::communication::bolt::Value const &extra) -> memgraph::query::QueryExtras {
|
||||
auto const &as_map = extra.ValueMap();
|
||||
|
||||
auto metadata_pv = std::map<std::string, memgraph::storage::PropertyValue>{};
|
||||
|
||||
if (auto const it = as_map.find("tx_metadata"); it != as_map.cend() && it->second.IsMap()) {
|
||||
for (const auto &[key, bolt_md] : it->second.ValueMap()) {
|
||||
metadata_pv.emplace(key, memgraph::glue::ToPropertyValue(bolt_md));
|
||||
}
|
||||
}
|
||||
|
||||
auto tx_timeout = std::optional<int64_t>{};
|
||||
if (auto const it = as_map.find("tx_timeout"); it != as_map.cend() && it->second.IsInt()) {
|
||||
tx_timeout = it->second.ValueInt();
|
||||
}
|
||||
|
||||
return memgraph::query::QueryExtras{std::move(metadata_pv), tx_timeout};
|
||||
}
|
||||
|
||||
class SessionHL final : public memgraph::communication::bolt::Session<memgraph::communication::v2::InputStream,
|
||||
memgraph::communication::v2::OutputStream> {
|
||||
public:
|
||||
struct ContextWrapper {
|
||||
explicit ContextWrapper(memgraph::dbms::SessionContext sc)
|
||||
: session_context(sc),
|
||||
interpreter(std::make_unique<memgraph::query::Interpreter>(session_context.interpreter_context.get())),
|
||||
defunct_(false) {
|
||||
session_context.interpreter_context->interpreters.WithLock(
|
||||
[this](auto &interpreters) { interpreters.insert(interpreter.get()); });
|
||||
}
|
||||
~ContextWrapper() { Defunct(); }
|
||||
|
||||
void Defunct() {
|
||||
if (!defunct_) {
|
||||
session_context.interpreter_context->interpreters.WithLock(
|
||||
[this](auto &interpreters) { interpreters.erase(interpreter.get()); });
|
||||
defunct_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
ContextWrapper(const ContextWrapper &) = delete;
|
||||
ContextWrapper &operator=(const ContextWrapper &) = delete;
|
||||
|
||||
ContextWrapper(ContextWrapper &&in) noexcept
|
||||
: session_context(std::move(in.session_context)),
|
||||
interpreter(std::move(in.interpreter)),
|
||||
defunct_(in.defunct_) {
|
||||
in.defunct_ = true;
|
||||
}
|
||||
|
||||
ContextWrapper &operator=(ContextWrapper &&in) noexcept {
|
||||
if (this != &in) {
|
||||
Defunct();
|
||||
session_context = std::move(in.session_context);
|
||||
interpreter = std::move(in.interpreter);
|
||||
defunct_ = in.defunct_;
|
||||
in.defunct_ = true;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
memgraph::query::InterpreterContext *interpreter_context() { return session_context.interpreter_context.get(); }
|
||||
memgraph::query::Interpreter *interp() { return interpreter.get(); }
|
||||
memgraph::utils::Synchronized<memgraph::auth::Auth, memgraph::utils::WritePrioritizedRWLock> *auth() const {
|
||||
return session_context.auth;
|
||||
}
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::audit::Log *audit_log() const { return session_context.audit_log; }
|
||||
#endif
|
||||
std::string run_id() const { return session_context.run_id; }
|
||||
bool defunct() const { return defunct_; }
|
||||
|
||||
private:
|
||||
memgraph::dbms::SessionContext session_context;
|
||||
std::unique_ptr<memgraph::query::Interpreter> interpreter;
|
||||
bool defunct_;
|
||||
};
|
||||
|
||||
SessionHL(
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::dbms::SessionContextHandler &sc_handler,
|
||||
#else
|
||||
memgraph::dbms::SessionContext sc,
|
||||
#endif
|
||||
const memgraph::communication::v2::ServerEndpoint &endpoint,
|
||||
memgraph::communication::v2::InputStream *input_stream, memgraph::communication::v2::OutputStream *output_stream,
|
||||
const std::string &default_db = memgraph::dbms::kDefaultDB) // NOLINT
|
||||
: memgraph::communication::bolt::Session<memgraph::communication::v2::InputStream,
|
||||
memgraph::communication::v2::OutputStream>(input_stream, output_stream),
|
||||
#ifdef MG_ENTERPRISE
|
||||
sc_handler_(sc_handler),
|
||||
current_(sc_handler_.Get(default_db)),
|
||||
#else
|
||||
current_(sc),
|
||||
#endif
|
||||
interpreter_context_(current_.interpreter_context()),
|
||||
interpreter_(current_.interp()),
|
||||
auth_(current_.auth()),
|
||||
#ifdef MG_ENTERPRISE
|
||||
audit_log_(current_.audit_log()),
|
||||
#endif
|
||||
endpoint_(endpoint),
|
||||
run_id_(current_.run_id()) {
|
||||
memgraph::metrics::IncrementCounter(memgraph::metrics::ActiveBoltSessions);
|
||||
}
|
||||
|
||||
~SessionHL() override { memgraph::metrics::DecrementCounter(memgraph::metrics::ActiveBoltSessions); }
|
||||
|
||||
SessionHL(const SessionHL &) = delete;
|
||||
SessionHL &operator=(const SessionHL &) = delete;
|
||||
SessionHL(SessionHL &&) = delete;
|
||||
SessionHL &operator=(SessionHL &&) = delete;
|
||||
|
||||
void Configure(const std::map<std::string, memgraph::communication::bolt::Value> &run_time_info) override {
|
||||
#ifdef MG_ENTERPRISE
|
||||
std::string db;
|
||||
bool update = false;
|
||||
// Check if user explicitly defined the database to use
|
||||
if (run_time_info.contains("db")) {
|
||||
const auto &db_info = run_time_info.at("db");
|
||||
if (!db_info.IsString()) {
|
||||
throw memgraph::communication::bolt::ClientError("Malformed database name.");
|
||||
}
|
||||
db = db_info.ValueString();
|
||||
update = db != current_.interpreter_context()->db->id();
|
||||
in_explicit_db_ = true;
|
||||
// NOTE: Once in a transaction, the drivers stop explicitly sending the db and count on using it until commit
|
||||
} else if (in_explicit_db_ && !interpreter_->in_explicit_transaction_) { // Just on a switch
|
||||
db = GetDefaultDB();
|
||||
update = db != current_.interpreter_context()->db->id();
|
||||
in_explicit_db_ = false;
|
||||
}
|
||||
|
||||
// Check if the underlying database needs to be updated
|
||||
if (update) {
|
||||
sc_handler_.SetInPlace(db, [this](auto new_sc) mutable {
|
||||
const auto &db_name = new_sc.interpreter_context->db->id();
|
||||
MultiDatabaseAuth(db_name);
|
||||
try {
|
||||
Update(ContextWrapper(new_sc));
|
||||
return memgraph::dbms::SetForResult::SUCCESS;
|
||||
} catch (memgraph::dbms::UnknownDatabaseException &e) {
|
||||
throw memgraph::communication::bolt::ClientError("No database named \"{}\" found!", db_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
using TEncoder = memgraph::communication::bolt::Encoder<
|
||||
memgraph::communication::bolt::ChunkedEncoderBuffer<memgraph::communication::v2::OutputStream>>;
|
||||
|
||||
void BeginTransaction(const std::map<std::string, memgraph::communication::bolt::Value> &extra) override {
|
||||
interpreter_->BeginTransaction(ToQueryExtras(extra));
|
||||
}
|
||||
|
||||
void CommitTransaction() override { interpreter_->CommitTransaction(); }
|
||||
|
||||
void RollbackTransaction() override { interpreter_->RollbackTransaction(); }
|
||||
|
||||
std::pair<std::vector<std::string>, std::optional<int>> Interpret(
|
||||
const std::string &query, const std::map<std::string, memgraph::communication::bolt::Value> ¶ms,
|
||||
const std::map<std::string, memgraph::communication::bolt::Value> &extra) override {
|
||||
std::map<std::string, memgraph::storage::PropertyValue> params_pv;
|
||||
for (const auto &[key, bolt_param] : params) {
|
||||
params_pv.emplace(key, memgraph::glue::ToPropertyValue(bolt_param));
|
||||
}
|
||||
const std::string *username{nullptr};
|
||||
if (user_) {
|
||||
username = &user_->username();
|
||||
}
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
if (memgraph::license::global_license_checker.IsEnterpriseValidFast()) {
|
||||
audit_log_->Record(endpoint_.address().to_string(), user_ ? *username : "", query,
|
||||
memgraph::storage::PropertyValue(params_pv), interpreter_context_->db->id());
|
||||
}
|
||||
#endif
|
||||
try {
|
||||
auto result = interpreter_->Prepare(query, params_pv, username, ToQueryExtras(extra), UUID());
|
||||
const std::string db_name = result.db ? *result.db : "";
|
||||
if (user_ && !memgraph::glue::AuthChecker::IsUserAuthorized(*user_, result.privileges, db_name)) {
|
||||
interpreter_->Abort();
|
||||
if (db_name.empty()) {
|
||||
throw memgraph::communication::bolt::ClientError(
|
||||
"You are not authorized to execute this query! Please contact your database administrator.");
|
||||
}
|
||||
throw memgraph::communication::bolt::ClientError(
|
||||
"You are not authorized to execute this query on database \"{}\"! Please contact your database "
|
||||
"administrator.",
|
||||
db_name);
|
||||
}
|
||||
return {result.headers, result.qid};
|
||||
|
||||
} catch (const memgraph::query::QueryException &e) {
|
||||
// Wrap QueryException into ClientError, because we want to allow the
|
||||
// client to fix their query.
|
||||
throw memgraph::communication::bolt::ClientError(e.what());
|
||||
} catch (const memgraph::query::ReplicationException &e) {
|
||||
throw memgraph::communication::bolt::ClientError(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, memgraph::communication::bolt::Value> Pull(TEncoder *encoder, std::optional<int> n,
|
||||
std::optional<int> qid) override {
|
||||
TypedValueResultStream stream(encoder, interpreter_context_);
|
||||
return PullResults(stream, n, qid);
|
||||
}
|
||||
|
||||
std::map<std::string, memgraph::communication::bolt::Value> Discard(std::optional<int> n,
|
||||
std::optional<int> qid) override {
|
||||
memgraph::query::DiscardValueResultStream stream;
|
||||
return PullResults(stream, n, qid);
|
||||
}
|
||||
|
||||
void Abort() override { interpreter_->Abort(); }
|
||||
|
||||
// Called during Init
|
||||
// During Init, the user cannot choose the landing DB (switch is done during query execution)
|
||||
bool Authenticate(const std::string &username, const std::string &password) override {
|
||||
auto locked_auth = auth_->Lock();
|
||||
if (!locked_auth->HasUsers()) {
|
||||
return true;
|
||||
}
|
||||
user_ = locked_auth->Authenticate(username, password);
|
||||
#ifdef MG_ENTERPRISE
|
||||
if (user_.has_value()) {
|
||||
const auto &db = user_->db_access().GetDefault();
|
||||
// Check if the underlying database needs to be updated
|
||||
if (db != current_.interpreter_context()->db->id()) {
|
||||
const auto &res = sc_handler_.SetFor(UUID(), db);
|
||||
return res == memgraph::dbms::SetForResult::SUCCESS || res == memgraph::dbms::SetForResult::ALREADY_SET;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return user_.has_value();
|
||||
}
|
||||
|
||||
std::optional<std::string> GetServerNameForInit() override {
|
||||
if (FLAGS_bolt_server_name_for_init.empty()) return std::nullopt;
|
||||
return FLAGS_bolt_server_name_for_init;
|
||||
}
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::dbms::SetForResult OnChange(const std::string &db_name) override {
|
||||
MultiDatabaseAuth(db_name);
|
||||
if (db_name != current_.interpreter_context()->db->id()) {
|
||||
UpdateAndDefunct(db_name); // Done during Pull, so we cannot just replace the current db
|
||||
return memgraph::dbms::SetForResult::SUCCESS;
|
||||
}
|
||||
return memgraph::dbms::SetForResult::ALREADY_SET;
|
||||
}
|
||||
|
||||
bool OnDelete(const std::string &db_name) override {
|
||||
MG_ASSERT(current_.interpreter_context()->db->id() != db_name && (!defunct_ || defunct_->defunct()),
|
||||
"Trying to delete a database while still in use.");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string GetDatabaseName() const override { return interpreter_context_->db->id(); }
|
||||
|
||||
private:
|
||||
template <typename TStream>
|
||||
std::map<std::string, memgraph::communication::bolt::Value> PullResults(TStream &stream, std::optional<int> n,
|
||||
std::optional<int> qid) {
|
||||
try {
|
||||
const auto &summary = interpreter_->Pull(&stream, n, qid);
|
||||
std::map<std::string, memgraph::communication::bolt::Value> decoded_summary;
|
||||
for (const auto &kv : summary) {
|
||||
auto maybe_value =
|
||||
memgraph::glue::ToBoltValue(kv.second, *interpreter_context_->db, memgraph::storage::View::NEW);
|
||||
if (maybe_value.HasError()) {
|
||||
switch (maybe_value.GetError()) {
|
||||
case memgraph::storage::Error::DELETED_OBJECT:
|
||||
case memgraph::storage::Error::SERIALIZATION_ERROR:
|
||||
case memgraph::storage::Error::VERTEX_HAS_EDGES:
|
||||
case memgraph::storage::Error::PROPERTIES_DISABLED:
|
||||
case memgraph::storage::Error::NONEXISTENT_OBJECT:
|
||||
throw memgraph::communication::bolt::ClientError("Unexpected storage error when streaming summary.");
|
||||
}
|
||||
}
|
||||
decoded_summary.emplace(kv.first, std::move(*maybe_value));
|
||||
}
|
||||
// Add this memgraph instance run_id, received from telemetry
|
||||
// This is sent with every query, instead of only on bolt init inside
|
||||
// communication/bolt/v1/states/init.hpp because neo4jdriver does not
|
||||
// read the init message.
|
||||
if (auto run_id = run_id_; run_id) {
|
||||
decoded_summary.emplace("run_id", *run_id);
|
||||
}
|
||||
|
||||
// Clean up previous session (session gets defunct when switching between databases)
|
||||
if (defunct_) {
|
||||
defunct_.reset();
|
||||
}
|
||||
|
||||
return decoded_summary;
|
||||
} catch (const memgraph::query::QueryException &e) {
|
||||
// Wrap QueryException into ClientError, because we want to allow the
|
||||
// client to fix their query.
|
||||
throw memgraph::communication::bolt::ClientError(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
/**
|
||||
* @brief Update setup to the new database.
|
||||
*
|
||||
* @param db_name name of the target database
|
||||
* @throws UnknownDatabaseException if handler cannot get it
|
||||
*/
|
||||
void UpdateAndDefunct(const std::string &db_name) { UpdateAndDefunct(ContextWrapper(sc_handler_.Get(db_name))); }
|
||||
|
||||
void UpdateAndDefunct(ContextWrapper &&cntxt) {
|
||||
defunct_.emplace(std::move(current_));
|
||||
Update(std::forward<ContextWrapper>(cntxt));
|
||||
defunct_->Defunct();
|
||||
}
|
||||
|
||||
void Update(const std::string &db_name) {
|
||||
ContextWrapper tmp(sc_handler_.Get(db_name));
|
||||
Update(std::move(tmp));
|
||||
}
|
||||
|
||||
void Update(ContextWrapper &&cntxt) {
|
||||
current_ = std::move(cntxt);
|
||||
interpreter_ = current_.interp();
|
||||
interpreter_->in_explicit_db_ = in_explicit_db_;
|
||||
interpreter_context_ = current_.interpreter_context();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Authenticate user on passed database.
|
||||
*
|
||||
* @param db database to check against
|
||||
* @throws bolt::ClientError when user is not authorized
|
||||
*/
|
||||
void MultiDatabaseAuth(const std::string &db) {
|
||||
if (user_ && !memgraph::glue::AuthChecker::IsUserAuthorized(*user_, {}, db)) {
|
||||
throw memgraph::communication::bolt::ClientError(
|
||||
"You are not authorized on the database \"{}\"! Please contact your database administrator.", db);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the user's default database
|
||||
*
|
||||
* @return std::string
|
||||
*/
|
||||
std::string GetDefaultDB() {
|
||||
if (user_.has_value()) {
|
||||
return user_->db_access().GetDefault();
|
||||
}
|
||||
return memgraph::dbms::kDefaultDB;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Wrapper around TEncoder which converts TypedValue to Value
|
||||
/// before forwarding the calls to original TEncoder.
|
||||
class TypedValueResultStream {
|
||||
public:
|
||||
TypedValueResultStream(TEncoder *encoder, memgraph::query::InterpreterContext *ic)
|
||||
: encoder_(encoder), interpreter_context_(ic) {}
|
||||
|
||||
void Result(const std::vector<memgraph::query::TypedValue> &values) {
|
||||
std::vector<memgraph::communication::bolt::Value> decoded_values;
|
||||
decoded_values.reserve(values.size());
|
||||
for (const auto &v : values) {
|
||||
auto maybe_value = memgraph::glue::ToBoltValue(v, *interpreter_context_->db, memgraph::storage::View::NEW);
|
||||
if (maybe_value.HasError()) {
|
||||
switch (maybe_value.GetError()) {
|
||||
case memgraph::storage::Error::DELETED_OBJECT:
|
||||
throw memgraph::communication::bolt::ClientError("Returning a deleted object as a result.");
|
||||
case memgraph::storage::Error::NONEXISTENT_OBJECT:
|
||||
throw memgraph::communication::bolt::ClientError("Returning a nonexistent object as a result.");
|
||||
case memgraph::storage::Error::VERTEX_HAS_EDGES:
|
||||
case memgraph::storage::Error::SERIALIZATION_ERROR:
|
||||
case memgraph::storage::Error::PROPERTIES_DISABLED:
|
||||
throw memgraph::communication::bolt::ClientError("Unexpected storage error when streaming results.");
|
||||
}
|
||||
}
|
||||
decoded_values.emplace_back(std::move(*maybe_value));
|
||||
}
|
||||
encoder_->MessageRecord(decoded_values);
|
||||
}
|
||||
|
||||
private:
|
||||
TEncoder *encoder_;
|
||||
// NOTE: Needed only for ToBoltValue conversions
|
||||
memgraph::query::InterpreterContext *interpreter_context_;
|
||||
};
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::dbms::SessionContextHandler &sc_handler_;
|
||||
#endif
|
||||
ContextWrapper current_;
|
||||
std::optional<ContextWrapper> defunct_;
|
||||
|
||||
memgraph::query::InterpreterContext *interpreter_context_;
|
||||
memgraph::query::Interpreter *interpreter_;
|
||||
memgraph::utils::Synchronized<memgraph::auth::Auth, memgraph::utils::WritePrioritizedRWLock> *auth_;
|
||||
std::optional<memgraph::auth::User> user_;
|
||||
#ifdef MG_ENTERPRISE
|
||||
memgraph::audit::Log *audit_log_;
|
||||
bool in_explicit_db_{false}; //!< If true, the user has defined the database to use via metadata
|
||||
#endif
|
||||
memgraph::communication::v2::ServerEndpoint endpoint_;
|
||||
// NOTE: run_id should be const but that complicates code a lot.
|
||||
std::optional<std::string> run_id_;
|
||||
};
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
using ServerT = memgraph::communication::v2::Server<SessionHL, memgraph::dbms::SessionContextHandler>;
|
||||
#else
|
||||
using ServerT = memgraph::communication::v2::Server<SessionHL, memgraph::dbms::SessionContext>;
|
||||
#endif
|
||||
using MonitoringServerT =
|
||||
memgraph::communication::http::Server<memgraph::http::MetricsRequestHandler<memgraph::dbms::SessionContext>,
|
||||
memgraph::dbms::SessionContext>;
|
||||
using memgraph::communication::ServerContext;
|
||||
|
||||
// Needed to correctly handle memgraph destruction from a signal handler.
|
||||
@@ -925,6 +72,7 @@ using memgraph::communication::ServerContext;
|
||||
// when we are exiting main, inside destructors of database::GraphDb and
|
||||
// similar. The signal handler may then initiate another shutdown on memgraph
|
||||
// which is in half destructed state, causing invalid memory access and crash.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
volatile sig_atomic_t is_shutting_down = 0;
|
||||
|
||||
void InitSignalHandlers(const std::function<void()> &shutdown_fun) {
|
||||
@@ -965,7 +113,7 @@ int main(int argc, char **argv) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
InitializeLogger();
|
||||
memgraph::flags::InitializeLogger();
|
||||
|
||||
// Unhandled exception handler init.
|
||||
std::set_terminate(&memgraph::utils::TerminateHandler);
|
||||
@@ -1049,7 +197,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
auto data_directory = std::filesystem::path(FLAGS_data_directory);
|
||||
|
||||
const auto memory_limit = GetMemoryLimit();
|
||||
const auto memory_limit = memgraph::flags::GetMemoryLimit();
|
||||
// NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
|
||||
spdlog::info("Memory limit in config is set to {}", memgraph::utils::GetReadableSize(memory_limit));
|
||||
memgraph::utils::total_memory_tracker.SetMaximumHardLimit(memory_limit);
|
||||
@@ -1112,7 +260,7 @@ int main(int argc, char **argv) {
|
||||
.items_per_batch = FLAGS_storage_items_per_batch,
|
||||
.recovery_thread_count = FLAGS_storage_recovery_thread_count,
|
||||
.allow_parallel_index_creation = FLAGS_storage_parallel_index_recovery},
|
||||
.transaction = {.isolation_level = ParseIsolationLevel()},
|
||||
.transaction = {.isolation_level = memgraph::flags::ParseIsolationLevel()},
|
||||
.disk = {.main_storage_directory = FLAGS_data_directory + "/rocksdb_main_storage",
|
||||
.label_index_directory = FLAGS_data_directory + "/rocksdb_label_index",
|
||||
.label_property_index_directory = FLAGS_data_directory + "/rocksdb_label_property_index",
|
||||
@@ -1192,7 +340,8 @@ int main(int argc, char **argv) {
|
||||
auto *auth = session_context.auth;
|
||||
auto &interpreter_context = *session_context.interpreter_context; // TODO remove
|
||||
|
||||
memgraph::query::procedure::gModuleRegistry.SetModulesDirectory(query_modules_directories, FLAGS_data_directory);
|
||||
memgraph::query::procedure::gModuleRegistry.SetModulesDirectory(memgraph::flags::ParseQueryModulesDirectory(),
|
||||
FLAGS_data_directory);
|
||||
memgraph::query::procedure::gModuleRegistry.UnloadAndLoadModulesFromDirectories();
|
||||
memgraph::query::procedure::gCallableAliasMapper.LoadMapping(FLAGS_query_callable_mappings_path);
|
||||
|
||||
@@ -1239,11 +388,11 @@ int main(int argc, char **argv) {
|
||||
auto server_endpoint = memgraph::communication::v2::ServerEndpoint{
|
||||
boost::asio::ip::address::from_string(FLAGS_bolt_address), static_cast<uint16_t>(FLAGS_bolt_port)};
|
||||
#ifdef MG_ENTERPRISE
|
||||
ServerT server(server_endpoint, &sc_handler, &context, FLAGS_bolt_session_inactivity_timeout, service_name,
|
||||
FLAGS_bolt_num_workers);
|
||||
memgraph::glue::ServerT server(server_endpoint, &sc_handler, &context, FLAGS_bolt_session_inactivity_timeout,
|
||||
service_name, FLAGS_bolt_num_workers);
|
||||
#else
|
||||
ServerT server(server_endpoint, &session_context, &context, FLAGS_bolt_session_inactivity_timeout, service_name,
|
||||
FLAGS_bolt_num_workers);
|
||||
memgraph::glue::ServerT server(server_endpoint, &session_context, &context, FLAGS_bolt_session_inactivity_timeout,
|
||||
service_name, FLAGS_bolt_num_workers);
|
||||
#endif
|
||||
|
||||
const auto machine_id = memgraph::utils::GetMachineId();
|
||||
@@ -1283,9 +432,9 @@ int main(int argc, char **argv) {
|
||||
memgraph::communication::websocket::SafeAuth websocket_auth{auth};
|
||||
memgraph::communication::websocket::Server websocket_server{
|
||||
{FLAGS_monitoring_address, static_cast<uint16_t>(FLAGS_monitoring_port)}, &context, websocket_auth};
|
||||
AddLoggerSink(websocket_server.GetLoggingSink());
|
||||
memgraph::flags::AddLoggerSink(websocket_server.GetLoggingSink());
|
||||
|
||||
MonitoringServerT metrics_server{
|
||||
memgraph::glue::MonitoringServerT metrics_server{
|
||||
{FLAGS_metrics_address, static_cast<uint16_t>(FLAGS_metrics_port)}, &session_context, &context};
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#include "helpers.hpp"
|
||||
#include "storage/v2/config.hpp"
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
@@ -700,13 +701,14 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
std::unordered_map<NodeId, memgraph::storage::Gid> node_id_map;
|
||||
std::unique_ptr<memgraph::storage::Storage> store{new memgraph::storage::InMemoryStorage{{
|
||||
auto store = std::make_unique<memgraph::storage::InMemoryStorage>(memgraph::storage::Config{
|
||||
|
||||
.items = {.properties_on_edges = FLAGS_storage_properties_on_edges},
|
||||
.durability = {.storage_directory = FLAGS_data_directory,
|
||||
.recover_on_startup = false,
|
||||
.snapshot_wal_mode = memgraph::storage::Config::Durability::SnapshotWalMode::DISABLED,
|
||||
.snapshot_on_exit = true},
|
||||
}}};
|
||||
});
|
||||
|
||||
memgraph::utils::Timer load_timer;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "query/typed_value.hpp"
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/result.hpp"
|
||||
#include "storage/v2/view.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
|
||||
@@ -75,6 +76,20 @@ inline void ExpectType(const Symbol &symbol, const TypedValue &value, TypedValue
|
||||
throw QueryRuntimeException("Expected a {} for '{}', but got {}.", expected, symbol.name(), value.type());
|
||||
}
|
||||
|
||||
inline void ProcessError(const storage::Error error) {
|
||||
switch (error) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set properties on a deleted object.");
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
case storage::Error::NONEXISTENT_OBJECT:
|
||||
throw QueryRuntimeException("Unexpected error when setting a property.");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
concept AccessorWithSetProperty = requires(T accessor, const storage::PropertyId key,
|
||||
const storage::PropertyValue new_value) {
|
||||
@@ -89,17 +104,7 @@ storage::PropertyValue PropsSetChecked(T *record, const storage::PropertyId &key
|
||||
try {
|
||||
auto maybe_old_value = record->SetProperty(key, storage::PropertyValue(value));
|
||||
if (maybe_old_value.HasError()) {
|
||||
switch (maybe_old_value.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set properties on a deleted object.");
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
case storage::Error::NONEXISTENT_OBJECT:
|
||||
throw QueryRuntimeException("Unexpected error when setting a property.");
|
||||
}
|
||||
ProcessError(maybe_old_value.GetError());
|
||||
}
|
||||
return std::move(*maybe_old_value);
|
||||
} catch (const TypedValueException &) {
|
||||
@@ -121,17 +126,7 @@ bool MultiPropsInitChecked(T *record, std::map<storage::PropertyId, storage::Pro
|
||||
try {
|
||||
auto maybe_values = record->InitProperties(properties);
|
||||
if (maybe_values.HasError()) {
|
||||
switch (maybe_values.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set properties on a deleted object.");
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
case storage::Error::NONEXISTENT_OBJECT:
|
||||
throw QueryRuntimeException("Unexpected error when setting a property.");
|
||||
}
|
||||
ProcessError(maybe_values.GetError());
|
||||
}
|
||||
return std::move(*maybe_values);
|
||||
} catch (const TypedValueException &) {
|
||||
@@ -139,5 +134,31 @@ bool MultiPropsInitChecked(T *record, std::map<storage::PropertyId, storage::Pro
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
concept AccessorWithUpdateProperties = requires(T accessor,
|
||||
std::map<storage::PropertyId, storage::PropertyValue> &properties) {
|
||||
{
|
||||
accessor.UpdateProperties(properties)
|
||||
} -> std::same_as<
|
||||
storage::Result<std::vector<std::tuple<storage::PropertyId, storage::PropertyValue, storage::PropertyValue>>>>;
|
||||
};
|
||||
|
||||
/// Set property `values` mapped with given `key` on a `record`.
|
||||
///
|
||||
/// @throw QueryRuntimeException if value cannot be set as a property value
|
||||
template <AccessorWithUpdateProperties T>
|
||||
auto UpdatePropertiesChecked(T *record, std::map<storage::PropertyId, storage::PropertyValue> &properties) ->
|
||||
typename std::remove_reference<decltype(record->UpdateProperties(properties).GetValue())>::type {
|
||||
try {
|
||||
auto maybe_values = record->UpdateProperties(properties);
|
||||
if (maybe_values.HasError()) {
|
||||
ProcessError(maybe_values.GetError());
|
||||
}
|
||||
return std::move(*maybe_values);
|
||||
} catch (const TypedValueException &) {
|
||||
throw QueryRuntimeException("Cannot update properties.");
|
||||
}
|
||||
}
|
||||
|
||||
int64_t QueryTimestamp();
|
||||
} // namespace memgraph::query
|
||||
|
||||
@@ -77,6 +77,11 @@ class EdgeAccessor final {
|
||||
return impl_.InitProperties(properties);
|
||||
}
|
||||
|
||||
storage::Result<std::vector<std::tuple<storage::PropertyId, storage::PropertyValue, storage::PropertyValue>>>
|
||||
UpdateProperties(std::map<storage::PropertyId, storage::PropertyValue> &properties) const {
|
||||
return impl_.UpdateProperties(properties);
|
||||
}
|
||||
|
||||
storage::Result<storage::PropertyValue> RemoveProperty(storage::PropertyId key) {
|
||||
return SetProperty(key, storage::PropertyValue());
|
||||
}
|
||||
@@ -135,6 +140,11 @@ class VertexAccessor final {
|
||||
return impl_.InitProperties(properties);
|
||||
}
|
||||
|
||||
storage::Result<std::vector<std::tuple<storage::PropertyId, storage::PropertyValue, storage::PropertyValue>>>
|
||||
UpdateProperties(std::map<storage::PropertyId, storage::PropertyValue> &properties) const {
|
||||
return impl_.UpdateProperties(properties);
|
||||
}
|
||||
|
||||
storage::Result<storage::PropertyValue> RemoveProperty(storage::PropertyId key) {
|
||||
return SetProperty(key, storage::PropertyValue());
|
||||
}
|
||||
|
||||
@@ -221,8 +221,7 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler {
|
||||
if (!mem_storage->SetMainReplicationRole()) {
|
||||
throw QueryRuntimeException("Couldn't set role to main!");
|
||||
}
|
||||
}
|
||||
if (replication_role == ReplicationQuery::ReplicationRole::REPLICA) {
|
||||
} else {
|
||||
if (!port || *port < 0 || *port > std::numeric_limits<uint16_t>::max()) {
|
||||
throw QueryRuntimeException("Port number invalid!");
|
||||
}
|
||||
@@ -1382,7 +1381,7 @@ InterpreterContext::InterpreterContext(const storage::Config storage_config, con
|
||||
}
|
||||
}
|
||||
|
||||
InterpreterContext::InterpreterContext(std::unique_ptr<storage::Storage> db, InterpreterConfig interpreter_config,
|
||||
InterpreterContext::InterpreterContext(std::unique_ptr<storage::Storage> &&db, InterpreterConfig interpreter_config,
|
||||
const std::filesystem::path &data_directory, query::AuthQueryHandler *ah,
|
||||
query::AuthChecker *ac)
|
||||
: db(std::move(db)),
|
||||
@@ -3486,9 +3485,7 @@ PreparedQuery PrepareShowDatabasesQuery(ParsedQuery parsed_query, InterpreterCon
|
||||
}
|
||||
|
||||
std::optional<uint64_t> Interpreter::GetTransactionId() const {
|
||||
if (db_accessor_) {
|
||||
return db_accessor_->GetTransactionId();
|
||||
}
|
||||
if (db_accessor_) return db_accessor_->GetTransactionId();
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ struct InterpreterContext {
|
||||
const std::filesystem::path &data_directory, query::AuthQueryHandler *ah = nullptr,
|
||||
query::AuthChecker *ac = nullptr);
|
||||
|
||||
InterpreterContext(std::unique_ptr<storage::Storage> db, InterpreterConfig interpreter_config,
|
||||
InterpreterContext(std::unique_ptr<storage::Storage> &&db, InterpreterConfig interpreter_config,
|
||||
const std::filesystem::path &data_directory, query::AuthQueryHandler *ah = nullptr,
|
||||
query::AuthChecker *ac = nullptr);
|
||||
|
||||
|
||||
@@ -2711,9 +2711,11 @@ namespace {
|
||||
|
||||
template <typename T>
|
||||
concept AccessorWithProperties = requires(T value, storage::PropertyId property_id,
|
||||
storage::PropertyValue property_value) {
|
||||
storage::PropertyValue property_value,
|
||||
std::map<storage::PropertyId, storage::PropertyValue> properties) {
|
||||
{ value.ClearProperties() } -> std::same_as<storage::Result<std::map<storage::PropertyId, storage::PropertyValue>>>;
|
||||
{value.SetProperty(property_id, property_value)};
|
||||
{value.UpdateProperties(properties)};
|
||||
};
|
||||
|
||||
/// Helper function that sets the given values on either a Vertex or an Edge.
|
||||
@@ -2723,7 +2725,8 @@ concept AccessorWithProperties = requires(T value, storage::PropertyId property_
|
||||
template <AccessorWithProperties TRecordAccessor>
|
||||
void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetProperties::Op op,
|
||||
ExecutionContext *context) {
|
||||
std::optional<std::map<storage::PropertyId, storage::PropertyValue>> old_values;
|
||||
using PropertiesMap = std::map<storage::PropertyId, storage::PropertyValue>;
|
||||
std::optional<PropertiesMap> old_values;
|
||||
const bool should_register_change =
|
||||
context->trigger_context_collector &&
|
||||
context->trigger_context_collector->ShouldRegisterObjectPropertyChange<TRecordAccessor>();
|
||||
@@ -2782,44 +2785,34 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr
|
||||
*record, key, TypedValue(std::move(old_value)), TypedValue(std::forward<decltype(new_value)>(new_value)));
|
||||
};
|
||||
|
||||
auto set_props = [&, record](auto properties) {
|
||||
for (auto &kv : properties) {
|
||||
auto maybe_error = record->SetProperty(kv.first, kv.second);
|
||||
if (maybe_error.HasError()) {
|
||||
switch (maybe_error.GetError()) {
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set properties on a deleted graph element.");
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
case storage::Error::NONEXISTENT_OBJECT:
|
||||
throw QueryRuntimeException("Unexpected error when setting properties.");
|
||||
}
|
||||
}
|
||||
auto update_props = [&, record](PropertiesMap &new_properties) {
|
||||
auto updated_properties = UpdatePropertiesChecked(record, new_properties);
|
||||
|
||||
if (should_register_change) {
|
||||
register_set_property(std::move(*maybe_error), kv.first, std::move(kv.second));
|
||||
if (should_register_change) {
|
||||
for (const auto &[id, old_value, new_value] : updated_properties) {
|
||||
register_set_property(std::move(old_value), id, std::move(new_value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch (rhs.type()) {
|
||||
case TypedValue::Type::Edge:
|
||||
set_props(get_props(rhs.ValueEdge()));
|
||||
case TypedValue::Type::Edge: {
|
||||
PropertiesMap new_properties = get_props(rhs.ValueEdge());
|
||||
update_props(new_properties);
|
||||
break;
|
||||
case TypedValue::Type::Vertex:
|
||||
set_props(get_props(rhs.ValueVertex()));
|
||||
}
|
||||
case TypedValue::Type::Vertex: {
|
||||
PropertiesMap new_properties = get_props(rhs.ValueVertex());
|
||||
update_props(new_properties);
|
||||
break;
|
||||
}
|
||||
case TypedValue::Type::Map: {
|
||||
for (const auto &kv : rhs.ValueMap()) {
|
||||
auto key = context->db_accessor->NameToProperty(kv.first);
|
||||
auto old_value = PropsSetChecked(record, key, kv.second);
|
||||
if (should_register_change) {
|
||||
register_set_property(std::move(old_value), key, kv.second);
|
||||
}
|
||||
PropertiesMap new_properties;
|
||||
for (const auto &[prop_id, prop_value] : rhs.ValueMap()) {
|
||||
auto key = context->db_accessor->NameToProperty(prop_id);
|
||||
new_properties.emplace(key, prop_value);
|
||||
}
|
||||
update_props(new_properties);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -2860,7 +2853,6 @@ bool SetProperties::SetPropertiesCursor::Pull(Frame &frame, ExecutionContext &co
|
||||
throw QueryRuntimeException("Vertex properties not set due to not having enough permission!");
|
||||
}
|
||||
#endif
|
||||
|
||||
SetPropertiesOnRecord(&lhs.ValueVertex(), rhs, self_.op_, &context);
|
||||
break;
|
||||
case TypedValue::Type::Edge:
|
||||
|
||||
@@ -1268,7 +1268,7 @@ void ModuleRegistry::UnloadAndLoadModulesFromDirectories() {
|
||||
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;
|
||||
if (found_it == modules_.end()) return ModulePtr{nullptr};
|
||||
return ModulePtr(found_it->second.get(), std::move(guard));
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class ModulePtr final {
|
||||
|
||||
public:
|
||||
ModulePtr() = default;
|
||||
ModulePtr(std::nullptr_t) {}
|
||||
explicit ModulePtr(std::nullptr_t) {}
|
||||
ModulePtr(const Module *module, std::shared_lock<utils::RWLock> lock) : module_(module), lock_(std::move(lock)) {}
|
||||
|
||||
explicit operator bool() const { return static_cast<bool>(module_); }
|
||||
@@ -176,6 +176,7 @@ class ModuleRegistry final {
|
||||
};
|
||||
|
||||
/// Single, global module registry.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern ModuleRegistry gModuleRegistry;
|
||||
|
||||
/// Return the ModulePtr and `mgp_proc *` of the found procedure after resolving
|
||||
@@ -231,7 +232,7 @@ void ConstructArguments(const std::vector<TypedValue> &args, const TCall &callab
|
||||
for (size_t i = 0; i < n_args; ++i) {
|
||||
auto arg = args[i];
|
||||
std::string_view name;
|
||||
const query::procedure::CypherType *type;
|
||||
const query::procedure::CypherType *type = nullptr;
|
||||
if (is_not_optional_arg(i)) {
|
||||
name = callable.args[i].first;
|
||||
type = callable.args[i].second;
|
||||
|
||||
@@ -57,6 +57,8 @@ PyObject *gMgpSerializationError{nullptr}; // NOLINT(cppcoreguidelines-avo
|
||||
PyObject *gMgpAuthorizationError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
constexpr bool kStartGarbageCollection{true};
|
||||
constexpr auto kMicrosecondsInMillisecond{1000};
|
||||
constexpr auto kMicrosecondsInSecond{1000000};
|
||||
|
||||
// Returns true if an exception is raised
|
||||
bool RaiseExceptionFromErrorCode(const mgp_error error) {
|
||||
@@ -2488,21 +2490,23 @@ py::Object MgpValueToPyObject(const mgp_value &value, PyGraph *py_graph) {
|
||||
}
|
||||
case MGP_VALUE_TYPE_LOCAL_TIME: {
|
||||
const auto &local_time = value.local_time_v->local_time;
|
||||
py::Object py_local_time(PyTime_FromTime(local_time.hour, local_time.minute, local_time.second,
|
||||
local_time.millisecond * 1000 + local_time.microsecond));
|
||||
py::Object py_local_time(
|
||||
PyTime_FromTime(local_time.hour, local_time.minute, local_time.second,
|
||||
local_time.millisecond * kMicrosecondsInMillisecond + local_time.microsecond));
|
||||
return py_local_time;
|
||||
}
|
||||
case MGP_VALUE_TYPE_LOCAL_DATE_TIME: {
|
||||
const auto &local_time = value.local_date_time_v->local_date_time.local_time;
|
||||
const auto &date = value.local_date_time_v->local_date_time.date;
|
||||
py::Object py_local_date_time(PyDateTime_FromDateAndTime(date.year, date.month, date.day, local_time.hour,
|
||||
local_time.minute, local_time.second,
|
||||
local_time.millisecond * 1000 + local_time.microsecond));
|
||||
py::Object py_local_date_time(PyDateTime_FromDateAndTime(
|
||||
date.year, date.month, date.day, local_time.hour, local_time.minute, local_time.second,
|
||||
local_time.millisecond * kMicrosecondsInMillisecond + local_time.microsecond));
|
||||
return py_local_date_time;
|
||||
}
|
||||
case MGP_VALUE_TYPE_DURATION: {
|
||||
const auto &duration = value.duration_v->duration;
|
||||
py::Object py_duration(PyDelta_FromDSU(0, 0, duration.microseconds));
|
||||
py::Object py_duration(PyDelta_FromDSU(0, duration.microseconds / kMicrosecondsInSecond,
|
||||
duration.microseconds % kMicrosecondsInSecond));
|
||||
return py_duration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
namespace memgraph::rpc {
|
||||
|
||||
Client::Client(const io::network::Endpoint &endpoint, communication::ClientContext *context)
|
||||
: endpoint_(endpoint), context_(context) {}
|
||||
Client::Client(io::network::Endpoint endpoint, communication::ClientContext *context)
|
||||
: endpoint_(std::move(endpoint)), context_(context) {}
|
||||
|
||||
void Client::Abort() {
|
||||
if (!client_) return;
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace memgraph::rpc {
|
||||
/// Client is thread safe, but it is recommended to use thread_local clients.
|
||||
class Client {
|
||||
public:
|
||||
Client(const io::network::Endpoint &endpoint, communication::ClientContext *context);
|
||||
Client(io::network::Endpoint endpoint, communication::ClientContext *context);
|
||||
|
||||
/// Object used to handle streaming of request data to the RPC server.
|
||||
template <class TRequestResponse>
|
||||
@@ -194,7 +194,7 @@ class Client {
|
||||
/// Call this function from another thread to abort a pending RPC call.
|
||||
void Abort();
|
||||
|
||||
const auto &Endpoint() const { return endpoint_; }
|
||||
auto Endpoint() const -> io::network::Endpoint const & { return endpoint_; }
|
||||
|
||||
private:
|
||||
io::network::Endpoint endpoint_;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
namespace memgraph::rpc {
|
||||
|
||||
Server::Server(const io::network::Endpoint &endpoint, communication::ServerContext *context, size_t workers_count)
|
||||
: server_(endpoint, this, context, -1, context->use_ssl() ? "RPCS" : "RPC", workers_count) {}
|
||||
Server::Server(io::network::Endpoint endpoint, communication::ServerContext *context, size_t workers_count)
|
||||
: server_(std::move(endpoint), this, context, -1, context->use_ssl() ? "RPCS" : "RPC", workers_count) {}
|
||||
|
||||
bool Server::Start() { return server_.Start(); }
|
||||
|
||||
@@ -22,5 +22,7 @@ void Server::Shutdown() { server_.Shutdown(); }
|
||||
|
||||
void Server::AwaitShutdown() { server_.AwaitShutdown(); }
|
||||
|
||||
bool Server::IsRunning() { return server_.IsRunning(); }
|
||||
|
||||
const io::network::Endpoint &Server::endpoint() const { return server_.endpoint(); }
|
||||
} // namespace memgraph::rpc
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace memgraph::rpc {
|
||||
|
||||
class Server {
|
||||
public:
|
||||
Server(const io::network::Endpoint &endpoint, communication::ServerContext *context,
|
||||
Server(io::network::Endpoint endpoint, communication::ServerContext *context,
|
||||
size_t workers_count = std::thread::hardware_concurrency());
|
||||
Server(const Server &) = delete;
|
||||
Server(Server &&) = delete;
|
||||
@@ -35,6 +35,7 @@ class Server {
|
||||
bool Start();
|
||||
void Shutdown();
|
||||
void AwaitShutdown();
|
||||
bool IsRunning();
|
||||
|
||||
const io::network::Endpoint &endpoint() const;
|
||||
|
||||
|
||||
@@ -1,45 +1,55 @@
|
||||
set(storage_v2_src_files
|
||||
commit_log.cpp
|
||||
constraints/existence_constraints.cpp
|
||||
temporal.cpp
|
||||
durability/durability.cpp
|
||||
durability/serialization.cpp
|
||||
durability/snapshot.cpp
|
||||
durability/wal.cpp
|
||||
edge_accessor.cpp
|
||||
property_store.cpp
|
||||
vertex_accessor.cpp
|
||||
storage.cpp
|
||||
indices/indices.cpp
|
||||
all_vertices_iterable.cpp
|
||||
vertices_iterable.cpp
|
||||
inmemory/storage.cpp
|
||||
inmemory/label_index.cpp
|
||||
inmemory/label_property_index.cpp
|
||||
inmemory/unique_constraints.cpp
|
||||
disk/storage.cpp
|
||||
disk/rocksdb_storage.cpp
|
||||
disk/label_index.cpp
|
||||
disk/label_property_index.cpp
|
||||
disk/unique_constraints.cpp
|
||||
storage_mode.cpp
|
||||
isolation_level.cpp)
|
||||
|
||||
|
||||
set(storage_v2_src_files
|
||||
${storage_v2_src_files}
|
||||
replication/replication_client.cpp
|
||||
replication/replication_server.cpp
|
||||
replication/serialization.cpp
|
||||
replication/slk.cpp
|
||||
replication/replication_persistence_helper.cpp
|
||||
replication/rpc.cpp)
|
||||
|
||||
#######################
|
||||
find_package(gflags REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library(mg-storage-v2 STATIC ${storage_v2_src_files})
|
||||
target_link_libraries(mg-storage-v2 Threads::Threads mg-utils gflags)
|
||||
add_library(mg-storage-v2 STATIC
|
||||
commit_log.cpp
|
||||
constraints/existence_constraints.cpp
|
||||
temporal.cpp
|
||||
durability/durability.cpp
|
||||
durability/serialization.cpp
|
||||
durability/snapshot.cpp
|
||||
durability/wal.cpp
|
||||
edge_accessor.cpp
|
||||
property_store.cpp
|
||||
vertex_accessor.cpp
|
||||
vertex_info_cache_fwd.hpp
|
||||
vertex_info_cache.hpp
|
||||
vertex_info_cache.cpp
|
||||
storage.cpp
|
||||
indices/indices.cpp
|
||||
all_vertices_iterable.cpp
|
||||
vertices_iterable.cpp
|
||||
inmemory/storage.cpp
|
||||
inmemory/label_index.cpp
|
||||
inmemory/label_property_index.cpp
|
||||
inmemory/unique_constraints.cpp
|
||||
disk/storage.cpp
|
||||
disk/rocksdb_storage.cpp
|
||||
disk/label_index.cpp
|
||||
disk/label_property_index.cpp
|
||||
disk/unique_constraints.cpp
|
||||
storage_mode.cpp
|
||||
isolation_level.cpp
|
||||
replication/replication_client.cpp
|
||||
replication/replication_server.cpp
|
||||
replication/serialization.cpp
|
||||
replication/slk.cpp
|
||||
replication/replication_persistence_helper.cpp
|
||||
replication/rpc.cpp
|
||||
replication/replication.cpp
|
||||
inmemory/replication/replication_server.cpp
|
||||
inmemory/replication/replication_client.cpp
|
||||
)
|
||||
target_link_libraries(mg-storage-v2 Threads::Threads mg-utils gflags absl::flat_hash_map mg-rpc mg-slk)
|
||||
|
||||
target_link_libraries(mg-storage-v2 mg-rpc mg-slk)
|
||||
# Until we get LTO there is an advantage to do some unity builds
|
||||
set_target_properties(mg-storage-v2
|
||||
PROPERTIES
|
||||
UNITY_BUILD ON
|
||||
UNITY_BUILD_MODE GROUP
|
||||
)
|
||||
set_source_files_properties(
|
||||
vertex_info_cache.cpp vertex_accessor.cpp
|
||||
PROPERTIES UNITY_GROUP "ensure inline of vertex_info_cache"
|
||||
)
|
||||
|
||||
@@ -69,7 +69,6 @@ struct Config {
|
||||
|
||||
struct DiskConfig {
|
||||
std::filesystem::path main_storage_directory{"storage/rocksdb_main_storage"};
|
||||
std::filesystem::path main_edge_directory{"storage/rocksdb_main_edge_storage"};
|
||||
std::filesystem::path label_index_directory{"storage/rocksdb_label_index"};
|
||||
std::filesystem::path label_property_index_directory{"storage/rocksdb_label_property_index"};
|
||||
std::filesystem::path unique_constraints_directory{"storage/rocksdb_unique_constraints"};
|
||||
@@ -77,7 +76,6 @@ struct Config {
|
||||
std::filesystem::path id_name_mapper_directory{"storage/rocksdb_id_name_mapper"};
|
||||
std::filesystem::path durability_directory{"storage/rocksdb_durability"};
|
||||
std::filesystem::path wal_directory{"storage/rocksdb_wal"};
|
||||
std::filesystem::path wal_edge_directory{"storage/rocksdb_wal_edge"};
|
||||
} disk;
|
||||
|
||||
std::string name;
|
||||
|
||||
@@ -154,9 +154,14 @@ struct Delta {
|
||||
struct RemoveInEdgeTag {};
|
||||
struct RemoveOutEdgeTag {};
|
||||
|
||||
Delta(DeleteDeserializedObjectTag /*tag*/, std::atomic<uint64_t> *timestamp,
|
||||
const std::optional<std::string> &old_disk_key)
|
||||
: action(Action::DELETE_DESERIALIZED_OBJECT), timestamp(timestamp), command_id(0), old_disk_key(old_disk_key) {}
|
||||
// DELETE_DESERIALIZED_OBJECT is used to load data from disk committed by past txs.
|
||||
// Because of this object was created in past txs, we create timestamp by ourselves inside instead of having it from
|
||||
// current tx. This timestamp we got from RocksDB timestamp stored in key.
|
||||
Delta(DeleteDeserializedObjectTag /*tag*/, uint64_t ts, const std::optional<std::string> &old_disk_key)
|
||||
: action(Action::DELETE_DESERIALIZED_OBJECT),
|
||||
timestamp(new std::atomic<uint64_t>(ts)),
|
||||
command_id(0),
|
||||
old_disk_key(old_disk_key) {}
|
||||
|
||||
Delta(DeleteObjectTag /*tag*/, std::atomic<uint64_t> *timestamp, uint64_t command_id)
|
||||
: action(Action::DELETE_OBJECT), timestamp(timestamp), command_id(command_id) {}
|
||||
@@ -174,6 +179,10 @@ struct Delta {
|
||||
uint64_t command_id)
|
||||
: action(Action::SET_PROPERTY), timestamp(timestamp), command_id(command_id), property({key, value}) {}
|
||||
|
||||
Delta(SetPropertyTag /*tag*/, PropertyId key, PropertyValue &&value, std::atomic<uint64_t> *timestamp,
|
||||
uint64_t command_id)
|
||||
: action(Action::SET_PROPERTY), timestamp(timestamp), command_id(command_id), property({key, std::move(value)}) {}
|
||||
|
||||
Delta(AddInEdgeTag /*tag*/, EdgeTypeId edge_type, Vertex *vertex, EdgeRef edge, std::atomic<uint64_t> *timestamp,
|
||||
uint64_t command_id)
|
||||
: action(Action::ADD_IN_EDGE),
|
||||
@@ -220,6 +229,8 @@ struct Delta {
|
||||
break;
|
||||
case Action::DELETE_DESERIALIZED_OBJECT:
|
||||
old_disk_key.reset();
|
||||
delete timestamp;
|
||||
timestamp = nullptr;
|
||||
break;
|
||||
case Action::SET_PROPERTY:
|
||||
property.value.~PropertyValue();
|
||||
|
||||
@@ -124,6 +124,10 @@ bool DiskLabelIndex::ClearDeletedVertex(std::string_view gid, uint64_t transacti
|
||||
|
||||
bool DiskLabelIndex::DeleteVerticesWithRemovedIndexingLabel(uint64_t transaction_start_timestamp,
|
||||
uint64_t transaction_commit_timestamp) {
|
||||
if (entries_for_deletion->empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto disk_transaction = CreateAllReadingRocksDBTransaction();
|
||||
|
||||
rocksdb::ReadOptions ro;
|
||||
|
||||
@@ -131,6 +131,9 @@ bool DiskLabelPropertyIndex::ClearDeletedVertex(std::string_view gid, uint64_t t
|
||||
|
||||
bool DiskLabelPropertyIndex::DeleteVerticesWithRemovedIndexingLabel(uint64_t transaction_start_timestamp,
|
||||
uint64_t transaction_commit_timestamp) {
|
||||
if (entries_for_deletion->empty()) {
|
||||
return true;
|
||||
}
|
||||
auto disk_transaction = CreateAllReadingRocksDBTransaction();
|
||||
|
||||
rocksdb::ReadOptions ro;
|
||||
|
||||
@@ -80,4 +80,38 @@ int ComparatorWithU64TsImpl::CompareTimestamp(const rocksdb::Slice &ts1, const r
|
||||
return 0;
|
||||
}
|
||||
|
||||
DiskEdgeKey::DiskEdgeKey(storage::EdgeAccessor *edge_acc) {
|
||||
auto from_gid = utils::SerializeIdType(edge_acc->FromVertex().Gid());
|
||||
auto to_gid = utils::SerializeIdType(edge_acc->ToVertex().Gid());
|
||||
auto edge_type = utils::SerializeIdType(edge_acc->EdgeType());
|
||||
auto edge_gid = utils::SerializeIdType(edge_acc->Gid());
|
||||
|
||||
key = fmt::format("{}|{}|{}|{}|{}", from_gid, to_gid, utils::outEdgeDirection, edge_type, edge_gid);
|
||||
}
|
||||
|
||||
DiskEdgeKey::DiskEdgeKey(storage::Gid src_vertex_gid, storage::Gid dest_vertex_gid, storage::EdgeTypeId edge_type_id,
|
||||
const storage::EdgeRef edge_ref, bool properties_on_edges) {
|
||||
auto from_gid = utils::SerializeIdType(src_vertex_gid);
|
||||
auto to_gid = utils::SerializeIdType(dest_vertex_gid);
|
||||
auto edge_type = utils::SerializeIdType(edge_type_id);
|
||||
std::string edge_gid;
|
||||
|
||||
if (properties_on_edges) {
|
||||
edge_gid = utils::SerializeIdType(edge_ref.ptr->gid);
|
||||
} else {
|
||||
edge_gid = utils::SerializeIdType(edge_ref.gid);
|
||||
}
|
||||
|
||||
key = fmt::format("{}|{}|{}|{}|{}", from_gid, to_gid, utils::outEdgeDirection, edge_type, edge_gid);
|
||||
}
|
||||
|
||||
std::string DiskEdgeKey::GetVertexOutGid() const { return key.substr(0, key.find('|')); }
|
||||
|
||||
std::string DiskEdgeKey::GetVertexInGid() const {
|
||||
auto vertex_in_start = key.find('|') + 1;
|
||||
return key.substr(vertex_in_start, key.find('|', vertex_in_start) - vertex_in_start);
|
||||
}
|
||||
|
||||
std::string DiskEdgeKey::GetEdgeGid() const { return key.substr(key.rfind('|') + 1); }
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <rocksdb/status.h>
|
||||
#include <rocksdb/utilities/transaction_db.h>
|
||||
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
#include "storage/v2/edge_direction.hpp"
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/property_store.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
@@ -45,6 +47,21 @@ struct RocksDBStorage {
|
||||
|
||||
rocksdb::Options options_;
|
||||
rocksdb::TransactionDB *db_;
|
||||
rocksdb::ColumnFamilyHandle *vertex_chandle = nullptr;
|
||||
rocksdb::ColumnFamilyHandle *edge_chandle = nullptr;
|
||||
rocksdb::ColumnFamilyHandle *default_chandle = nullptr;
|
||||
|
||||
uint64_t ApproximateVertexCount() const {
|
||||
uint64_t estimate_num_keys = 0;
|
||||
db_->GetIntProperty(vertex_chandle, "rocksdb.estimate-num-keys", &estimate_num_keys);
|
||||
return estimate_num_keys;
|
||||
}
|
||||
|
||||
uint64_t ApproximateEdgeCount() const {
|
||||
uint64_t estimate_num_keys = 0;
|
||||
db_->GetIntProperty(edge_chandle, "rocksdb.estimate-num-keys", &estimate_num_keys);
|
||||
return estimate_num_keys;
|
||||
}
|
||||
};
|
||||
|
||||
/// RocksDB comparator that compares keys with timestamps.
|
||||
@@ -71,4 +88,27 @@ class ComparatorWithU64TsImpl : public rocksdb::Comparator {
|
||||
const Comparator *cmp_without_ts_{nullptr};
|
||||
};
|
||||
|
||||
struct DiskEdgeKey {
|
||||
DiskEdgeKey(const std::string_view keyView) : key(keyView) {}
|
||||
|
||||
DiskEdgeKey(EdgeAccessor *edge_acc);
|
||||
|
||||
/// @tparam src_vertex_gid, dest_vertex_gid: Gid of the source and destination vertices
|
||||
/// @tparam edge_type_id: EdgeTypeId of the edge
|
||||
/// @tparam edge_ref: Edge to be serialized
|
||||
DiskEdgeKey(Gid src_vertex_gid, storage::Gid dest_vertex_gid, storage::EdgeTypeId edge_type_id,
|
||||
const EdgeRef edge_ref, bool properties_on_edges);
|
||||
|
||||
std::string GetSerializedKey() const { return key; }
|
||||
|
||||
std::string GetVertexOutGid() const;
|
||||
std::string GetVertexInGid() const;
|
||||
std::string GetEdgeGid() const;
|
||||
|
||||
private:
|
||||
// vertex_gid_1 | vertex_gid_2 | direction | edge_type | GID | commit_timestamp
|
||||
// Currently direction is only out.
|
||||
std::string key;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,13 @@
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/isolation_level.hpp"
|
||||
#include "storage/v2/property_store.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/storage.hpp"
|
||||
#include "utils/rw_lock.hpp"
|
||||
|
||||
#include <rocksdb/db.h>
|
||||
#include <rocksdb/slice.h>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
@@ -58,14 +61,48 @@ class DiskStorage final : public Storage {
|
||||
|
||||
VerticesIterable Vertices(LabelId label, View view) override;
|
||||
|
||||
std::unordered_set<Gid> MergeVerticesFromMainCacheWithLabelIndexCache(LabelId label, View view,
|
||||
std::list<Delta> &index_deltas,
|
||||
utils::SkipList<Vertex> *indexed_vertices);
|
||||
|
||||
void LoadVerticesFromDiskLabelIndex(LabelId label, const std::unordered_set<storage::Gid> &gids,
|
||||
std::list<Delta> &index_deltas, utils::SkipList<Vertex> *indexed_vertices);
|
||||
|
||||
VerticesIterable Vertices(LabelId label, PropertyId property, View view) override;
|
||||
|
||||
std::unordered_set<Gid> MergeVerticesFromMainCacheWithLabelPropertyIndexCache(
|
||||
LabelId label, PropertyId property, View view, std::list<Delta> &index_deltas,
|
||||
utils::SkipList<Vertex> *indexed_vertices, const auto &label_property_filter);
|
||||
|
||||
void LoadVerticesFromDiskLabelPropertyIndex(LabelId label, PropertyId property,
|
||||
const std::unordered_set<storage::Gid> &gids,
|
||||
std::list<Delta> &index_deltas,
|
||||
utils::SkipList<Vertex> *indexed_vertices,
|
||||
const auto &label_property_filter);
|
||||
|
||||
VerticesIterable Vertices(LabelId label, PropertyId property, const PropertyValue &value, View view) override;
|
||||
|
||||
void LoadVerticesFromDiskLabelPropertyIndexWithPointValueLookup(LabelId label, PropertyId property,
|
||||
const std::unordered_set<storage::Gid> &gids,
|
||||
const PropertyValue &value,
|
||||
std::list<Delta> &index_deltas,
|
||||
utils::SkipList<Vertex> *indexed_vertices);
|
||||
|
||||
VerticesIterable Vertices(LabelId label, PropertyId property,
|
||||
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
|
||||
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) override;
|
||||
|
||||
std::unordered_set<Gid> MergeVerticesFromMainCacheWithLabelPropertyIndexCacheForIntervalSearch(
|
||||
LabelId label, PropertyId property, View view, const std::optional<utils::Bound<PropertyValue>> &lower_bound,
|
||||
const std::optional<utils::Bound<PropertyValue>> &upper_bound, std::list<Delta> &index_deltas,
|
||||
utils::SkipList<Vertex> *indexed_vertices);
|
||||
|
||||
void LoadVerticesFromDiskLabelPropertyIndexForIntervalSearch(
|
||||
LabelId label, PropertyId property, const std::unordered_set<storage::Gid> &gids,
|
||||
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
|
||||
const std::optional<utils::Bound<PropertyValue>> &upper_bound, std::list<Delta> &index_deltas,
|
||||
utils::SkipList<Vertex> *indexed_vertices);
|
||||
|
||||
uint64_t ApproximateVertexCount() const override;
|
||||
|
||||
uint64_t ApproximateVertexCount(LabelId /*label*/) const override { return 10; }
|
||||
@@ -161,26 +198,30 @@ class DiskStorage final : public Storage {
|
||||
void FinalizeTransaction() override;
|
||||
|
||||
std::optional<storage::VertexAccessor> LoadVertexToLabelIndexCache(
|
||||
const rocksdb::Slice &key, const rocksdb::Slice &value, Delta *index_delta,
|
||||
LabelId indexing_label, std::string &&key, std::string &&value, Delta *index_delta,
|
||||
utils::SkipList<storage::Vertex>::Accessor index_accessor);
|
||||
|
||||
std::optional<storage::VertexAccessor> LoadVertexToMainMemoryCache(const rocksdb::Slice &key,
|
||||
const rocksdb::Slice &value);
|
||||
std::optional<storage::VertexAccessor> LoadVertexToMainMemoryCache(const std::string &key, const std::string &value,
|
||||
const std::string &ts);
|
||||
|
||||
std::optional<storage::VertexAccessor> LoadVertexToLabelPropertyIndexCache(
|
||||
const rocksdb::Slice &key, const rocksdb::Slice &value, Delta *index_delta,
|
||||
LabelId indexing_label, std::string &&key, std::string &&value, Delta *index_delta,
|
||||
utils::SkipList<storage::Vertex>::Accessor index_accessor);
|
||||
|
||||
std::optional<storage::EdgeAccessor> DeserializeEdge(const rocksdb::Slice &key, const rocksdb::Slice &value);
|
||||
std::optional<storage::EdgeAccessor> DeserializeEdge(const rocksdb::Slice &key, const rocksdb::Slice &value,
|
||||
const rocksdb::Slice &ts);
|
||||
|
||||
private:
|
||||
VertexAccessor CreateVertex(utils::SkipList<Vertex>::Accessor &accessor, storage::Gid gid,
|
||||
const std::vector<LabelId> &label_ids, PropertyStore &&properties, Delta *delta);
|
||||
std::vector<LabelId> &&label_ids, PropertyStore &&properties, Delta *delta);
|
||||
|
||||
void PrefetchEdges(const auto &prefetch_edge_filter);
|
||||
bool PrefetchEdgeFilter(const std::string_view disk_edge_key_str, const VertexAccessor &vertex_acc,
|
||||
EdgeDirection edge_direction);
|
||||
void PrefetchEdges(const VertexAccessor &vertex_acc, EdgeDirection edge_direction);
|
||||
|
||||
Result<EdgeAccessor> CreateEdge(const VertexAccessor *from, const VertexAccessor *to, EdgeTypeId edge_type,
|
||||
storage::Gid gid, std::string_view properties, const std::string &old_disk_key);
|
||||
storage::Gid gid, std::string_view properties, const std::string &old_disk_key,
|
||||
const std::string &ts);
|
||||
|
||||
/// Flushes vertices and edges to the disk with the commit timestamp.
|
||||
/// At the time of calling, the commit_timestamp_ must already exist.
|
||||
@@ -206,10 +247,10 @@ class DiskStorage final : public Storage {
|
||||
std::vector<std::list<Delta>> index_deltas_storage_;
|
||||
utils::SkipList<storage::Edge> edges_;
|
||||
Config::Items config_;
|
||||
std::vector<std::string> edges_to_delete_;
|
||||
std::unordered_set<std::string> edges_to_delete_;
|
||||
std::vector<std::pair<std::string, std::string>> vertices_to_delete_;
|
||||
rocksdb::Transaction *vertex_disk_transaction_;
|
||||
rocksdb::Transaction *edge_disk_transaction_;
|
||||
rocksdb::Transaction *disk_transaction_;
|
||||
bool scanned_all_vertices_ = false;
|
||||
};
|
||||
|
||||
std::unique_ptr<Storage::Accessor> Access(std::optional<IsolationLevel> override_isolation_level) override {
|
||||
@@ -220,9 +261,7 @@ class DiskStorage final : public Storage {
|
||||
return std::unique_ptr<DiskAccessor>(new DiskAccessor{this, isolation_level, storage_mode_});
|
||||
}
|
||||
|
||||
void PrepareRocksDBOptions();
|
||||
|
||||
RocksDBStorage *GetRocksDBStorage() const { return vertex_kvstore_.get(); }
|
||||
RocksDBStorage *GetRocksDBStorage() const { return kvstore_.get(); }
|
||||
|
||||
utils::BasicResult<StorageIndexDefinitionError, void> CreateIndex(
|
||||
LabelId label, std::optional<uint64_t> desired_commit_timestamp) override;
|
||||
@@ -299,8 +338,21 @@ class DiskStorage final : public Storage {
|
||||
|
||||
uint64_t CommitTimestamp(std::optional<uint64_t> desired_commit_timestamp = {});
|
||||
|
||||
std::unique_ptr<RocksDBStorage> vertex_kvstore_;
|
||||
std::unique_ptr<RocksDBStorage> edge_kvstore_;
|
||||
void EstablishNewEpoch() override { throw utils::BasicException("Disk storage mode does not support replication."); }
|
||||
|
||||
auto CreateReplicationClient(std::string name, io::network::Endpoint endpoint, replication::ReplicationMode mode,
|
||||
const replication::ReplicationClientConfig &config)
|
||||
-> std::unique_ptr<ReplicationClient> override {
|
||||
throw utils::BasicException("Disk storage mode does not support replication.");
|
||||
}
|
||||
|
||||
auto CreateReplicationServer(io::network::Endpoint endpoint, const replication::ReplicationServerConfig &config)
|
||||
-> std::unique_ptr<ReplicationServer> override {
|
||||
throw utils::BasicException("Disk storage mode does not support replication.");
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<RocksDBStorage> kvstore_;
|
||||
std::unique_ptr<kvstore::KVStore> durability_kvstore_;
|
||||
};
|
||||
|
||||
|
||||
@@ -175,6 +175,10 @@ bool DiskUniqueConstraints::ClearDeletedVertex(const std::string_view gid,
|
||||
|
||||
bool DiskUniqueConstraints::DeleteVerticesWithRemovedConstraintLabel(uint64_t transaction_start_timestamp,
|
||||
uint64_t transaction_commit_timestamp) {
|
||||
if (entries_for_deletion->empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto disk_transaction = std::unique_ptr<rocksdb::Transaction>(
|
||||
kvstore_->db_->BeginTransaction(rocksdb::WriteOptions(), rocksdb::TransactionOptions()));
|
||||
disk_transaction->SetReadTimestampForValidation(std::numeric_limits<uint64_t>::max());
|
||||
@@ -198,12 +202,11 @@ bool DiskUniqueConstraints::DeleteVerticesWithRemovedConstraintLabel(uint64_t tr
|
||||
disk_transaction->SetCommitTimestamp(transaction_commit_timestamp);
|
||||
auto status = disk_transaction->Commit();
|
||||
if (!status.ok()) {
|
||||
/// TODO: better naming
|
||||
spdlog::error("rocksdb: {}", status.getState());
|
||||
}
|
||||
return status.ok();
|
||||
}
|
||||
spdlog::error("Deletetion of vertices with removed constraint label failed.");
|
||||
spdlog::error("Deletion of vertices with removed constraint label failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -214,7 +217,7 @@ bool DiskUniqueConstraints::SyncVertexToUniqueConstraintsStorage(const Vertex &v
|
||||
kvstore_->db_->BeginTransaction(rocksdb::WriteOptions(), rocksdb::TransactionOptions()));
|
||||
|
||||
if (auto maybe_old_disk_key = utils::GetOldDiskKeyOrNull(vertex.delta); maybe_old_disk_key.has_value()) {
|
||||
spdlog::debug("Found old disk key {} for vertex {}", maybe_old_disk_key.value(),
|
||||
spdlog::trace("Found old disk key {} for vertex {}", maybe_old_disk_key.value(),
|
||||
utils::SerializeIdType(vertex.gid));
|
||||
if (auto status = disk_transaction->Delete(maybe_old_disk_key.value()); !status.ok()) {
|
||||
return false;
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
|
||||
#include "storage/v2/delta.hpp"
|
||||
#include "storage/v2/mvcc.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/result.hpp"
|
||||
#include "storage/v2/vertex_accessor.hpp"
|
||||
#include "utils/memory_tracker.hpp"
|
||||
|
||||
@@ -145,6 +147,26 @@ Result<bool> EdgeAccessor::InitProperties(const std::map<storage::PropertyId, st
|
||||
return true;
|
||||
}
|
||||
|
||||
Result<std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>>> EdgeAccessor::UpdateProperties(
|
||||
std::map<storage::PropertyId, storage::PropertyValue> &properties) const {
|
||||
utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
|
||||
if (!config_.properties_on_edges) return Error::PROPERTIES_DISABLED;
|
||||
|
||||
std::lock_guard<utils::SpinLock> guard(edge_.ptr->lock);
|
||||
|
||||
if (!PrepareForWrite(transaction_, edge_.ptr)) return Error::SERIALIZATION_ERROR;
|
||||
|
||||
if (edge_.ptr->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
auto id_old_new_change = edge_.ptr->properties.UpdateProperties(properties);
|
||||
|
||||
for (auto &[property, old_value, new_value] : id_old_new_change) {
|
||||
CreateAndLinkDelta(transaction_, edge_.ptr, Delta::SetPropertyTag(), property, std::move(old_value));
|
||||
}
|
||||
|
||||
return id_old_new_change;
|
||||
}
|
||||
|
||||
Result<std::map<PropertyId, PropertyValue>> EdgeAccessor::ClearProperties() {
|
||||
if (!config_.properties_on_edges) return Error::PROPERTIES_DISABLED;
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ class EdgeAccessor final {
|
||||
/// @throw std::bad_alloc
|
||||
Result<bool> InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties);
|
||||
|
||||
Result<std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>>> UpdateProperties(
|
||||
std::map<storage::PropertyId, storage::PropertyValue> &properties) const;
|
||||
|
||||
/// Remove all properties and return old values for each removed property.
|
||||
/// @throw std::bad_alloc
|
||||
Result<std::map<PropertyId, PropertyValue>> ClearProperties();
|
||||
|
||||
20
src/storage/v2/edge_direction.hpp
Normal file
20
src/storage/v2/edge_direction.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace memgraph::storage {
|
||||
enum class EdgeDirection : uint8_t {
|
||||
OUT = 0,
|
||||
IN = 1,
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "storage/v2/mvcc.hpp"
|
||||
#include "storage/v2/transaction.hpp"
|
||||
#include "storage/v2/vertex.hpp"
|
||||
#include "storage/v2/vertex_info_helpers.hpp"
|
||||
#include "utils/spin_lock.hpp"
|
||||
#include "utils/synchronized.hpp"
|
||||
|
||||
@@ -156,62 +157,12 @@ inline bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label, Prop
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function for iterating through label index. Returns true if this
|
||||
// transaction can see the given vertex, and the visible version has the given
|
||||
// label.
|
||||
inline bool CurrentVersionHasLabel(const Vertex &vertex, LabelId label, Transaction *transaction, View view) {
|
||||
bool deleted = false;
|
||||
bool has_label = false;
|
||||
const Delta *delta = nullptr;
|
||||
{
|
||||
std::lock_guard<utils::SpinLock> guard(vertex.lock);
|
||||
deleted = vertex.deleted;
|
||||
has_label = utils::Contains(vertex.labels, label);
|
||||
delta = vertex.delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction, delta, view, [&deleted, &has_label, label](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::REMOVE_LABEL: {
|
||||
if (delta.label == label) {
|
||||
MG_ASSERT(has_label, "Invalid database state!");
|
||||
has_label = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL: {
|
||||
if (delta.label == label) {
|
||||
MG_ASSERT(!has_label, "Invalid database state!");
|
||||
has_label = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
MG_ASSERT(!deleted, "Invalid database state!");
|
||||
deleted = true;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
MG_ASSERT(deleted, "Invalid database state!");
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
}
|
||||
});
|
||||
return !deleted && has_label;
|
||||
}
|
||||
|
||||
// Helper function for iterating through label-property index. Returns true if
|
||||
// this transaction can see the given vertex, and the visible version has the
|
||||
// given label and property.
|
||||
inline bool CurrentVersionHasLabelProperty(const Vertex &vertex, LabelId label, PropertyId key,
|
||||
const PropertyValue &value, Transaction *transaction, View view) {
|
||||
bool exists = true;
|
||||
bool deleted = false;
|
||||
bool has_label = false;
|
||||
bool current_value_equal_to_value = value.IsNull();
|
||||
@@ -223,46 +174,46 @@ inline bool CurrentVersionHasLabelProperty(const Vertex &vertex, LabelId label,
|
||||
current_value_equal_to_value = vertex.properties.IsPropertyEqual(key, value);
|
||||
delta = vertex.delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction, delta, view,
|
||||
[&deleted, &has_label, ¤t_value_equal_to_value, key, label, &value](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::SET_PROPERTY: {
|
||||
if (delta.property.key == key) {
|
||||
current_value_equal_to_value = delta.property.value == value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
MG_ASSERT(!deleted, "Invalid database state!");
|
||||
deleted = true;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
MG_ASSERT(deleted, "Invalid database state!");
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL:
|
||||
if (delta.label == label) {
|
||||
MG_ASSERT(!has_label, "Invalid database state!");
|
||||
has_label = true;
|
||||
}
|
||||
break;
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
if (delta.label == label) {
|
||||
MG_ASSERT(has_label, "Invalid database state!");
|
||||
has_label = false;
|
||||
}
|
||||
break;
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
}
|
||||
});
|
||||
return !deleted && has_label && current_value_equal_to_value;
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, &vertex, false); resError) return false;
|
||||
auto resLabel = cache.GetHasLabel(view, &vertex, label);
|
||||
if (resLabel && *resLabel) {
|
||||
auto resProp = cache.GetProperty(view, &vertex, key);
|
||||
if (resProp && *resProp == value) return true;
|
||||
}
|
||||
}
|
||||
|
||||
auto const n_processed = ApplyDeltasForRead(transaction, delta, view, [&, label, key](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
HasLabel_ActionMethod(has_label, label),
|
||||
PropertyValueMatch_ActionMethod(current_value_equal_to_value, key,value)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction->manyDeltasCache;
|
||||
cache.StoreExists(view, &vertex, exists);
|
||||
cache.StoreDeleted(view, &vertex, deleted);
|
||||
cache.StoreHasLabel(view, &vertex, label, has_label);
|
||||
if (current_value_equal_to_value) {
|
||||
cache.StoreProperty(view, &vertex, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return exists && !deleted && has_label && current_value_equal_to_value;
|
||||
}
|
||||
|
||||
template <typename TIndexAccessor>
|
||||
|
||||
@@ -128,10 +128,12 @@ void InMemoryLabelIndex::Iterable::Iterator::AdvanceUntilValid() {
|
||||
if (index_iterator_->vertex == current_vertex_) {
|
||||
continue;
|
||||
}
|
||||
if (CurrentVersionHasLabel(*index_iterator_->vertex, self_->label_, self_->transaction_, self_->view_)) {
|
||||
current_vertex_ = index_iterator_->vertex;
|
||||
current_vertex_accessor_ = VertexAccessor{current_vertex_, self_->transaction_, self_->indices_,
|
||||
self_->constraints_, self_->config_.items};
|
||||
auto accessor = VertexAccessor{index_iterator_->vertex, self_->transaction_, self_->indices_, self_->constraints_,
|
||||
self_->config_.items};
|
||||
auto res = accessor.HasLabel(self_->label_, self_->view_);
|
||||
if (!res.HasError() and res.GetValue()) {
|
||||
current_vertex_ = accessor.vertex_;
|
||||
current_vertex_accessor_ = accessor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ bool InMemoryLabelPropertyIndex::CreateIndex(LabelId label, PropertyId property,
|
||||
|
||||
auto [it, emplaced] =
|
||||
index_.emplace(std::piecewise_construct, std::forward_as_tuple(label, property), std::forward_as_tuple());
|
||||
|
||||
indices_by_property_[property].insert({label, &index_.at({label, property})});
|
||||
|
||||
if (!emplaced) {
|
||||
// Index already exists.
|
||||
return false;
|
||||
@@ -75,6 +78,7 @@ bool InMemoryLabelPropertyIndex::CreateIndex(LabelId label, PropertyId property,
|
||||
if (parallel_exec_info) {
|
||||
return create_index_par(label, property, vertices, it, *parallel_exec_info);
|
||||
}
|
||||
|
||||
return create_index_seq(label, property, vertices, it);
|
||||
}
|
||||
|
||||
@@ -97,18 +101,26 @@ void InMemoryLabelPropertyIndex::UpdateOnSetProperty(PropertyId property, const
|
||||
if (value.IsNull()) {
|
||||
return;
|
||||
}
|
||||
for (auto &[label_prop, storage] : index_) {
|
||||
if (label_prop.second != property) {
|
||||
continue;
|
||||
}
|
||||
if (utils::Contains(vertex->labels, label_prop.first)) {
|
||||
auto acc = storage.access();
|
||||
acc.insert(Entry{value, vertex, tx.start_timestamp});
|
||||
}
|
||||
|
||||
if (!indices_by_property_.contains(property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &[_, storage] : indices_by_property_.at(property)) {
|
||||
auto acc = storage->access();
|
||||
acc.insert(Entry{value, vertex, tx.start_timestamp});
|
||||
}
|
||||
}
|
||||
|
||||
bool InMemoryLabelPropertyIndex::DropIndex(LabelId label, PropertyId property) {
|
||||
if (indices_by_property_.find(property) != indices_by_property_.end()) {
|
||||
indices_by_property_.at(property).erase(label);
|
||||
|
||||
if (indices_by_property_.at(property).empty()) {
|
||||
indices_by_property_.erase(property);
|
||||
}
|
||||
}
|
||||
|
||||
return index_.erase({label, property}) > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ class InMemoryLabelPropertyIndex : public storage::LabelPropertyIndex {
|
||||
|
||||
private:
|
||||
std::map<std::pair<LabelId, PropertyId>, utils::SkipList<Entry>> index_;
|
||||
std::unordered_map<PropertyId, std::unordered_map<LabelId, utils::SkipList<Entry> *>> indices_by_property_;
|
||||
std::map<std::pair<LabelId, PropertyId>, storage::LabelPropertyIndexStats> stats_;
|
||||
};
|
||||
|
||||
|
||||
566
src/storage/v2/inmemory/replication/replication_client.cpp
Normal file
566
src/storage/v2/inmemory/replication/replication_client.cpp
Normal file
@@ -0,0 +1,566 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#include "storage/v2/inmemory/replication/replication_client.hpp"
|
||||
|
||||
#include "storage/v2/durability/durability.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
namespace {
|
||||
template <typename>
|
||||
[[maybe_unused]] inline constexpr bool always_false_v = false;
|
||||
} // namespace
|
||||
|
||||
// Handler for transfering the current WAL file whose data is
|
||||
// contained in the internal buffer and the file.
|
||||
class CurrentWalHandler {
|
||||
public:
|
||||
explicit CurrentWalHandler(ReplicationClient *self);
|
||||
void AppendFilename(const std::string &filename);
|
||||
|
||||
void AppendSize(size_t size);
|
||||
|
||||
void AppendFileData(utils::InputFile *file);
|
||||
|
||||
void AppendBufferData(const uint8_t *buffer, size_t buffer_size);
|
||||
|
||||
/// @throw rpc::RpcFailedException
|
||||
replication::CurrentWalRes Finalize();
|
||||
|
||||
private:
|
||||
ReplicationClient *self_;
|
||||
rpc::Client::StreamHandler<replication::CurrentWalRpc> stream_;
|
||||
};
|
||||
|
||||
////// CurrentWalHandler //////
|
||||
CurrentWalHandler::CurrentWalHandler(ReplicationClient *self)
|
||||
: self_(self), stream_(self_->rpc_client_.Stream<replication::CurrentWalRpc>()) {}
|
||||
|
||||
void CurrentWalHandler::AppendFilename(const std::string &filename) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
encoder.WriteString(filename);
|
||||
}
|
||||
|
||||
void CurrentWalHandler::AppendSize(const size_t size) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
encoder.WriteUint(size);
|
||||
}
|
||||
|
||||
void CurrentWalHandler::AppendFileData(utils::InputFile *file) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
encoder.WriteFileData(file);
|
||||
}
|
||||
|
||||
void CurrentWalHandler::AppendBufferData(const uint8_t *buffer, const size_t buffer_size) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
encoder.WriteBuffer(buffer, buffer_size);
|
||||
}
|
||||
|
||||
replication::CurrentWalRes CurrentWalHandler::Finalize() { return stream_.AwaitResponse(); }
|
||||
|
||||
////// ReplicationClient //////
|
||||
InMemoryReplicationClient::InMemoryReplicationClient(InMemoryStorage *storage, std::string name,
|
||||
io::network::Endpoint endpoint, replication::ReplicationMode mode,
|
||||
const replication::ReplicationClientConfig &config)
|
||||
: ReplicationClient{std::move(name), std::move(endpoint), mode, config}, storage_{storage} {}
|
||||
|
||||
void InMemoryReplicationClient::TryInitializeClientAsync() {
|
||||
thread_pool_.AddTask([this] {
|
||||
rpc_client_.Abort();
|
||||
this->TryInitializeClientSync();
|
||||
});
|
||||
}
|
||||
void InMemoryReplicationClient::FrequentCheck() {
|
||||
const auto is_success = std::invoke([this]() {
|
||||
try {
|
||||
auto stream{rpc_client_.Stream<replication::FrequentHeartbeatRpc>()};
|
||||
const auto response = stream.AwaitResponse();
|
||||
return response.success;
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
// States: READY, REPLICATING, RECOVERY, INVALID
|
||||
// If success && ready, replicating, recovery -> stay the same because something good is going on.
|
||||
// If success && INVALID -> [it's possible that replica came back to life] -> TryInitializeClient.
|
||||
// If fail -> [replica is not reachable at all] -> INVALID state.
|
||||
// NOTE: TryInitializeClient might return nothing if there is a branching point.
|
||||
// NOTE: The early return pattern simplified the code, but the behavior should be as explained.
|
||||
if (!is_success) {
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
return;
|
||||
}
|
||||
if (replica_state_.load() == replication::ReplicaState::INVALID) {
|
||||
TryInitializeClientAsync();
|
||||
}
|
||||
} /// @throws rpc::RpcFailedException
|
||||
|
||||
void InMemoryReplicationClient::Start() {
|
||||
auto const &endpoint = rpc_client_.Endpoint();
|
||||
spdlog::trace("Replication client started at: {}:{}", endpoint.address, endpoint.port);
|
||||
|
||||
TryInitializeClientSync();
|
||||
|
||||
// Help the user to get the most accurate replica state possible.
|
||||
if (replica_check_frequency_ > std::chrono::seconds(0)) {
|
||||
replica_checker_.Run("Replica Checker", replica_check_frequency_, [this] { FrequentCheck(); });
|
||||
}
|
||||
}
|
||||
|
||||
void InMemoryReplicationClient::InitializeClient() {
|
||||
uint64_t current_commit_timestamp{kTimestampInitialId};
|
||||
|
||||
const auto &main_epoch = storage_->replication_state_.GetEpoch();
|
||||
|
||||
auto stream{rpc_client_.Stream<replication::HeartbeatRpc>(storage_->replication_state_.last_commit_timestamp_,
|
||||
main_epoch.id)};
|
||||
|
||||
const auto replica = stream.AwaitResponse();
|
||||
std::optional<uint64_t> branching_point;
|
||||
if (replica.epoch_id != main_epoch.id && replica.current_commit_timestamp != kTimestampInitialId) {
|
||||
auto const &history = storage_->replication_state_.history;
|
||||
const auto epoch_info_iter = std::find_if(history.crbegin(), history.crend(), [&](const auto &main_epoch_info) {
|
||||
return main_epoch_info.first == replica.epoch_id;
|
||||
});
|
||||
if (epoch_info_iter == history.crend()) {
|
||||
branching_point = 0;
|
||||
} else if (epoch_info_iter->second != replica.current_commit_timestamp) {
|
||||
branching_point = epoch_info_iter->second;
|
||||
}
|
||||
}
|
||||
if (branching_point) {
|
||||
spdlog::error(
|
||||
"You cannot register Replica {} to this Main because at one point "
|
||||
"Replica {} acted as the Main instance. Both the Main and Replica {} "
|
||||
"now hold unique data. Please resolve data conflicts and start the "
|
||||
"replication on a clean instance.",
|
||||
name_, name_, name_);
|
||||
return;
|
||||
}
|
||||
|
||||
current_commit_timestamp = replica.current_commit_timestamp;
|
||||
spdlog::trace("Current timestamp on replica {}: {}", name_, current_commit_timestamp);
|
||||
spdlog::trace("Current timestamp on main: {}", storage_->replication_state_.last_commit_timestamp_.load());
|
||||
if (current_commit_timestamp == storage_->replication_state_.last_commit_timestamp_.load()) {
|
||||
spdlog::debug("Replica '{}' up to date", name_);
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::READY);
|
||||
} else {
|
||||
spdlog::debug("Replica '{}' is behind", name_);
|
||||
{
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::RECOVERY);
|
||||
}
|
||||
thread_pool_.AddTask([=, this] { this->RecoverReplica(current_commit_timestamp); });
|
||||
}
|
||||
}
|
||||
void InMemoryReplicationClient::TryInitializeClientSync() {
|
||||
try {
|
||||
InitializeClient();
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
std::unique_lock client_guarde{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
spdlog::error(utils::MessageWithLink("Failed to connect to replica {} at the endpoint {}.", name_,
|
||||
rpc_client_.Endpoint(), "https://memgr.ph/replication"));
|
||||
}
|
||||
}
|
||||
void InMemoryReplicationClient::HandleRpcFailure() {
|
||||
spdlog::error(utils::MessageWithLink("Couldn't replicate data to {}.", name_, "https://memgr.ph/replication"));
|
||||
TryInitializeClientAsync();
|
||||
}
|
||||
void InMemoryReplicationClient::StartTransactionReplication(const uint64_t current_wal_seq_num) {
|
||||
std::unique_lock guard(client_lock_);
|
||||
const auto status = replica_state_.load();
|
||||
switch (status) {
|
||||
case replication::ReplicaState::RECOVERY:
|
||||
spdlog::debug("Replica {} is behind MAIN instance", name_);
|
||||
return;
|
||||
case replication::ReplicaState::REPLICATING:
|
||||
spdlog::debug("Replica {} missed a transaction", name_);
|
||||
// We missed a transaction because we're still replicating
|
||||
// the previous transaction so we need to go to RECOVERY
|
||||
// state to catch up with the missing transaction
|
||||
// We cannot queue the recovery process here because
|
||||
// an error can happen while we're replicating the previous
|
||||
// transaction after which the client should go to
|
||||
// INVALID state before starting the recovery process
|
||||
replica_state_.store(replication::ReplicaState::RECOVERY);
|
||||
return;
|
||||
case replication::ReplicaState::INVALID:
|
||||
HandleRpcFailure();
|
||||
return;
|
||||
case replication::ReplicaState::READY:
|
||||
MG_ASSERT(!replica_stream_);
|
||||
try {
|
||||
replica_stream_.emplace(
|
||||
ReplicaStream{this, storage_->replication_state_.last_commit_timestamp_.load(), current_wal_seq_num});
|
||||
replica_state_.store(replication::ReplicaState::REPLICATING);
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
HandleRpcFailure();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
void InMemoryReplicationClient::IfStreamingTransaction(const std::function<void(ReplicaStream &)> &callback) {
|
||||
// We can only check the state because it guarantees to be only
|
||||
// valid during a single transaction replication (if the assumption
|
||||
// that this and other transaction replication functions can only be
|
||||
// called from a one thread stands)
|
||||
if (replica_state_ != replication::ReplicaState::REPLICATING) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callback(*replica_stream_);
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
{
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
}
|
||||
HandleRpcFailure();
|
||||
}
|
||||
}
|
||||
bool InMemoryReplicationClient::FinalizeTransactionReplication() {
|
||||
// We can only check the state because it guarantees to be only
|
||||
// valid during a single transaction replication (if the assumption
|
||||
// that this and other transaction replication functions can only be
|
||||
// called from a one thread stands)
|
||||
if (replica_state_ != replication::ReplicaState::REPLICATING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode_ == replication::ReplicationMode::ASYNC) {
|
||||
thread_pool_.AddTask([this] { static_cast<void>(this->FinalizeTransactionReplicationInternal()); });
|
||||
return true;
|
||||
}
|
||||
|
||||
return FinalizeTransactionReplicationInternal();
|
||||
}
|
||||
bool InMemoryReplicationClient::FinalizeTransactionReplicationInternal() {
|
||||
MG_ASSERT(replica_stream_, "Missing stream for transaction deltas");
|
||||
try {
|
||||
auto response = replica_stream_->Finalize();
|
||||
replica_stream_.reset();
|
||||
std::unique_lock client_guard(client_lock_);
|
||||
if (!response.success || replica_state_ == replication::ReplicaState::RECOVERY) {
|
||||
replica_state_.store(replication::ReplicaState::RECOVERY);
|
||||
thread_pool_.AddTask([&, this] { this->RecoverReplica(response.current_commit_timestamp); });
|
||||
} else {
|
||||
replica_state_.store(replication::ReplicaState::READY);
|
||||
return true;
|
||||
}
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
replica_stream_.reset();
|
||||
{
|
||||
std::unique_lock client_guard(client_lock_);
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
}
|
||||
HandleRpcFailure();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void InMemoryReplicationClient::RecoverReplica(uint64_t replica_commit) {
|
||||
spdlog::debug("Starting replica recover");
|
||||
while (true) {
|
||||
auto file_locker = storage_->file_retainer_.AddLocker();
|
||||
|
||||
const auto steps = GetRecoverySteps(replica_commit, &file_locker);
|
||||
int i = 0;
|
||||
for (const auto &recovery_step : steps) {
|
||||
spdlog::trace("Recovering in step: {}", i++);
|
||||
try {
|
||||
std::visit(
|
||||
[&, this]<typename T>(T &&arg) {
|
||||
using StepType = std::remove_cvref_t<T>;
|
||||
if constexpr (std::is_same_v<StepType, RecoverySnapshot>) {
|
||||
spdlog::debug("Sending the latest snapshot file: {}", arg);
|
||||
auto response = TransferSnapshot(arg);
|
||||
replica_commit = response.current_commit_timestamp;
|
||||
} else if constexpr (std::is_same_v<StepType, RecoveryWals>) {
|
||||
spdlog::debug("Sending the latest wal files");
|
||||
auto response = TransferWalFiles(arg);
|
||||
replica_commit = response.current_commit_timestamp;
|
||||
spdlog::debug("Wal files successfully transferred.");
|
||||
} else if constexpr (std::is_same_v<StepType, RecoveryCurrentWal>) {
|
||||
std::unique_lock transaction_guard(storage_->engine_lock_);
|
||||
if (storage_->wal_file_ && storage_->wal_file_->SequenceNumber() == arg.current_wal_seq_num) {
|
||||
storage_->wal_file_->DisableFlushing();
|
||||
transaction_guard.unlock();
|
||||
spdlog::debug("Sending current wal file");
|
||||
replica_commit = ReplicateCurrentWal();
|
||||
storage_->wal_file_->EnableFlushing();
|
||||
} else {
|
||||
spdlog::debug("Cannot recover using current wal file");
|
||||
}
|
||||
} else {
|
||||
static_assert(always_false_v<T>, "Missing type from variant visitor");
|
||||
}
|
||||
},
|
||||
recovery_step);
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
{
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
}
|
||||
HandleRpcFailure();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
spdlog::trace("Current timestamp on replica: {}", replica_commit);
|
||||
// To avoid the situation where we read a correct commit timestamp in
|
||||
// one thread, and after that another thread commits a different a
|
||||
// transaction and THEN we set the state to READY in the first thread,
|
||||
// we set this lock before checking the timestamp.
|
||||
// We will detect that the state is invalid during the next commit,
|
||||
// because replication::AppendDeltasRpc sends the last commit timestamp which
|
||||
// replica checks if it's the same last commit timestamp it received
|
||||
// and we will go to recovery.
|
||||
// By adding this lock, we can avoid that, and go to RECOVERY immediately.
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
SPDLOG_INFO("Replica timestamp: {}", replica_commit);
|
||||
SPDLOG_INFO("Last commit: {}", storage_->replication_state_.last_commit_timestamp_);
|
||||
if (storage_->replication_state_.last_commit_timestamp_.load() == replica_commit) {
|
||||
replica_state_.store(replication::ReplicaState::READY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint64_t InMemoryReplicationClient::ReplicateCurrentWal() {
|
||||
const auto &wal_file = storage_->wal_file_;
|
||||
auto stream = CurrentWalHandler{this};
|
||||
stream.AppendFilename(wal_file->Path().filename());
|
||||
utils::InputFile file;
|
||||
MG_ASSERT(file.Open(storage_->wal_file_->Path()), "Failed to open current WAL file!");
|
||||
const auto [buffer, buffer_size] = wal_file->CurrentFileBuffer();
|
||||
stream.AppendSize(file.GetSize() + buffer_size);
|
||||
stream.AppendFileData(&file);
|
||||
stream.AppendBufferData(buffer, buffer_size);
|
||||
auto response = stream.Finalize();
|
||||
return response.current_commit_timestamp;
|
||||
} /// This method tries to find the optimal path for recoverying a single replica.
|
||||
/// Based on the last commit transfered to replica it tries to update the
|
||||
/// replica using durability files - WALs and Snapshots. WAL files are much
|
||||
/// smaller in size as they contain only the Deltas (changes) made during the
|
||||
/// transactions while Snapshots contain all the data. For that reason we prefer
|
||||
/// WALs as much as possible. As the WAL file that is currently being updated
|
||||
/// can change during the process we ignore it as much as possible. Also, it
|
||||
/// uses the transaction lock so locking it can be really expensive. After we
|
||||
/// fetch the list of finalized WALs, we try to find the longest chain of
|
||||
/// sequential WALs, starting from the latest one, that will update the recovery
|
||||
/// with the all missed updates. If the WAL chain cannot be created, replica is
|
||||
/// behind by a lot, so we use the regular recovery process, we send the latest
|
||||
/// snapshot and all the necessary WAL files, starting from the newest WAL that
|
||||
/// contains a timestamp before the snapshot. If we registered the existence of
|
||||
/// the current WAL, we add the sequence number we read from it to the recovery
|
||||
/// process. After all the other steps are finished, if the current WAL contains
|
||||
/// the same sequence number, it's the same WAL we read while fetching the
|
||||
/// recovery steps, so we can safely send it to the replica.
|
||||
/// We assume that the property of preserving at least 1 WAL before the snapshot
|
||||
/// is satisfied as we extract the timestamp information from it.
|
||||
std::vector<InMemoryReplicationClient::RecoveryStep> InMemoryReplicationClient::GetRecoverySteps(
|
||||
const uint64_t replica_commit, utils::FileRetainer::FileLocker *file_locker) {
|
||||
// First check if we can recover using the current wal file only
|
||||
// otherwise save the seq_num of the current wal file
|
||||
// This lock is also necessary to force the missed transaction to finish.
|
||||
std::optional<uint64_t> current_wal_seq_num;
|
||||
std::optional<uint64_t> current_wal_from_timestamp;
|
||||
if (std::unique_lock transtacion_guard(storage_->engine_lock_); storage_->wal_file_) {
|
||||
current_wal_seq_num.emplace(storage_->wal_file_->SequenceNumber());
|
||||
current_wal_from_timestamp.emplace(storage_->wal_file_->FromTimestamp());
|
||||
}
|
||||
|
||||
auto locker_acc = file_locker->Access();
|
||||
auto wal_files = durability::GetWalFiles(storage_->wal_directory_, storage_->uuid_, current_wal_seq_num);
|
||||
MG_ASSERT(wal_files, "Wal files could not be loaded");
|
||||
|
||||
auto snapshot_files = durability::GetSnapshotFiles(storage_->snapshot_directory_, storage_->uuid_);
|
||||
std::optional<durability::SnapshotDurabilityInfo> latest_snapshot;
|
||||
if (!snapshot_files.empty()) {
|
||||
std::sort(snapshot_files.begin(), snapshot_files.end());
|
||||
latest_snapshot.emplace(std::move(snapshot_files.back()));
|
||||
}
|
||||
|
||||
std::vector<RecoveryStep> recovery_steps;
|
||||
|
||||
// No finalized WAL files were found. This means the difference is contained
|
||||
// inside the current WAL or the snapshot.
|
||||
if (wal_files->empty()) {
|
||||
if (current_wal_from_timestamp && replica_commit >= *current_wal_from_timestamp) {
|
||||
MG_ASSERT(current_wal_seq_num);
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
// Without the finalized WAL containing the current timestamp of replica,
|
||||
// we cannot know if the difference is only in the current WAL or we need
|
||||
// to send the snapshot.
|
||||
if (latest_snapshot) {
|
||||
const auto lock_success = locker_acc.AddPath(latest_snapshot->path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
recovery_steps.emplace_back(std::in_place_type_t<RecoverySnapshot>{}, std::move(latest_snapshot->path));
|
||||
}
|
||||
// if there are no finalized WAL files, snapshot left the current WAL
|
||||
// as the WAL file containing a transaction before snapshot creation
|
||||
// so we can be sure that the current WAL is present
|
||||
MG_ASSERT(current_wal_seq_num);
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
// Find the longest chain of WALs for recovery.
|
||||
// The chain consists ONLY of sequential WALs.
|
||||
auto rwal_it = wal_files->rbegin();
|
||||
|
||||
// if the last finalized WAL is before the replica commit
|
||||
// then we can recovery only from current WAL
|
||||
if (rwal_it->to_timestamp <= replica_commit) {
|
||||
MG_ASSERT(current_wal_seq_num);
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
uint64_t previous_seq_num{rwal_it->seq_num};
|
||||
for (; rwal_it != wal_files->rend(); ++rwal_it) {
|
||||
// If the difference between two consecutive wal files is not 0 or 1
|
||||
// we have a missing WAL in our chain
|
||||
if (previous_seq_num - rwal_it->seq_num > 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Find first WAL that contains up to replica commit, i.e. WAL
|
||||
// that is before the replica commit or conatins the replica commit
|
||||
// as the last committed transaction OR we managed to find the first WAL
|
||||
// file.
|
||||
if (replica_commit >= rwal_it->from_timestamp || rwal_it->seq_num == 0) {
|
||||
if (replica_commit >= rwal_it->to_timestamp) {
|
||||
// We want the WAL after because the replica already contains all the
|
||||
// commits from this WAL
|
||||
--rwal_it;
|
||||
}
|
||||
std::vector<std::filesystem::path> wal_chain;
|
||||
auto distance_from_first = std::distance(rwal_it, wal_files->rend() - 1);
|
||||
// We have managed to create WAL chain
|
||||
// We need to lock these files and add them to the chain
|
||||
for (auto result_wal_it = wal_files->begin() + distance_from_first; result_wal_it != wal_files->end();
|
||||
++result_wal_it) {
|
||||
const auto lock_success = locker_acc.AddPath(result_wal_it->path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
wal_chain.push_back(std::move(result_wal_it->path));
|
||||
}
|
||||
|
||||
recovery_steps.emplace_back(std::in_place_type_t<RecoveryWals>{}, std::move(wal_chain));
|
||||
|
||||
if (current_wal_seq_num) {
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
}
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
previous_seq_num = rwal_it->seq_num;
|
||||
}
|
||||
|
||||
MG_ASSERT(latest_snapshot, "Invalid durability state, missing snapshot");
|
||||
// We didn't manage to find a WAL chain, we need to send the latest snapshot
|
||||
// with its WALs
|
||||
const auto lock_success = locker_acc.AddPath(latest_snapshot->path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
recovery_steps.emplace_back(std::in_place_type_t<RecoverySnapshot>{}, std::move(latest_snapshot->path));
|
||||
|
||||
std::vector<std::filesystem::path> recovery_wal_files;
|
||||
auto wal_it = wal_files->begin();
|
||||
for (; wal_it != wal_files->end(); ++wal_it) {
|
||||
// Assuming recovery process is correct the snashpot should
|
||||
// always retain a single WAL that contains a transaction
|
||||
// before its creation
|
||||
if (latest_snapshot->start_timestamp < wal_it->to_timestamp) {
|
||||
if (latest_snapshot->start_timestamp < wal_it->from_timestamp) {
|
||||
MG_ASSERT(wal_it != wal_files->begin(), "Invalid durability files state");
|
||||
--wal_it;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (; wal_it != wal_files->end(); ++wal_it) {
|
||||
const auto lock_success = locker_acc.AddPath(wal_it->path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
recovery_wal_files.push_back(std::move(wal_it->path));
|
||||
}
|
||||
|
||||
// We only have a WAL before the snapshot
|
||||
if (recovery_wal_files.empty()) {
|
||||
const auto lock_success = locker_acc.AddPath(wal_files->back().path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
recovery_wal_files.push_back(std::move(wal_files->back().path));
|
||||
}
|
||||
|
||||
recovery_steps.emplace_back(std::in_place_type_t<RecoveryWals>{}, std::move(recovery_wal_files));
|
||||
|
||||
if (current_wal_seq_num) {
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
}
|
||||
|
||||
return recovery_steps;
|
||||
}
|
||||
TimestampInfo InMemoryReplicationClient::GetTimestampInfo() {
|
||||
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_->replication_state_.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;
|
||||
}
|
||||
std::string const &InMemoryReplicationClient::GetEpochId() const { return storage_->replication_state_.GetEpoch().id; }
|
||||
auto InMemoryReplicationClient::GetStorage() -> Storage * { return storage_; }
|
||||
|
||||
replication::SnapshotRes InMemoryReplicationClient::TransferSnapshot(const std::filesystem::path &path) {
|
||||
auto stream{rpc_client_.Stream<replication::SnapshotRpc>()};
|
||||
replication::Encoder encoder(stream.GetBuilder());
|
||||
encoder.WriteFile(path);
|
||||
return stream.AwaitResponse();
|
||||
}
|
||||
|
||||
replication::WalFilesRes InMemoryReplicationClient::TransferWalFiles(
|
||||
const std::vector<std::filesystem::path> &wal_files) {
|
||||
MG_ASSERT(!wal_files.empty(), "Wal files list is empty!");
|
||||
auto stream{rpc_client_.Stream<replication::WalFilesRpc>(wal_files.size())};
|
||||
replication::Encoder encoder(stream.GetBuilder());
|
||||
for (const auto &wal : wal_files) {
|
||||
spdlog::debug("Sending wal file: {}", wal);
|
||||
encoder.WriteFile(wal);
|
||||
}
|
||||
|
||||
return stream.AwaitResponse();
|
||||
}
|
||||
|
||||
} // namespace memgraph::storage
|
||||
68
src/storage/v2/inmemory/replication/replication_client.hpp
Normal file
68
src/storage/v2/inmemory/replication/replication_client.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
#include "storage/v2/replication/replication_client.hpp"
|
||||
namespace memgraph::storage {
|
||||
|
||||
class InMemoryReplicationClient : public ReplicationClient {
|
||||
public:
|
||||
InMemoryReplicationClient(InMemoryStorage *storage, std::string name, io::network::Endpoint endpoint,
|
||||
replication::ReplicationMode mode, const replication::ReplicationClientConfig &config = {});
|
||||
void StartTransactionReplication(uint64_t current_wal_seq_num) override;
|
||||
// Replication clients can be removed at any point
|
||||
// so to avoid any complexity of checking if the client was removed whenever
|
||||
// we want to send part of transaction and to avoid adding some GC logic this
|
||||
// function will run a callback if, after previously callling
|
||||
// StartTransactionReplication, stream is created.
|
||||
void IfStreamingTransaction(const std::function<void(ReplicaStream &)> &callback) override;
|
||||
|
||||
auto GetEpochId() const -> std::string const & override;
|
||||
|
||||
auto GetStorage() -> Storage * override;
|
||||
|
||||
void Start() override;
|
||||
|
||||
// Return whether the transaction could be finalized on the replication client or not.
|
||||
[[nodiscard]] bool FinalizeTransactionReplication() override;
|
||||
TimestampInfo GetTimestampInfo() override;
|
||||
|
||||
private:
|
||||
void TryInitializeClientAsync();
|
||||
void FrequentCheck();
|
||||
void InitializeClient();
|
||||
void TryInitializeClientSync();
|
||||
void HandleRpcFailure();
|
||||
[[nodiscard]] bool FinalizeTransactionReplicationInternal();
|
||||
void RecoverReplica(uint64_t replica_commit);
|
||||
uint64_t ReplicateCurrentWal();
|
||||
|
||||
using RecoverySnapshot = std::filesystem::path;
|
||||
using RecoveryWals = std::vector<std::filesystem::path>;
|
||||
struct RecoveryCurrentWal {
|
||||
explicit RecoveryCurrentWal(const uint64_t current_wal_seq_num) : current_wal_seq_num(current_wal_seq_num) {}
|
||||
uint64_t current_wal_seq_num;
|
||||
};
|
||||
using RecoveryStep = std::variant<RecoverySnapshot, RecoveryWals, RecoveryCurrentWal>;
|
||||
std::vector<RecoveryStep> GetRecoverySteps(uint64_t replica_commit, utils::FileRetainer::FileLocker *file_locker);
|
||||
|
||||
// Transfer the snapshot file.
|
||||
// @param path Path of the snapshot file.
|
||||
replication::SnapshotRes TransferSnapshot(const std::filesystem::path &path);
|
||||
|
||||
// Transfer the WAL files
|
||||
replication::WalFilesRes TransferWalFiles(const std::vector<std::filesystem::path> &wal_files);
|
||||
|
||||
InMemoryStorage *storage_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
575
src/storage/v2/inmemory/replication/replication_server.cpp
Normal file
575
src/storage/v2/inmemory/replication/replication_server.cpp
Normal file
@@ -0,0 +1,575 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#include "storage/v2/inmemory/replication/replication_server.hpp"
|
||||
#include "storage/v2/durability/durability.hpp"
|
||||
#include "storage/v2/durability/snapshot.hpp"
|
||||
#include "storage/v2/durability/version.hpp"
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
namespace {
|
||||
std::pair<uint64_t, durability::WalDeltaData> ReadDelta(durability::BaseDecoder *decoder) {
|
||||
try {
|
||||
auto timestamp = ReadWalDeltaHeader(decoder);
|
||||
SPDLOG_INFO(" Timestamp {}", timestamp);
|
||||
auto delta = ReadWalDeltaData(decoder);
|
||||
return {timestamp, delta};
|
||||
} catch (const slk::SlkReaderException &) {
|
||||
throw utils::BasicException("Missing data!");
|
||||
} catch (const durability::RecoveryFailure &) {
|
||||
throw utils::BasicException("Invalid data!");
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
InMemoryReplicationServer::InMemoryReplicationServer(InMemoryStorage *storage, memgraph::io::network::Endpoint endpoint,
|
||||
const replication::ReplicationServerConfig &config)
|
||||
: ReplicationServer{std::move(endpoint), config}, storage_(storage) {
|
||||
rpc_server_.Register<replication::HeartbeatRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received HeartbeatRpc");
|
||||
this->HeartbeatHandler(req_reader, res_builder);
|
||||
});
|
||||
|
||||
rpc_server_.Register<replication::AppendDeltasRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received AppendDeltasRpc");
|
||||
this->AppendDeltasHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_.Register<replication::SnapshotRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received SnapshotRpc");
|
||||
this->SnapshotHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_.Register<replication::WalFilesRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received WalFilesRpc");
|
||||
this->WalFilesHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_.Register<replication::CurrentWalRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
void InMemoryReplicationServer::HeartbeatHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::HeartbeatReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
replication::HeartbeatRes res{true, storage_->replication_state_.last_commit_timestamp_.load(),
|
||||
storage_->replication_state_.GetEpoch().id};
|
||||
slk::Save(res, res_builder);
|
||||
}
|
||||
|
||||
void InMemoryReplicationServer::AppendDeltasHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::AppendDeltasReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
replication::Decoder decoder(req_reader);
|
||||
|
||||
auto maybe_epoch_id = decoder.ReadString();
|
||||
MG_ASSERT(maybe_epoch_id, "Invalid replication message");
|
||||
|
||||
if (*maybe_epoch_id != storage_->replication_state_.GetEpoch().id) {
|
||||
storage_->replication_state_.AppendEpoch(*maybe_epoch_id);
|
||||
}
|
||||
|
||||
if (storage_->wal_file_) {
|
||||
if (req.seq_num > storage_->wal_file_->SequenceNumber() ||
|
||||
*maybe_epoch_id != storage_->replication_state_.GetEpoch().id) {
|
||||
storage_->wal_file_->FinalizeWal();
|
||||
storage_->wal_file_.reset();
|
||||
storage_->wal_seq_num_ = req.seq_num;
|
||||
spdlog::trace("Finalized WAL file");
|
||||
} else {
|
||||
MG_ASSERT(storage_->wal_file_->SequenceNumber() == req.seq_num, "Invalid sequence number of current wal file");
|
||||
storage_->wal_seq_num_ = req.seq_num + 1;
|
||||
}
|
||||
} else {
|
||||
storage_->wal_seq_num_ = req.seq_num;
|
||||
}
|
||||
|
||||
if (req.previous_commit_timestamp != storage_->replication_state_.last_commit_timestamp_.load()) {
|
||||
// Empty the stream
|
||||
bool transaction_complete = false;
|
||||
while (!transaction_complete) {
|
||||
SPDLOG_INFO("Skipping delta");
|
||||
const auto [timestamp, delta] = ReadDelta(&decoder);
|
||||
transaction_complete = durability::IsWalDeltaDataTypeTransactionEnd(delta.type);
|
||||
}
|
||||
|
||||
replication::AppendDeltasRes res{false, storage_->replication_state_.last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
return;
|
||||
}
|
||||
|
||||
ReadAndApplyDelta(storage_, &decoder);
|
||||
|
||||
replication::AppendDeltasRes res{true, storage_->replication_state_.last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
spdlog::debug("Replication recovery from append deltas finished, replica is now up to date!");
|
||||
}
|
||||
|
||||
void InMemoryReplicationServer::SnapshotHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::SnapshotReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
replication::Decoder decoder(req_reader);
|
||||
|
||||
utils::EnsureDirOrDie(storage_->snapshot_directory_);
|
||||
|
||||
const auto maybe_snapshot_path = decoder.ReadFile(storage_->snapshot_directory_);
|
||||
MG_ASSERT(maybe_snapshot_path, "Failed to load snapshot!");
|
||||
spdlog::info("Received snapshot saved to {}", *maybe_snapshot_path);
|
||||
|
||||
std::unique_lock<utils::RWLock> storage_guard(storage_->main_lock_);
|
||||
spdlog::trace("Clearing database since recovering from snapshot.");
|
||||
// Clear the database
|
||||
storage_->vertices_.clear();
|
||||
storage_->edges_.clear();
|
||||
|
||||
storage_->constraints_.existence_constraints_ = std::make_unique<ExistenceConstraints>();
|
||||
storage_->constraints_.unique_constraints_ = std::make_unique<InMemoryUniqueConstraints>();
|
||||
storage_->indices_.label_index_ =
|
||||
std::make_unique<InMemoryLabelIndex>(&storage_->indices_, &storage_->constraints_, storage_->config_);
|
||||
storage_->indices_.label_property_index_ =
|
||||
std::make_unique<InMemoryLabelPropertyIndex>(&storage_->indices_, &storage_->constraints_, storage_->config_);
|
||||
try {
|
||||
spdlog::debug("Loading snapshot");
|
||||
auto &epoch =
|
||||
storage_->replication_state_
|
||||
.GetEpoch(); // This needs to be a non-const ref since we are updating it in LoadSnapshot TODO fix
|
||||
auto recovered_snapshot = durability::LoadSnapshot(
|
||||
*maybe_snapshot_path, &storage_->vertices_, &storage_->edges_, &storage_->replication_state_.history,
|
||||
storage_->name_id_mapper_.get(), &storage_->edge_count_, storage_->config_);
|
||||
spdlog::debug("Snapshot loaded successfully");
|
||||
// If this step is present it should always be the first step of
|
||||
// the recovery so we use the UUID we read from snasphost
|
||||
storage_->uuid_ = std::move(recovered_snapshot.snapshot_info.uuid);
|
||||
epoch.id = std::move(recovered_snapshot.snapshot_info.epoch_id);
|
||||
const auto &recovery_info = recovered_snapshot.recovery_info;
|
||||
storage_->vertex_id_ = recovery_info.next_vertex_id;
|
||||
storage_->edge_id_ = recovery_info.next_edge_id;
|
||||
storage_->timestamp_ = std::max(storage_->timestamp_, recovery_info.next_timestamp);
|
||||
|
||||
spdlog::trace("Recovering indices and constraints from snapshot.");
|
||||
durability::RecoverIndicesAndConstraints(recovered_snapshot.indices_constraints, &storage_->indices_,
|
||||
&storage_->constraints_, &storage_->vertices_);
|
||||
} catch (const durability::RecoveryFailure &e) {
|
||||
LOG_FATAL("Couldn't load the snapshot because of: {}", e.what());
|
||||
}
|
||||
storage_guard.unlock();
|
||||
|
||||
replication::SnapshotRes res{true, storage_->replication_state_.last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
|
||||
spdlog::trace("Deleting old snapshot files due to snapshot recovery.");
|
||||
// Delete other durability files
|
||||
auto snapshot_files = durability::GetSnapshotFiles(storage_->snapshot_directory_, storage_->uuid_);
|
||||
for (const auto &[path, uuid, _] : snapshot_files) {
|
||||
if (path != *maybe_snapshot_path) {
|
||||
spdlog::trace("Deleting snapshot file {}", path);
|
||||
storage_->file_retainer_.DeleteFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
spdlog::trace("Deleting old WAL files due to snapshot recovery.");
|
||||
auto wal_files = durability::GetWalFiles(storage_->wal_directory_, storage_->uuid_);
|
||||
if (wal_files) {
|
||||
for (const auto &wal_file : *wal_files) {
|
||||
spdlog::trace("Deleting WAL file {}", wal_file.path);
|
||||
storage_->file_retainer_.DeleteFile(wal_file.path);
|
||||
}
|
||||
|
||||
storage_->wal_file_.reset();
|
||||
}
|
||||
spdlog::debug("Replication recovery from snapshot finished!");
|
||||
}
|
||||
|
||||
void InMemoryReplicationServer::WalFilesHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::WalFilesReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
const auto wal_file_number = req.file_number;
|
||||
spdlog::debug("Received WAL files: {}", wal_file_number);
|
||||
|
||||
replication::Decoder decoder(req_reader);
|
||||
|
||||
utils::EnsureDirOrDie(storage_->wal_directory_);
|
||||
|
||||
for (auto i = 0; i < wal_file_number; ++i) {
|
||||
LoadWal(storage_, &decoder);
|
||||
}
|
||||
|
||||
replication::WalFilesRes res{true, storage_->replication_state_.last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
spdlog::debug("Replication recovery from WAL files ended successfully, replica is now up to date!");
|
||||
}
|
||||
|
||||
void InMemoryReplicationServer::CurrentWalHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::CurrentWalReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
replication::Decoder decoder(req_reader);
|
||||
|
||||
utils::EnsureDirOrDie(storage_->wal_directory_);
|
||||
|
||||
LoadWal(storage_, &decoder);
|
||||
|
||||
replication::CurrentWalRes res{true, storage_->replication_state_.last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
spdlog::debug("Replication recovery from current WAL ended successfully, replica is now up to date!");
|
||||
}
|
||||
|
||||
void InMemoryReplicationServer::LoadWal(InMemoryStorage *storage, replication::Decoder *decoder) {
|
||||
const auto temp_wal_directory = std::filesystem::temp_directory_path() / "memgraph" / durability::kWalDirectory;
|
||||
utils::EnsureDir(temp_wal_directory);
|
||||
auto maybe_wal_path = decoder->ReadFile(temp_wal_directory);
|
||||
MG_ASSERT(maybe_wal_path, "Failed to load WAL!");
|
||||
spdlog::trace("Received WAL saved to {}", *maybe_wal_path);
|
||||
try {
|
||||
auto wal_info = durability::ReadWalInfo(*maybe_wal_path);
|
||||
if (wal_info.seq_num == 0) {
|
||||
storage->uuid_ = wal_info.uuid;
|
||||
}
|
||||
|
||||
if (wal_info.epoch_id != storage->replication_state_.GetEpoch().id) {
|
||||
storage->replication_state_.AppendEpoch(wal_info.epoch_id);
|
||||
}
|
||||
|
||||
if (storage->wal_file_) {
|
||||
if (storage->wal_file_->SequenceNumber() != wal_info.seq_num) {
|
||||
storage->wal_file_->FinalizeWal();
|
||||
storage->wal_seq_num_ = wal_info.seq_num;
|
||||
storage->wal_file_.reset();
|
||||
spdlog::trace("WAL file {} finalized successfully", *maybe_wal_path);
|
||||
}
|
||||
} else {
|
||||
storage->wal_seq_num_ = wal_info.seq_num;
|
||||
}
|
||||
spdlog::trace("Loading WAL deltas from {}", *maybe_wal_path);
|
||||
durability::Decoder wal;
|
||||
const auto version = wal.Initialize(*maybe_wal_path, durability::kWalMagic);
|
||||
spdlog::debug("WAL file {} loaded successfully", *maybe_wal_path);
|
||||
if (!version) throw durability::RecoveryFailure("Couldn't read WAL magic and/or version!");
|
||||
if (!durability::IsVersionSupported(*version)) throw durability::RecoveryFailure("Invalid WAL version!");
|
||||
wal.SetPosition(wal_info.offset_deltas);
|
||||
|
||||
for (size_t i = 0; i < wal_info.num_deltas;) {
|
||||
i += ReadAndApplyDelta(storage, &wal);
|
||||
}
|
||||
|
||||
spdlog::debug("Replication from current WAL successful!");
|
||||
} catch (const durability::RecoveryFailure &e) {
|
||||
LOG_FATAL("Couldn't recover WAL deltas from {} because of: {}", *maybe_wal_path, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void InMemoryReplicationServer::TimestampHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::TimestampReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
replication::TimestampRes res{true, storage_->replication_state_.last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
}
|
||||
|
||||
uint64_t InMemoryReplicationServer::ReadAndApplyDelta(InMemoryStorage *storage, durability::BaseDecoder *decoder) {
|
||||
auto edge_acc = storage->edges_.access();
|
||||
auto vertex_acc = storage->vertices_.access();
|
||||
|
||||
std::optional<std::pair<uint64_t, InMemoryStorage::ReplicationAccessor>> commit_timestamp_and_accessor;
|
||||
auto get_transaction = [storage, &commit_timestamp_and_accessor](uint64_t commit_timestamp) {
|
||||
if (!commit_timestamp_and_accessor) {
|
||||
auto acc = storage->Access(std::nullopt);
|
||||
auto inmem_acc = std::unique_ptr<InMemoryStorage::InMemoryAccessor>(
|
||||
static_cast<InMemoryStorage::InMemoryAccessor *>(acc.release()));
|
||||
commit_timestamp_and_accessor.emplace(commit_timestamp, std::move(*inmem_acc));
|
||||
} else if (commit_timestamp_and_accessor->first != commit_timestamp) {
|
||||
throw utils::BasicException("Received more than one transaction!");
|
||||
}
|
||||
return &commit_timestamp_and_accessor->second;
|
||||
};
|
||||
|
||||
uint64_t applied_deltas = 0;
|
||||
auto max_commit_timestamp = storage->replication_state_.last_commit_timestamp_.load();
|
||||
|
||||
for (bool transaction_complete = false; !transaction_complete; ++applied_deltas) {
|
||||
const auto [timestamp, delta] = ReadDelta(decoder);
|
||||
if (timestamp > max_commit_timestamp) {
|
||||
max_commit_timestamp = timestamp;
|
||||
}
|
||||
|
||||
transaction_complete = durability::IsWalDeltaDataTypeTransactionEnd(delta.type);
|
||||
|
||||
if (timestamp < storage->timestamp_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SPDLOG_INFO(" Delta {}", applied_deltas);
|
||||
switch (delta.type) {
|
||||
case durability::WalDeltaData::Type::VERTEX_CREATE: {
|
||||
spdlog::trace(" Create vertex {}", delta.vertex_create_delete.gid.AsUint());
|
||||
auto *transaction = get_transaction(timestamp);
|
||||
transaction->CreateVertexEx(delta.vertex_create_delete.gid);
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::VERTEX_DELETE: {
|
||||
spdlog::trace(" Delete vertex {}", delta.vertex_create_delete.gid.AsUint());
|
||||
auto *transaction = get_transaction(timestamp);
|
||||
auto vertex = transaction->FindVertex(delta.vertex_create_delete.gid, View::NEW);
|
||||
if (!vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = transaction->DeleteVertex(&*vertex);
|
||||
if (ret.HasError() || !ret.GetValue()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::VERTEX_ADD_LABEL: {
|
||||
spdlog::trace(" Vertex {} add label {}", delta.vertex_add_remove_label.gid.AsUint(),
|
||||
delta.vertex_add_remove_label.label);
|
||||
auto *transaction = get_transaction(timestamp);
|
||||
auto vertex = transaction->FindVertex(delta.vertex_add_remove_label.gid, View::NEW);
|
||||
if (!vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = vertex->AddLabel(transaction->NameToLabel(delta.vertex_add_remove_label.label));
|
||||
if (ret.HasError() || !ret.GetValue()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::VERTEX_REMOVE_LABEL: {
|
||||
spdlog::trace(" Vertex {} remove label {}", delta.vertex_add_remove_label.gid.AsUint(),
|
||||
delta.vertex_add_remove_label.label);
|
||||
auto *transaction = get_transaction(timestamp);
|
||||
auto vertex = transaction->FindVertex(delta.vertex_add_remove_label.gid, View::NEW);
|
||||
if (!vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = vertex->RemoveLabel(transaction->NameToLabel(delta.vertex_add_remove_label.label));
|
||||
if (ret.HasError() || !ret.GetValue()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::VERTEX_SET_PROPERTY: {
|
||||
spdlog::trace(" Vertex {} set property {} to {}", delta.vertex_edge_set_property.gid.AsUint(),
|
||||
delta.vertex_edge_set_property.property, delta.vertex_edge_set_property.value);
|
||||
auto *transaction = get_transaction(timestamp);
|
||||
auto vertex = transaction->FindVertex(delta.vertex_edge_set_property.gid, View::NEW);
|
||||
if (!vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = vertex->SetProperty(transaction->NameToProperty(delta.vertex_edge_set_property.property),
|
||||
delta.vertex_edge_set_property.value);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EDGE_CREATE: {
|
||||
spdlog::trace(" Create edge {} of type {} from vertex {} to vertex {}",
|
||||
delta.edge_create_delete.gid.AsUint(), delta.edge_create_delete.edge_type,
|
||||
delta.edge_create_delete.from_vertex.AsUint(), delta.edge_create_delete.to_vertex.AsUint());
|
||||
auto *transaction = get_transaction(timestamp);
|
||||
auto from_vertex = transaction->FindVertex(delta.edge_create_delete.from_vertex, View::NEW);
|
||||
if (!from_vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto to_vertex = transaction->FindVertex(delta.edge_create_delete.to_vertex, View::NEW);
|
||||
if (!to_vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto edge = transaction->CreateEdgeEx(&*from_vertex, &*to_vertex,
|
||||
transaction->NameToEdgeType(delta.edge_create_delete.edge_type),
|
||||
delta.edge_create_delete.gid);
|
||||
if (edge.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EDGE_DELETE: {
|
||||
spdlog::trace(" Delete edge {} of type {} from vertex {} to vertex {}",
|
||||
delta.edge_create_delete.gid.AsUint(), delta.edge_create_delete.edge_type,
|
||||
delta.edge_create_delete.from_vertex.AsUint(), delta.edge_create_delete.to_vertex.AsUint());
|
||||
auto *transaction = get_transaction(timestamp);
|
||||
auto from_vertex = transaction->FindVertex(delta.edge_create_delete.from_vertex, View::NEW);
|
||||
if (!from_vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto to_vertex = transaction->FindVertex(delta.edge_create_delete.to_vertex, View::NEW);
|
||||
if (!to_vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto edges = from_vertex->OutEdges(View::NEW, {transaction->NameToEdgeType(delta.edge_create_delete.edge_type)},
|
||||
&*to_vertex);
|
||||
if (edges.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
if (edges->size() != 1) throw utils::BasicException("Invalid transaction!");
|
||||
auto &edge = (*edges)[0];
|
||||
auto ret = transaction->DeleteEdge(&edge);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EDGE_SET_PROPERTY: {
|
||||
spdlog::trace(" Edge {} set property {} to {}", delta.vertex_edge_set_property.gid.AsUint(),
|
||||
delta.vertex_edge_set_property.property, delta.vertex_edge_set_property.value);
|
||||
if (!storage->config_.items.properties_on_edges)
|
||||
throw utils::BasicException(
|
||||
"Can't set properties on edges because properties on edges "
|
||||
"are disabled!");
|
||||
|
||||
auto *transaction = get_transaction(timestamp);
|
||||
|
||||
// The following block of code effectively implements `FindEdge` and
|
||||
// yields an accessor that is only valid for managing the edge's
|
||||
// properties.
|
||||
auto edge = edge_acc.find(delta.vertex_edge_set_property.gid);
|
||||
if (edge == edge_acc.end()) throw utils::BasicException("Invalid transaction!");
|
||||
// The edge visibility check must be done here manually because we
|
||||
// don't allow direct access to the edges through the public API.
|
||||
{
|
||||
bool is_visible = true;
|
||||
Delta *delta = nullptr;
|
||||
{
|
||||
std::lock_guard<utils::SpinLock> guard(edge->lock);
|
||||
is_visible = !edge->deleted;
|
||||
delta = edge->delta;
|
||||
}
|
||||
ApplyDeltasForRead(&transaction->GetTransaction(), delta, View::NEW, [&is_visible](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
is_visible = true;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
is_visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!is_visible) throw utils::BasicException("Invalid transaction!");
|
||||
}
|
||||
EdgeRef edge_ref(&*edge);
|
||||
// Here we create an edge accessor that we will use to get the
|
||||
// properties of the edge. The accessor is created with an invalid
|
||||
// type and invalid from/to pointers because we don't know them
|
||||
// here, but that isn't an issue because we won't use that part of
|
||||
// the API here.
|
||||
auto ea = EdgeAccessor{edge_ref,
|
||||
EdgeTypeId::FromUint(0UL),
|
||||
nullptr,
|
||||
nullptr,
|
||||
&transaction->GetTransaction(),
|
||||
&storage->indices_,
|
||||
&storage->constraints_,
|
||||
storage->config_.items};
|
||||
|
||||
auto ret = ea.SetProperty(transaction->NameToProperty(delta.vertex_edge_set_property.property),
|
||||
delta.vertex_edge_set_property.value);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
|
||||
case durability::WalDeltaData::Type::TRANSACTION_END: {
|
||||
spdlog::trace(" Transaction end");
|
||||
if (!commit_timestamp_and_accessor || commit_timestamp_and_accessor->first != timestamp)
|
||||
throw utils::BasicException("Invalid data!");
|
||||
auto ret = commit_timestamp_and_accessor->second.Commit(commit_timestamp_and_accessor->first);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
commit_timestamp_and_accessor = std::nullopt;
|
||||
break;
|
||||
}
|
||||
|
||||
case durability::WalDeltaData::Type::LABEL_INDEX_CREATE: {
|
||||
spdlog::trace(" Create label index on :{}", delta.operation_label.label);
|
||||
// Need to send the timestamp
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage->CreateIndex(storage->NameToLabel(delta.operation_label.label), timestamp).HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::LABEL_INDEX_DROP: {
|
||||
spdlog::trace(" Drop label index on :{}", delta.operation_label.label);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage->DropIndex(storage->NameToLabel(delta.operation_label.label), timestamp).HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::LABEL_PROPERTY_INDEX_CREATE: {
|
||||
spdlog::trace(" Create label+property index on :{} ({})", delta.operation_label_property.label,
|
||||
delta.operation_label_property.property);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage
|
||||
->CreateIndex(storage->NameToLabel(delta.operation_label_property.label),
|
||||
storage->NameToProperty(delta.operation_label_property.property), timestamp)
|
||||
.HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::LABEL_PROPERTY_INDEX_DROP: {
|
||||
spdlog::trace(" Drop label+property index on :{} ({})", delta.operation_label_property.label,
|
||||
delta.operation_label_property.property);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage
|
||||
->DropIndex(storage->NameToLabel(delta.operation_label_property.label),
|
||||
storage->NameToProperty(delta.operation_label_property.property), timestamp)
|
||||
.HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE: {
|
||||
spdlog::trace(" Create existence constraint on :{} ({})", delta.operation_label_property.label,
|
||||
delta.operation_label_property.property);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = storage->CreateExistenceConstraint(storage->NameToLabel(delta.operation_label_property.label),
|
||||
storage->NameToProperty(delta.operation_label_property.property),
|
||||
timestamp);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EXISTENCE_CONSTRAINT_DROP: {
|
||||
spdlog::trace(" Drop existence constraint on :{} ({})", delta.operation_label_property.label,
|
||||
delta.operation_label_property.property);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage
|
||||
->DropExistenceConstraint(storage->NameToLabel(delta.operation_label_property.label),
|
||||
storage->NameToProperty(delta.operation_label_property.property), timestamp)
|
||||
.HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE: {
|
||||
std::stringstream ss;
|
||||
utils::PrintIterable(ss, delta.operation_label_properties.properties);
|
||||
spdlog::trace(" Create unique constraint on :{} ({})", delta.operation_label_properties.label, ss.str());
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
std::set<PropertyId> properties;
|
||||
for (const auto &prop : delta.operation_label_properties.properties) {
|
||||
properties.emplace(storage->NameToProperty(prop));
|
||||
}
|
||||
auto ret = storage->CreateUniqueConstraint(storage->NameToLabel(delta.operation_label_properties.label),
|
||||
properties, timestamp);
|
||||
if (!ret.HasValue() || ret.GetValue() != UniqueConstraints::CreationStatus::SUCCESS)
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP: {
|
||||
std::stringstream ss;
|
||||
utils::PrintIterable(ss, delta.operation_label_properties.properties);
|
||||
spdlog::trace(" Drop unique constraint on :{} ({})", delta.operation_label_properties.label, ss.str());
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
std::set<PropertyId> properties;
|
||||
for (const auto &prop : delta.operation_label_properties.properties) {
|
||||
properties.emplace(storage->NameToProperty(prop));
|
||||
}
|
||||
auto ret = storage->DropUniqueConstraint(storage->NameToLabel(delta.operation_label_properties.label),
|
||||
properties, timestamp);
|
||||
if (ret.HasError() || ret.GetValue() != UniqueConstraints::DeletionStatus::SUCCESS)
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid data!");
|
||||
|
||||
storage->replication_state_.last_commit_timestamp_ = max_commit_timestamp;
|
||||
|
||||
spdlog::debug("Applied {} deltas", applied_deltas);
|
||||
return applied_deltas;
|
||||
}
|
||||
|
||||
} // namespace memgraph::storage
|
||||
47
src/storage/v2/inmemory/replication/replication_server.hpp
Normal file
47
src/storage/v2/inmemory/replication/replication_server.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "storage/v2/replication/replication_server.hpp"
|
||||
#include "storage/v2/replication/serialization.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
class InMemoryStorage;
|
||||
|
||||
class InMemoryReplicationServer : public ReplicationServer {
|
||||
public:
|
||||
explicit InMemoryReplicationServer(InMemoryStorage *storage, io::network::Endpoint endpoint,
|
||||
const replication::ReplicationServerConfig &config);
|
||||
|
||||
private:
|
||||
// RPC handlers
|
||||
void HeartbeatHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
|
||||
void AppendDeltasHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
|
||||
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);
|
||||
|
||||
static void LoadWal(InMemoryStorage *storage, replication::Decoder *decoder);
|
||||
|
||||
static uint64_t ReadAndApplyDelta(InMemoryStorage *storage, durability::BaseDecoder *decoder);
|
||||
|
||||
InMemoryStorage *storage_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
@@ -15,45 +15,28 @@
|
||||
#include "storage/v2/durability/snapshot.hpp"
|
||||
#include "storage/v2/durability/wal.hpp"
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
#include "storage/v2/edge_direction.hpp"
|
||||
#include "storage/v2/storage_mode.hpp"
|
||||
#include "storage/v2/vertex_accessor.hpp"
|
||||
#include "utils/stat.hpp"
|
||||
|
||||
/// REPLICATION ///
|
||||
#include "storage/v2/replication/replication_client.hpp"
|
||||
#include "storage/v2/replication/replication_server.hpp"
|
||||
#include "storage/v2/replication/rpc.hpp"
|
||||
#include "storage/v2/storage_error.hpp"
|
||||
#include "storage/v2/inmemory/replication/replication_client.hpp"
|
||||
#include "storage/v2/inmemory/replication/replication_server.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
using OOMExceptionEnabler = utils::MemoryTracker::OutOfMemoryExceptionEnabler;
|
||||
|
||||
namespace {
|
||||
inline constexpr uint16_t kEpochHistoryRetention = 1000;
|
||||
|
||||
std::string RegisterReplicaErrorToString(InMemoryStorage::RegisterReplicaError error) {
|
||||
switch (error) {
|
||||
case InMemoryStorage::RegisterReplicaError::NAME_EXISTS:
|
||||
return "NAME_EXISTS";
|
||||
case InMemoryStorage::RegisterReplicaError::END_POINT_EXISTS:
|
||||
return "END_POINT_EXISTS";
|
||||
case InMemoryStorage::RegisterReplicaError::CONNECTION_FAILED:
|
||||
return "CONNECTION_FAILED";
|
||||
case InMemoryStorage::RegisterReplicaError::COULD_NOT_BE_PERSISTED:
|
||||
return "COULD_NOT_BE_PERSISTED";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InMemoryStorage::InMemoryStorage(Config config)
|
||||
: Storage(config, StorageMode::IN_MEMORY_TRANSACTIONAL),
|
||||
snapshot_directory_(config.durability.storage_directory / durability::kSnapshotDirectory),
|
||||
lock_file_path_(config.durability.storage_directory / durability::kLockFile),
|
||||
wal_directory_(config.durability.storage_directory / durability::kWalDirectory),
|
||||
uuid_(utils::GenerateUUID()),
|
||||
epoch_id_(utils::GenerateUUID()),
|
||||
global_locker_(file_retainer_.AddLocker()) {
|
||||
global_locker_(file_retainer_.AddLocker()),
|
||||
replication_state_(config_.durability.restore_replication_state_on_startup,
|
||||
config_.durability.storage_directory) {
|
||||
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
|
||||
@@ -80,15 +63,16 @@ InMemoryStorage::InMemoryStorage(Config config)
|
||||
config_.durability.storage_directory);
|
||||
}
|
||||
if (config_.durability.recover_on_startup) {
|
||||
auto info = durability::RecoverData(snapshot_directory_, wal_directory_, &uuid_, &epoch_id_, &epoch_history_,
|
||||
&vertices_, &edges_, &edge_count_, name_id_mapper_.get(), &indices_,
|
||||
&constraints_, config_, &wal_seq_num_);
|
||||
auto &epoch = replication_state_.GetEpoch();
|
||||
auto info = durability::RecoverData(snapshot_directory_, wal_directory_, &uuid_, &epoch.id,
|
||||
&replication_state_.history, &vertices_, &edges_, &edge_count_,
|
||||
name_id_mapper_.get(), &indices_, &constraints_, config_, &wal_seq_num_);
|
||||
if (info) {
|
||||
vertex_id_ = info->next_vertex_id;
|
||||
edge_id_ = info->next_edge_id;
|
||||
timestamp_ = std::max(timestamp_, info->next_timestamp);
|
||||
if (info->last_commit_timestamp) {
|
||||
last_commit_timestamp_ = *info->last_commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = *info->last_commit_timestamp;
|
||||
}
|
||||
}
|
||||
} else if (config_.durability.snapshot_wal_mode != Config::Durability::SnapshotWalMode::DISABLED ||
|
||||
@@ -151,13 +135,9 @@ InMemoryStorage::InMemoryStorage(Config config)
|
||||
|
||||
if (config_.durability.restore_replication_state_on_startup) {
|
||||
spdlog::info("Replication configuration will be stored and will be automatically restored in case of a crash.");
|
||||
utils::EnsureDirOrDie(config_.durability.storage_directory / durability::kReplicationDirectory);
|
||||
storage_ =
|
||||
std::make_unique<kvstore::KVStore>(config_.durability.storage_directory / durability::kReplicationDirectory);
|
||||
|
||||
RestoreReplicationRole();
|
||||
|
||||
if (replication_role_ == replication::ReplicationRole::MAIN) {
|
||||
if (replication_state_.GetRole() == replication::ReplicationRole::MAIN) {
|
||||
RestoreReplicas();
|
||||
}
|
||||
} else {
|
||||
@@ -167,7 +147,7 @@ InMemoryStorage::InMemoryStorage(Config config)
|
||||
}
|
||||
|
||||
if (config_.durability.snapshot_wal_mode == Config::Durability::SnapshotWalMode::DISABLED &&
|
||||
replication_role_ == replication::ReplicationRole::MAIN) {
|
||||
replication_state_.GetRole() == replication::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 "
|
||||
@@ -181,8 +161,7 @@ InMemoryStorage::~InMemoryStorage() {
|
||||
}
|
||||
{
|
||||
// Clear replication data
|
||||
replication_server_.reset();
|
||||
replication_clients_.WithLock([&](auto &clients) { clients.clear(); });
|
||||
replication_state_.Reset();
|
||||
}
|
||||
if (wal_file_) {
|
||||
wal_file_->FinalizeWal();
|
||||
@@ -240,7 +219,7 @@ VertexAccessor InMemoryStorage::InMemoryAccessor::CreateVertex() {
|
||||
return {&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_};
|
||||
}
|
||||
|
||||
VertexAccessor InMemoryStorage::InMemoryAccessor::CreateVertex(storage::Gid gid) {
|
||||
VertexAccessor InMemoryStorage::InMemoryAccessor::CreateVertexEx(storage::Gid gid) {
|
||||
OOMExceptionEnabler oom_exception;
|
||||
// NOTE: When we update the next `vertex_id_` here we perform a RMW
|
||||
// (read-modify-write) operation that ISN'T atomic! But, that isn't an issue
|
||||
@@ -289,6 +268,7 @@ Result<std::optional<VertexAccessor>> InMemoryStorage::InMemoryAccessor::DeleteV
|
||||
|
||||
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
||||
vertex_ptr->deleted = true;
|
||||
transaction_.manyDeltasCache.Invalidate(vertex_ptr);
|
||||
|
||||
// Need to inform the next CollectGarbage call that there are some
|
||||
// non-transactional deletions that need to be collected
|
||||
@@ -366,6 +346,7 @@ InMemoryStorage::InMemoryAccessor::DetachDeleteVertex(VertexAccessor *vertex) {
|
||||
|
||||
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
||||
vertex_ptr->deleted = true;
|
||||
transaction_.manyDeltasCache.Invalidate(vertex_ptr);
|
||||
|
||||
// Need to inform the next CollectGarbage call that there are some
|
||||
// non-transactional deletions that need to be collected
|
||||
@@ -435,6 +416,9 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdge(VertexAccesso
|
||||
CreateAndLinkDelta(&transaction_, to_vertex, Delta::RemoveInEdgeTag(), edge_type, from_vertex, edge);
|
||||
to_vertex->in_edges.emplace_back(edge_type, from_vertex, edge);
|
||||
|
||||
transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
|
||||
transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
|
||||
|
||||
// Increment edge count.
|
||||
storage_->edge_count_.fetch_add(1, std::memory_order_acq_rel);
|
||||
|
||||
@@ -442,8 +426,8 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdge(VertexAccesso
|
||||
&storage_->constraints_, config_);
|
||||
}
|
||||
|
||||
Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdge(VertexAccessor *from, VertexAccessor *to,
|
||||
EdgeTypeId edge_type, storage::Gid gid) {
|
||||
Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdgeEx(VertexAccessor *from, VertexAccessor *to,
|
||||
EdgeTypeId edge_type, storage::Gid gid) {
|
||||
OOMExceptionEnabler oom_exception;
|
||||
MG_ASSERT(from->transaction_ == to->transaction_,
|
||||
"VertexAccessors must be from the same transaction when creating "
|
||||
@@ -507,6 +491,9 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdge(VertexAccesso
|
||||
CreateAndLinkDelta(&transaction_, to_vertex, Delta::RemoveInEdgeTag(), edge_type, from_vertex, edge);
|
||||
to_vertex->in_edges.emplace_back(edge_type, from_vertex, edge);
|
||||
|
||||
transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
|
||||
transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
|
||||
|
||||
// Increment edge count.
|
||||
storage_->edge_count_.fetch_add(1, std::memory_order_acq_rel);
|
||||
|
||||
@@ -598,6 +585,9 @@ Result<std::optional<EdgeAccessor>> InMemoryStorage::InMemoryAccessor::DeleteEdg
|
||||
CreateAndLinkDelta(&transaction_, from_vertex, Delta::AddOutEdgeTag(), edge_type, to_vertex, edge_ref);
|
||||
CreateAndLinkDelta(&transaction_, to_vertex, Delta::AddInEdgeTag(), edge_type, from_vertex, edge_ref);
|
||||
|
||||
transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
|
||||
transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
|
||||
|
||||
// Decrement edge count.
|
||||
storage_->edge_count_.fetch_add(-1, std::memory_order_acq_rel);
|
||||
|
||||
@@ -690,7 +680,7 @@ utils::BasicResult<StorageDataManipulationError, void> InMemoryStorage::InMemory
|
||||
// modifications before they are written to disk.
|
||||
// Replica can log only the write transaction received from Main
|
||||
// so the Wal files are consistent
|
||||
if (mem_storage->replication_role_ == replication::ReplicationRole::MAIN ||
|
||||
if (mem_storage->replication_state_.GetRole() == replication::ReplicationRole::MAIN ||
|
||||
desired_commit_timestamp.has_value()) {
|
||||
could_replicate_all_sync_replicas =
|
||||
mem_storage->AppendToWalDataManipulation(transaction_, *commit_timestamp_);
|
||||
@@ -706,10 +696,10 @@ utils::BasicResult<StorageDataManipulationError, void> InMemoryStorage::InMemory
|
||||
transaction_.commit_timestamp->store(*commit_timestamp_, std::memory_order_release);
|
||||
// Replica can only update the last commit timestamp with
|
||||
// the commits received from main.
|
||||
if (mem_storage->replication_role_ == replication::ReplicationRole::MAIN ||
|
||||
if (mem_storage->replication_state_.GetRole() == replication::ReplicationRole::MAIN ||
|
||||
desired_commit_timestamp.has_value()) {
|
||||
// Update the last commit timestamp
|
||||
mem_storage->last_commit_timestamp_.store(*commit_timestamp_);
|
||||
mem_storage->replication_state_.last_commit_timestamp_.store(*commit_timestamp_);
|
||||
}
|
||||
// Release engine lock because we don't have to hold it anymore
|
||||
// and emplace back could take a long time.
|
||||
@@ -725,6 +715,7 @@ utils::BasicResult<StorageDataManipulationError, void> InMemoryStorage::InMemory
|
||||
return StorageDataManipulationError{*unique_constraint_violation};
|
||||
}
|
||||
}
|
||||
|
||||
is_transaction_active_ = false;
|
||||
|
||||
if (!could_replicate_all_sync_replicas) {
|
||||
@@ -924,7 +915,7 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::CreateInd
|
||||
const auto success =
|
||||
AppendToWalDataDefinition(durability::StorageGlobalOperation::LABEL_INDEX_CREATE, label, {}, commit_timestamp);
|
||||
commit_log_->MarkFinished(commit_timestamp);
|
||||
last_commit_timestamp_ = commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = commit_timestamp;
|
||||
|
||||
// We don't care if there is a replication error because on main node the change will go through
|
||||
memgraph::metrics::IncrementCounter(memgraph::metrics::ActiveLabelIndices);
|
||||
@@ -947,7 +938,7 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::CreateInd
|
||||
auto success = AppendToWalDataDefinition(durability::StorageGlobalOperation::LABEL_PROPERTY_INDEX_CREATE, label,
|
||||
{property}, commit_timestamp);
|
||||
commit_log_->MarkFinished(commit_timestamp);
|
||||
last_commit_timestamp_ = commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = commit_timestamp;
|
||||
|
||||
// We don't care if there is a replication error because on main node the change will go through
|
||||
memgraph::metrics::IncrementCounter(memgraph::metrics::ActiveLabelPropertyIndices);
|
||||
@@ -969,7 +960,7 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::DropIndex
|
||||
auto success =
|
||||
AppendToWalDataDefinition(durability::StorageGlobalOperation::LABEL_INDEX_DROP, label, {}, commit_timestamp);
|
||||
commit_log_->MarkFinished(commit_timestamp);
|
||||
last_commit_timestamp_ = commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = commit_timestamp;
|
||||
|
||||
// We don't care if there is a replication error because on main node the change will go through
|
||||
memgraph::metrics::DecrementCounter(memgraph::metrics::ActiveLabelIndices);
|
||||
@@ -993,7 +984,7 @@ utils::BasicResult<StorageIndexDefinitionError, void> InMemoryStorage::DropIndex
|
||||
auto success = AppendToWalDataDefinition(durability::StorageGlobalOperation::LABEL_PROPERTY_INDEX_DROP, label,
|
||||
{property}, commit_timestamp);
|
||||
commit_log_->MarkFinished(commit_timestamp);
|
||||
last_commit_timestamp_ = commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = commit_timestamp;
|
||||
|
||||
// We don't care if there is a replication error because on main node the change will go through
|
||||
memgraph::metrics::DecrementCounter(memgraph::metrics::ActiveLabelPropertyIndices);
|
||||
@@ -1024,7 +1015,7 @@ utils::BasicResult<StorageExistenceConstraintDefinitionError, void> InMemoryStor
|
||||
auto success = AppendToWalDataDefinition(durability::StorageGlobalOperation::EXISTENCE_CONSTRAINT_CREATE, label,
|
||||
{property}, commit_timestamp);
|
||||
commit_log_->MarkFinished(commit_timestamp);
|
||||
last_commit_timestamp_ = commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = commit_timestamp;
|
||||
|
||||
if (success) {
|
||||
return {};
|
||||
@@ -1043,7 +1034,7 @@ utils::BasicResult<StorageExistenceConstraintDroppingError, void> InMemoryStorag
|
||||
auto success = AppendToWalDataDefinition(durability::StorageGlobalOperation::EXISTENCE_CONSTRAINT_DROP, label,
|
||||
{property}, commit_timestamp);
|
||||
commit_log_->MarkFinished(commit_timestamp);
|
||||
last_commit_timestamp_ = commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = commit_timestamp;
|
||||
|
||||
if (success) {
|
||||
return {};
|
||||
@@ -1068,7 +1059,7 @@ InMemoryStorage::CreateUniqueConstraint(LabelId label, const std::set<PropertyId
|
||||
auto success = AppendToWalDataDefinition(durability::StorageGlobalOperation::UNIQUE_CONSTRAINT_CREATE, label,
|
||||
properties, commit_timestamp);
|
||||
commit_log_->MarkFinished(commit_timestamp);
|
||||
last_commit_timestamp_ = commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = commit_timestamp;
|
||||
|
||||
if (success) {
|
||||
return UniqueConstraints::CreationStatus::SUCCESS;
|
||||
@@ -1089,7 +1080,7 @@ InMemoryStorage::DropUniqueConstraint(LabelId label, const std::set<PropertyId>
|
||||
auto success = AppendToWalDataDefinition(durability::StorageGlobalOperation::UNIQUE_CONSTRAINT_DROP, label,
|
||||
properties, commit_timestamp);
|
||||
commit_log_->MarkFinished(commit_timestamp);
|
||||
last_commit_timestamp_ = commit_timestamp;
|
||||
replication_state_.last_commit_timestamp_ = commit_timestamp;
|
||||
|
||||
if (success) {
|
||||
return UniqueConstraints::DeletionStatus::SUCCESS;
|
||||
@@ -1142,7 +1133,7 @@ Transaction InMemoryStorage::CreateTransaction(IsolationLevel isolation_level, S
|
||||
// of any query on replica to the last commited transaction
|
||||
// which is timestamp_ as only commit of transaction with writes
|
||||
// can change the value of it.
|
||||
if (replication_role_ == replication::ReplicationRole::REPLICA) {
|
||||
if (replication_state_.GetRole() == replication::ReplicationRole::REPLICA) {
|
||||
start_timestamp = timestamp_;
|
||||
} else {
|
||||
start_timestamp = timestamp_++;
|
||||
@@ -1473,8 +1464,8 @@ bool InMemoryStorage::InitializeWalFile() {
|
||||
if (config_.durability.snapshot_wal_mode != Config::Durability::SnapshotWalMode::PERIODIC_SNAPSHOT_WITH_WAL)
|
||||
return false;
|
||||
if (!wal_file_) {
|
||||
wal_file_.emplace(wal_directory_, uuid_, epoch_id_, config_.items, name_id_mapper_.get(), wal_seq_num_++,
|
||||
&file_retainer_);
|
||||
wal_file_.emplace(wal_directory_, uuid_, replication_state_.GetEpoch().id, config_.items, name_id_mapper_.get(),
|
||||
wal_seq_num_++, &file_retainer_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1506,184 +1497,166 @@ bool InMemoryStorage::AppendToWalDataManipulation(const Transaction &transaction
|
||||
// A single transaction will always be contained in a single WAL file.
|
||||
auto current_commit_timestamp = transaction.commit_timestamp->load(std::memory_order_acquire);
|
||||
|
||||
if (replication_role_.load() == replication::ReplicationRole::MAIN) {
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->StartTransactionReplication(wal_file_->SequenceNumber());
|
||||
}
|
||||
});
|
||||
}
|
||||
replication_state_.InitializeTransaction(wal_file_->SequenceNumber());
|
||||
|
||||
// Helper lambda that traverses the delta chain on order to find the first
|
||||
// delta that should be processed and then appends all discovered deltas.
|
||||
auto find_and_apply_deltas = [&](const auto *delta, const auto &parent, auto filter) {
|
||||
while (true) {
|
||||
auto *older = delta->next.load(std::memory_order_acquire);
|
||||
if (older == nullptr || older->timestamp->load(std::memory_order_acquire) != current_commit_timestamp) break;
|
||||
delta = older;
|
||||
}
|
||||
while (true) {
|
||||
if (filter(delta->action)) {
|
||||
wal_file_->AppendDelta(*delta, parent, final_commit_timestamp);
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->IfStreamingTransaction(
|
||||
[&](auto &stream) { stream.AppendDelta(*delta, parent, final_commit_timestamp); });
|
||||
}
|
||||
});
|
||||
auto append_deltas = [&](auto callback) {
|
||||
// Helper lambda that traverses the delta chain on order to find the first
|
||||
// delta that should be processed and then appends all discovered deltas.
|
||||
auto find_and_apply_deltas = [&](const auto *delta, const auto &parent, auto filter) {
|
||||
while (true) {
|
||||
auto *older = delta->next.load(std::memory_order_acquire);
|
||||
if (older == nullptr || older->timestamp->load(std::memory_order_acquire) != current_commit_timestamp) break;
|
||||
delta = older;
|
||||
}
|
||||
auto prev = delta->prev.Get();
|
||||
while (true) {
|
||||
if (filter(delta->action)) {
|
||||
callback(*delta, parent, final_commit_timestamp);
|
||||
}
|
||||
auto prev = delta->prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::DELTA) break;
|
||||
delta = prev.delta;
|
||||
}
|
||||
};
|
||||
|
||||
// The deltas are ordered correctly in the `transaction.deltas` buffer, but we
|
||||
// don't traverse them in that order. That is because for each delta we need
|
||||
// information about the vertex or edge they belong to and that information
|
||||
// isn't stored in the deltas themselves. In order to find out information
|
||||
// about the corresponding vertex or edge it is necessary to traverse the
|
||||
// delta chain for each delta until a vertex or edge is encountered. This
|
||||
// operation is very expensive as the chain grows.
|
||||
// Instead, we traverse the edges until we find a vertex or edge and traverse
|
||||
// their delta chains. This approach has a drawback because we lose the
|
||||
// correct order of the operations. Because of that, we need to traverse the
|
||||
// deltas several times and we have to manually ensure that the stored deltas
|
||||
// will be ordered correctly.
|
||||
|
||||
// 1. Process all Vertex deltas and store all operations that create vertices
|
||||
// and modify vertex data.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::DELTA) break;
|
||||
delta = prev.delta;
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) continue;
|
||||
find_and_apply_deltas(&delta, *prev.vertex, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
return true;
|
||||
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 2. Process all Vertex deltas and store all operations that create edges.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) continue;
|
||||
find_and_apply_deltas(&delta, *prev.vertex, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return true;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 3. Process all Edge deltas and store all operations that modify edge data.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::EDGE) continue;
|
||||
find_and_apply_deltas(&delta, *prev.edge, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
return true;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 4. Process all Vertex deltas and store all operations that delete edges.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) continue;
|
||||
find_and_apply_deltas(&delta, *prev.vertex, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
return true;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 5. Process all Vertex deltas and store all operations that delete vertices.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) continue;
|
||||
find_and_apply_deltas(&delta, *prev.vertex, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
return true;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// The deltas are ordered correctly in the `transaction.deltas` buffer, but we
|
||||
// don't traverse them in that order. That is because for each delta we need
|
||||
// information about the vertex or edge they belong to and that information
|
||||
// isn't stored in the deltas themselves. In order to find out information
|
||||
// about the corresponding vertex or edge it is necessary to traverse the
|
||||
// delta chain for each delta until a vertex or edge is encountered. This
|
||||
// operation is very expensive as the chain grows.
|
||||
// Instead, we traverse the edges until we find a vertex or edge and traverse
|
||||
// their delta chains. This approach has a drawback because we lose the
|
||||
// correct order of the operations. Because of that, we need to traverse the
|
||||
// deltas several times and we have to manually ensure that the stored deltas
|
||||
// will be ordered correctly.
|
||||
|
||||
// 1. Process all Vertex deltas and store all operations that create vertices
|
||||
// and modify vertex data.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) continue;
|
||||
find_and_apply_deltas(&delta, *prev.vertex, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
return true;
|
||||
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 2. Process all Vertex deltas and store all operations that create edges.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) continue;
|
||||
find_and_apply_deltas(&delta, *prev.vertex, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return true;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 3. Process all Edge deltas and store all operations that modify edge data.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::EDGE) continue;
|
||||
find_and_apply_deltas(&delta, *prev.edge, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
return true;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 4. Process all Vertex deltas and store all operations that delete edges.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) continue;
|
||||
find_and_apply_deltas(&delta, *prev.vertex, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
return true;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 5. Process all Vertex deltas and store all operations that delete vertices.
|
||||
for (const auto &delta : transaction.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
MG_ASSERT(prev.type != PreviousPtr::Type::NULLPTR, "Invalid pointer!");
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) continue;
|
||||
find_and_apply_deltas(&delta, *prev.vertex, [](auto action) {
|
||||
switch (action) {
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
return true;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add a delta that indicates that the transaction is fully written to the WAL
|
||||
// file.
|
||||
wal_file_->AppendTransactionEnd(final_commit_timestamp);
|
||||
|
||||
FinalizeWalFile();
|
||||
|
||||
auto finalized_on_all_replicas = true;
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->IfStreamingTransaction([&](auto &stream) { stream.AppendTransactionEnd(final_commit_timestamp); });
|
||||
const auto finalized = client->FinalizeTransactionReplication();
|
||||
|
||||
if (client->Mode() == replication::ReplicationMode::SYNC) {
|
||||
finalized_on_all_replicas = finalized && finalized_on_all_replicas;
|
||||
}
|
||||
}
|
||||
append_deltas([&](const Delta &delta, const auto &parent, uint64_t timestamp) {
|
||||
wal_file_->AppendDelta(delta, parent, timestamp);
|
||||
replication_state_.AppendDelta(delta, parent, timestamp);
|
||||
});
|
||||
|
||||
return finalized_on_all_replicas;
|
||||
// Add a delta that indicates that the transaction is fully written to the WAL
|
||||
// file.replication_clients_.WithLock
|
||||
wal_file_->AppendTransactionEnd(final_commit_timestamp);
|
||||
FinalizeWalFile();
|
||||
|
||||
return replication_state_.FinalizeTransaction(final_commit_timestamp);
|
||||
}
|
||||
|
||||
bool InMemoryStorage::AppendToWalDataDefinition(durability::StorageGlobalOperation operation, LabelId label,
|
||||
@@ -1693,42 +1666,27 @@ bool InMemoryStorage::AppendToWalDataDefinition(durability::StorageGlobalOperati
|
||||
return true;
|
||||
}
|
||||
|
||||
auto finalized_on_all_replicas = true;
|
||||
wal_file_->AppendOperation(operation, label, properties, final_commit_timestamp);
|
||||
{
|
||||
if (replication_role_.load() == replication::ReplicationRole::MAIN) {
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->StartTransactionReplication(wal_file_->SequenceNumber());
|
||||
client->IfStreamingTransaction(
|
||||
[&](auto &stream) { stream.AppendOperation(operation, label, properties, final_commit_timestamp); });
|
||||
|
||||
const auto finalized = client->FinalizeTransactionReplication();
|
||||
if (client->Mode() == replication::ReplicationMode::SYNC) {
|
||||
finalized_on_all_replicas = finalized && finalized_on_all_replicas;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
FinalizeWalFile();
|
||||
return finalized_on_all_replicas;
|
||||
return replication_state_.AppendToWalDataDefinition(wal_file_->SequenceNumber(), operation, label, properties,
|
||||
final_commit_timestamp);
|
||||
}
|
||||
|
||||
utils::BasicResult<InMemoryStorage::CreateSnapshotError> InMemoryStorage::CreateSnapshot(
|
||||
std::optional<bool> is_periodic) {
|
||||
if (replication_role_.load() != replication::ReplicationRole::MAIN) {
|
||||
if (replication_state_.GetRole() != replication::ReplicationRole::MAIN) {
|
||||
return CreateSnapshotError::DisabledForReplica;
|
||||
}
|
||||
|
||||
auto snapshot_creator = [this]() {
|
||||
utils::Timer timer;
|
||||
|
||||
const auto &epoch = replication_state_.GetEpoch();
|
||||
auto transaction = CreateTransaction(IsolationLevel::SNAPSHOT_ISOLATION, storage_mode_);
|
||||
// Create snapshot.
|
||||
durability::CreateSnapshot(&transaction, snapshot_directory_, wal_directory_,
|
||||
config_.durability.snapshot_retention_count, &vertices_, &edges_, name_id_mapper_.get(),
|
||||
&indices_, &constraints_, config_, uuid_, epoch_id_, epoch_history_, &file_retainer_);
|
||||
&indices_, &constraints_, config_, uuid_, epoch.id, replication_state_.history,
|
||||
&file_retainer_);
|
||||
// Finalize snapshot transaction.
|
||||
commit_log_->MarkFinished(transaction.start_timestamp);
|
||||
|
||||
@@ -1783,268 +1741,16 @@ uint64_t InMemoryStorage::CommitTimestamp(const std::optional<uint64_t> desired_
|
||||
return *desired_commit_timestamp;
|
||||
}
|
||||
|
||||
bool InMemoryStorage::SetReplicaRole(io::network::Endpoint endpoint,
|
||||
const replication::ReplicationServerConfig &config) {
|
||||
// We don't want to restart the server if we're already a REPLICA
|
||||
if (replication_role_ == replication::ReplicationRole::REPLICA) {
|
||||
return false;
|
||||
void InMemoryStorage::EstablishNewEpoch() {
|
||||
std::unique_lock engine_guard{engine_lock_};
|
||||
if (wal_file_) {
|
||||
wal_file_->FinalizeWal();
|
||||
wal_file_.reset();
|
||||
}
|
||||
|
||||
auto port = endpoint.port; // assigning because we will move the endpoint
|
||||
replication_server_ = std::make_unique<ReplicationServer>(this, std::move(endpoint), config);
|
||||
|
||||
if (ShouldStoreAndRestoreReplicationState()) {
|
||||
// Only thing that matters here is the role saved as REPLICA and the listening port
|
||||
auto data = replication::ReplicationStatusToJSON(
|
||||
replication::ReplicationStatus{.name = replication::kReservedReplicationRoleName,
|
||||
.ip_address = "",
|
||||
.port = port,
|
||||
.sync_mode = replication::ReplicationMode::SYNC,
|
||||
.replica_check_frequency = std::chrono::seconds(0),
|
||||
.ssl = std::nullopt,
|
||||
.role = replication::ReplicationRole::REPLICA});
|
||||
|
||||
if (!storage_->Put(replication::kReservedReplicationRoleName, data.dump())) {
|
||||
spdlog::error("Error when saving REPLICA replication role in settings.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
replication_role_.store(replication::ReplicationRole::REPLICA);
|
||||
return true;
|
||||
// TODO: Move out of storage (no need for the lock) <- missing commit_timestamp at a higher level
|
||||
replication_state_.NewEpoch();
|
||||
}
|
||||
|
||||
bool InMemoryStorage::SetMainReplicationRole() {
|
||||
// We don't want to generate new epoch_id and do the
|
||||
// cleanup if we're already a MAIN
|
||||
if (replication_role_ == replication::ReplicationRole::MAIN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Main instance does not need replication server
|
||||
// This should be always called first so we finalize everything
|
||||
replication_server_.reset(nullptr);
|
||||
|
||||
{
|
||||
std::unique_lock engine_guard{engine_lock_};
|
||||
if (wal_file_) {
|
||||
wal_file_->FinalizeWal();
|
||||
wal_file_.reset();
|
||||
}
|
||||
|
||||
// Generate new epoch id and save the last one to the history.
|
||||
if (epoch_history_.size() == kEpochHistoryRetention) {
|
||||
epoch_history_.pop_front();
|
||||
}
|
||||
epoch_history_.emplace_back(std::move(epoch_id_), last_commit_timestamp_);
|
||||
epoch_id_ = utils::GenerateUUID();
|
||||
}
|
||||
|
||||
if (ShouldStoreAndRestoreReplicationState()) {
|
||||
// Only thing that matters here is the role saved as MAIN
|
||||
auto data = replication::ReplicationStatusToJSON(
|
||||
replication::ReplicationStatus{.name = replication::kReservedReplicationRoleName,
|
||||
.ip_address = "",
|
||||
.port = 0,
|
||||
.sync_mode = replication::ReplicationMode::SYNC,
|
||||
.replica_check_frequency = std::chrono::seconds(0),
|
||||
.ssl = std::nullopt,
|
||||
.role = replication::ReplicationRole::MAIN});
|
||||
|
||||
if (!storage_->Put(replication::kReservedReplicationRoleName, data.dump())) {
|
||||
spdlog::error("Error when saving MAIN replication role in settings.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
replication_role_.store(replication::ReplicationRole::MAIN);
|
||||
return true;
|
||||
}
|
||||
|
||||
utils::BasicResult<InMemoryStorage::RegisterReplicaError> InMemoryStorage::RegisterReplica(
|
||||
std::string name, io::network::Endpoint endpoint, const replication::ReplicationMode replication_mode,
|
||||
const replication::RegistrationMode registration_mode, const replication::ReplicationClientConfig &config) {
|
||||
MG_ASSERT(replication_role_.load() == replication::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(), [&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;
|
||||
}
|
||||
|
||||
if (ShouldStoreAndRestoreReplicationState()) {
|
||||
auto data = replication::ReplicationStatusToJSON(
|
||||
replication::ReplicationStatus{.name = name,
|
||||
.ip_address = endpoint.address,
|
||||
.port = endpoint.port,
|
||||
.sync_mode = replication_mode,
|
||||
.replica_check_frequency = config.replica_check_frequency,
|
||||
.ssl = config.ssl,
|
||||
.role = replication::ReplicationRole::REPLICA});
|
||||
if (!storage_->Put(name, data.dump())) {
|
||||
spdlog::error("Error when saving replica {} in settings.", name);
|
||||
return RegisterReplicaError::COULD_NOT_BE_PERSISTED;
|
||||
}
|
||||
}
|
||||
|
||||
auto client = std::make_unique<ReplicationClient>(std::move(name), this, endpoint, replication_mode, config);
|
||||
|
||||
if (client->State() == replication::ReplicaState::INVALID) {
|
||||
if (replication::RegistrationMode::CAN_BE_INVALID != registration_mode) {
|
||||
return RegisterReplicaError::CONNECTION_FAILED;
|
||||
}
|
||||
|
||||
spdlog::warn("Connection failed when registering replica {}. Replica will still be registered.", client->Name());
|
||||
}
|
||||
|
||||
return replication_clients_.WithLock([&](auto &clients) -> utils::BasicResult<InMemoryStorage::RegisterReplicaError> {
|
||||
// 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(),
|
||||
[&](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 {};
|
||||
});
|
||||
}
|
||||
|
||||
bool InMemoryStorage::UnregisterReplica(const std::string &name) {
|
||||
MG_ASSERT(replication_role_.load() == replication::ReplicationRole::MAIN,
|
||||
"Only main instance can unregister a replica!");
|
||||
if (ShouldStoreAndRestoreReplicationState()) {
|
||||
if (!storage_->Delete(name)) {
|
||||
spdlog::error("Error when removing replica {} from settings.", name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return replication_clients_.WithLock([&](auto &clients) {
|
||||
return std::erase_if(clients, [&](const auto &client) { return client->Name() == name; });
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<replication::ReplicaState> InMemoryStorage::GetReplicaState(const std::string_view name) {
|
||||
return replication_clients_.WithLock([&](auto &clients) -> std::optional<replication::ReplicaState> {
|
||||
const auto client_it =
|
||||
std::find_if(clients.cbegin(), clients.cend(), [name](auto &client) { return client->Name() == name; });
|
||||
if (client_it == clients.cend()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return (*client_it)->State();
|
||||
});
|
||||
}
|
||||
|
||||
replication::ReplicationRole InMemoryStorage::GetReplicationRole() const { return replication_role_; }
|
||||
|
||||
std::vector<InMemoryStorage::ReplicaInfo> InMemoryStorage::ReplicasInfo() {
|
||||
return replication_clients_.WithLock([](auto &clients) {
|
||||
std::vector<InMemoryStorage::ReplicaInfo> replica_info;
|
||||
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->Endpoint(), client->State(), client->GetTimestampInfo()};
|
||||
});
|
||||
return replica_info;
|
||||
});
|
||||
}
|
||||
|
||||
void InMemoryStorage::RestoreReplicationRole() {
|
||||
if (!ShouldStoreAndRestoreReplicationState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
spdlog::info("Restoring replication role.");
|
||||
uint16_t port = replication::kDefaultReplicationPort;
|
||||
|
||||
const auto replication_data = storage_->Get(replication::kReservedReplicationRoleName);
|
||||
if (!replication_data.has_value()) {
|
||||
spdlog::debug("Cannot find data needed for restore replication role in persisted metadata.");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto maybe_replication_status = replication::JSONToReplicationStatus(nlohmann::json::parse(*replication_data));
|
||||
if (!maybe_replication_status.has_value()) {
|
||||
LOG_FATAL("Cannot parse previously saved configuration of replication role {}.",
|
||||
replication::kReservedReplicationRoleName);
|
||||
}
|
||||
|
||||
const auto replication_status = *maybe_replication_status;
|
||||
if (!replication_status.role.has_value()) {
|
||||
replication_role_.store(replication::ReplicationRole::MAIN);
|
||||
} else {
|
||||
replication_role_.store(*replication_status.role);
|
||||
port = replication_status.port;
|
||||
}
|
||||
|
||||
if (replication_role_ == replication::ReplicationRole::REPLICA) {
|
||||
io::network::Endpoint endpoint(replication::kDefaultReplicationServerIp, port);
|
||||
replication_server_ =
|
||||
std::make_unique<ReplicationServer>(this, std::move(endpoint), replication::ReplicationServerConfig{});
|
||||
}
|
||||
|
||||
spdlog::info("Replication role restored to {}.",
|
||||
replication_role_ == replication::ReplicationRole::MAIN ? "MAIN" : "REPLICA");
|
||||
}
|
||||
|
||||
void InMemoryStorage::RestoreReplicas() {
|
||||
if (!ShouldStoreAndRestoreReplicationState()) {
|
||||
return;
|
||||
}
|
||||
spdlog::info("Restoring replicas.");
|
||||
|
||||
for (const auto &[replica_name, replica_data] : *storage_) {
|
||||
spdlog::info("Restoring replica {}.", replica_name);
|
||||
|
||||
const auto maybe_replica_status = replication::JSONToReplicationStatus(nlohmann::json::parse(replica_data));
|
||||
if (!maybe_replica_status.has_value()) {
|
||||
LOG_FATAL("Cannot parse previously saved configuration of replica {}.", replica_name);
|
||||
}
|
||||
|
||||
auto replica_status = *maybe_replica_status;
|
||||
MG_ASSERT(replica_status.name == replica_name, "Expected replica name is '{}', but got '{}'", replica_status.name,
|
||||
replica_name);
|
||||
|
||||
if (replica_name == replication::kReservedReplicationRoleName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto ret =
|
||||
RegisterReplica(std::move(replica_status.name), {std::move(replica_status.ip_address), replica_status.port},
|
||||
replica_status.sync_mode, replication::RegistrationMode::CAN_BE_INVALID,
|
||||
{
|
||||
.replica_check_frequency = replica_status.replica_check_frequency,
|
||||
.ssl = replica_status.ssl,
|
||||
});
|
||||
|
||||
if (ret.HasError()) {
|
||||
MG_ASSERT(RegisterReplicaError::CONNECTION_FAILED != ret.GetError());
|
||||
LOG_FATAL("Failure when restoring replica {}: {}.", replica_name, RegisterReplicaErrorToString(ret.GetError()));
|
||||
}
|
||||
spdlog::info("Replica {} restored.", replica_name);
|
||||
}
|
||||
}
|
||||
|
||||
bool InMemoryStorage::ShouldStoreAndRestoreReplicationState() const { return nullptr != storage_; }
|
||||
|
||||
utils::FileRetainer::FileLockerAccessor::ret_type InMemoryStorage::IsPathLocked() {
|
||||
auto locker_accessor = global_locker_.Access();
|
||||
return locker_accessor.IsPathLocked(config_.durability.storage_directory);
|
||||
@@ -2070,4 +1776,16 @@ utils::FileRetainer::FileLockerAccessor::ret_type InMemoryStorage::UnlockPath()
|
||||
return true;
|
||||
}
|
||||
|
||||
auto InMemoryStorage::CreateReplicationClient(std::string name, io::network::Endpoint endpoint,
|
||||
replication::ReplicationMode mode,
|
||||
replication::ReplicationClientConfig const &config)
|
||||
-> std::unique_ptr<ReplicationClient> {
|
||||
return std::make_unique<InMemoryReplicationClient>(this, std::move(name), endpoint, mode, config);
|
||||
}
|
||||
|
||||
std::unique_ptr<ReplicationServer> InMemoryStorage::CreateReplicationServer(
|
||||
io::network::Endpoint endpoint, const replication::ReplicationServerConfig &config) {
|
||||
return std::make_unique<InMemoryReplicationServer>(this, std::move(endpoint), config);
|
||||
}
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include "storage/v2/inmemory/label_index.hpp"
|
||||
#include "storage/v2/inmemory/label_property_index.hpp"
|
||||
#include "storage/v2/storage.hpp"
|
||||
@@ -23,6 +24,8 @@
|
||||
#include "storage/v2/replication/rpc.hpp"
|
||||
#include "storage/v2/replication/serialization.hpp"
|
||||
|
||||
#include "storage/v2/replication/replication.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
// The storage is based on this paper:
|
||||
@@ -31,27 +34,11 @@ namespace memgraph::storage {
|
||||
// only implement snapshot isolation for transactions.
|
||||
|
||||
class InMemoryStorage final : public Storage {
|
||||
friend class InMemoryReplicationServer;
|
||||
friend class InMemoryReplicationClient;
|
||||
friend class ReplicationClient;
|
||||
|
||||
public:
|
||||
enum class RegisterReplicaError : uint8_t {
|
||||
NAME_EXISTS,
|
||||
END_POINT_EXISTS,
|
||||
CONNECTION_FAILED,
|
||||
COULD_NOT_BE_PERSISTED
|
||||
};
|
||||
|
||||
struct TimestampInfo {
|
||||
uint64_t current_timestamp_of_replica;
|
||||
uint64_t current_number_of_timestamp_behind_master;
|
||||
};
|
||||
|
||||
struct ReplicaInfo {
|
||||
std::string name;
|
||||
replication::ReplicationMode mode;
|
||||
io::network::Endpoint endpoint;
|
||||
replication::ReplicaState state;
|
||||
TimestampInfo timestamp_info;
|
||||
};
|
||||
|
||||
enum class CreateSnapshotError : uint8_t {
|
||||
DisabledForReplica,
|
||||
DisabledForAnalyticsPeriodicCommit,
|
||||
@@ -69,7 +56,7 @@ class InMemoryStorage final : public Storage {
|
||||
|
||||
~InMemoryStorage() override;
|
||||
|
||||
class InMemoryAccessor final : public Storage::Accessor {
|
||||
class InMemoryAccessor : public Storage::Accessor {
|
||||
private:
|
||||
friend class InMemoryStorage;
|
||||
|
||||
@@ -268,16 +255,33 @@ class InMemoryStorage final : public Storage {
|
||||
|
||||
void FinalizeTransaction() override;
|
||||
|
||||
private:
|
||||
protected:
|
||||
// TODO Better naming
|
||||
/// @throw std::bad_alloc
|
||||
VertexAccessor CreateVertex(storage::Gid gid);
|
||||
|
||||
VertexAccessor CreateVertexEx(storage::Gid gid);
|
||||
/// @throw std::bad_alloc
|
||||
Result<EdgeAccessor> CreateEdge(VertexAccessor *from, VertexAccessor *to, EdgeTypeId edge_type, storage::Gid gid);
|
||||
Result<EdgeAccessor> CreateEdgeEx(VertexAccessor *from, VertexAccessor *to, EdgeTypeId edge_type, storage::Gid gid);
|
||||
|
||||
Config::Items config_;
|
||||
};
|
||||
|
||||
class ReplicationAccessor final : public InMemoryAccessor {
|
||||
public:
|
||||
explicit ReplicationAccessor(InMemoryAccessor &&inmem) : InMemoryAccessor(std::move(inmem)) {}
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
VertexAccessor CreateVertexEx(storage::Gid gid) { return InMemoryAccessor::CreateVertexEx(gid); }
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
Result<EdgeAccessor> CreateEdgeEx(VertexAccessor *from, VertexAccessor *to, EdgeTypeId edge_type,
|
||||
storage::Gid gid) {
|
||||
return InMemoryAccessor::CreateEdgeEx(from, to, edge_type, gid);
|
||||
}
|
||||
|
||||
const Transaction &GetTransaction() const { return transaction_; }
|
||||
Transaction &GetTransaction() { return transaction_; }
|
||||
};
|
||||
|
||||
std::unique_ptr<Storage::Accessor> Access(std::optional<IsolationLevel> override_isolation_level) override {
|
||||
return std::unique_ptr<InMemoryAccessor>(
|
||||
new InMemoryAccessor{this, override_isolation_level.value_or(isolation_level_), storage_mode_});
|
||||
@@ -359,24 +363,32 @@ class InMemoryStorage final : public Storage {
|
||||
utils::BasicResult<StorageUniqueConstraintDroppingError, UniqueConstraints::DeletionStatus> DropUniqueConstraint(
|
||||
LabelId label, const std::set<PropertyId> &properties, std::optional<uint64_t> desired_commit_timestamp) override;
|
||||
|
||||
bool SetReplicaRole(io::network::Endpoint endpoint, const replication::ReplicationServerConfig &config);
|
||||
bool SetReplicaRole(io::network::Endpoint endpoint, const replication::ReplicationServerConfig &config) {
|
||||
return replication_state_.SetReplicaRole(std::move(endpoint), config, this);
|
||||
}
|
||||
|
||||
bool SetMainReplicationRole();
|
||||
bool SetMainReplicationRole() { return replication_state_.SetMainReplicationRole(this); }
|
||||
|
||||
/// @pre The instance should have a MAIN role
|
||||
/// @pre Timeout can only be set for SYNC replication
|
||||
utils::BasicResult<RegisterReplicaError, void> RegisterReplica(std::string name, io::network::Endpoint endpoint,
|
||||
replication::ReplicationMode replication_mode,
|
||||
replication::RegistrationMode registration_mode,
|
||||
const replication::ReplicationClientConfig &config);
|
||||
auto RegisterReplica(std::string name, io::network::Endpoint endpoint,
|
||||
const replication::ReplicationMode replication_mode,
|
||||
const replication::RegistrationMode registration_mode,
|
||||
const replication::ReplicationClientConfig &config) {
|
||||
return replication_state_.RegisterReplica(std::move(name), std::move(endpoint), replication_mode, registration_mode,
|
||||
config, this);
|
||||
}
|
||||
|
||||
/// @pre The instance should have a MAIN role
|
||||
bool UnregisterReplica(const std::string &name);
|
||||
bool UnregisterReplica(const std::string &name) { return replication_state_.UnregisterReplica(name); }
|
||||
|
||||
std::optional<replication::ReplicaState> GetReplicaState(std::string_view name);
|
||||
replication::ReplicationRole GetReplicationRole() const { return replication_state_.GetRole(); }
|
||||
|
||||
replication::ReplicationRole GetReplicationRole() const;
|
||||
auto ReplicasInfo() { return replication_state_.ReplicasInfo(); }
|
||||
|
||||
std::vector<ReplicaInfo> ReplicasInfo();
|
||||
std::optional<replication::ReplicaState> GetReplicaState(std::string_view name) {
|
||||
return replication_state_.GetReplicaState(name);
|
||||
}
|
||||
|
||||
void FreeMemory(std::unique_lock<utils::RWLock> main_guard) override;
|
||||
|
||||
@@ -388,6 +400,13 @@ class InMemoryStorage final : public Storage {
|
||||
|
||||
Transaction CreateTransaction(IsolationLevel isolation_level, StorageMode storage_mode) override;
|
||||
|
||||
auto CreateReplicationClient(std::string name, io::network::Endpoint endpoint, replication::ReplicationMode mode,
|
||||
replication::ReplicationClientConfig const &config)
|
||||
-> std::unique_ptr<ReplicationClient> override;
|
||||
|
||||
auto CreateReplicationServer(io::network::Endpoint endpoint, const replication::ReplicationServerConfig &config)
|
||||
-> std::unique_ptr<ReplicationServer> override;
|
||||
|
||||
private:
|
||||
/// The force parameter determines the behaviour of the garbage collector.
|
||||
/// If it's set to true, it will behave as a global operation, i.e. it can't
|
||||
@@ -416,11 +435,11 @@ class InMemoryStorage final : public Storage {
|
||||
|
||||
uint64_t CommitTimestamp(std::optional<uint64_t> desired_commit_timestamp = {});
|
||||
|
||||
void RestoreReplicas();
|
||||
void RestoreReplicationRole() { return replication_state_.RestoreReplicationRole(this); }
|
||||
|
||||
void RestoreReplicationRole();
|
||||
void RestoreReplicas() { return replication_state_.RestoreReplicas(this); }
|
||||
|
||||
bool ShouldStoreAndRestoreReplicationState() const;
|
||||
void EstablishNewEpoch() override;
|
||||
|
||||
// Main object storage
|
||||
utils::SkipList<storage::Vertex> vertices_;
|
||||
@@ -430,7 +449,6 @@ class InMemoryStorage final : public Storage {
|
||||
std::filesystem::path snapshot_directory_;
|
||||
std::filesystem::path lock_file_path_;
|
||||
utils::OutputFile lock_file_handle_;
|
||||
std::unique_ptr<kvstore::KVStore> storage_;
|
||||
std::filesystem::path wal_directory_;
|
||||
|
||||
utils::Scheduler snapshot_runner_;
|
||||
@@ -441,26 +459,6 @@ class InMemoryStorage final : public Storage {
|
||||
// Sequence number used to keep track of the chain of WALs.
|
||||
uint64_t wal_seq_num_{0};
|
||||
|
||||
// UUID to distinguish different main instance runs for replication process
|
||||
// on SAME storage.
|
||||
// Multiple instances can have same storage UUID and be MAIN at the same time.
|
||||
// We cannot compare commit timestamps of those instances if one of them
|
||||
// becomes the replica of the other so we use epoch_id_ as additional
|
||||
// discriminating property.
|
||||
// Example of this:
|
||||
// We have 2 instances of the same storage, S1 and S2.
|
||||
// S1 and S2 are MAIN and accept their own commits and write them to the WAL.
|
||||
// At the moment when S1 commited a transaction with timestamp 20, and S2
|
||||
// a different transaction with timestamp 15, we change S2's role to REPLICA
|
||||
// and register it on S1.
|
||||
// Without using the epoch_id, we don't know that S1 and S2 have completely
|
||||
// different transactions, we think that the S2 is behind only by 5 commits.
|
||||
std::string epoch_id_;
|
||||
// History of the previous epoch ids.
|
||||
// Each value consists of the epoch id along the last commit belonging to that
|
||||
// epoch.
|
||||
std::deque<std::pair<std::string, uint64_t>> epoch_history_;
|
||||
|
||||
std::optional<durability::WalFile> wal_file_;
|
||||
uint64_t wal_unsynced_transactions_{0};
|
||||
|
||||
@@ -497,26 +495,7 @@ class InMemoryStorage final : public Storage {
|
||||
std::atomic<bool> gc_full_scan_vertices_delete_ = false;
|
||||
std::atomic<bool> gc_full_scan_edges_delete_ = false;
|
||||
|
||||
std::atomic<uint64_t> last_commit_timestamp_{kTimestampInitialId};
|
||||
|
||||
class ReplicationServer;
|
||||
std::unique_ptr<ReplicationServer> replication_server_{nullptr};
|
||||
|
||||
class ReplicationClient;
|
||||
// We create ReplicationClient using unique_ptr so we can move
|
||||
// newly created client into the vector.
|
||||
// We cannot move the client directly because it contains ThreadPool
|
||||
// which cannot be moved. Also, the move is necessary because
|
||||
// we don't want to create the client directly inside the vector
|
||||
// because that would require the lock on the list putting all
|
||||
// commits (they iterate list of clients) to halt.
|
||||
// This way we can initialize client in main thread which means
|
||||
// that we can immediately notify the user if the initialization
|
||||
// failed.
|
||||
using ReplicationClientList = utils::Synchronized<std::vector<std::unique_ptr<ReplicationClient>>, utils::SpinLock>;
|
||||
ReplicationClientList replication_clients_;
|
||||
|
||||
std::atomic<replication::ReplicationRole> replication_role_{replication::ReplicationRole::MAIN};
|
||||
ReplicationState replication_state_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -241,13 +241,19 @@ bool InMemoryUniqueConstraints::Entry::operator<(const std::vector<PropertyValue
|
||||
bool InMemoryUniqueConstraints::Entry::operator==(const std::vector<PropertyValue> &rhs) const { return values == rhs; }
|
||||
|
||||
void InMemoryUniqueConstraints::UpdateBeforeCommit(const Vertex *vertex, const Transaction &tx) {
|
||||
for (auto &[label_props, storage] : constraints_) {
|
||||
if (!utils::Contains(vertex->labels, label_props.first)) {
|
||||
for (const auto &label : vertex->labels) {
|
||||
if (!constraints_by_label_.contains(label)) {
|
||||
continue;
|
||||
}
|
||||
auto values = vertex->properties.ExtractPropertyValues(label_props.second);
|
||||
if (values) {
|
||||
auto acc = storage.access();
|
||||
|
||||
for (auto &[props, storage] : constraints_by_label_.at(label)) {
|
||||
auto values = vertex->properties.ExtractPropertyValues(props);
|
||||
|
||||
if (!values) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto acc = storage->access();
|
||||
acc.insert(Entry{std::move(*values), vertex, tx.start_timestamp});
|
||||
}
|
||||
}
|
||||
@@ -303,6 +309,9 @@ InMemoryUniqueConstraints::CreateConstraint(LabelId label, const std::set<Proper
|
||||
constraints_.erase(constraint);
|
||||
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label, properties};
|
||||
}
|
||||
|
||||
// Add the new constraint to the optimized structure only if there are no violations.
|
||||
constraints_by_label_[label].insert({properties, &constraints_.at({label, properties})});
|
||||
return CreationStatus::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -312,7 +321,21 @@ InMemoryUniqueConstraints::DeletionStatus InMemoryUniqueConstraints::DropConstra
|
||||
drop_properties_check_result != UniqueConstraints::DeletionStatus::SUCCESS) {
|
||||
return drop_properties_check_result;
|
||||
}
|
||||
if (constraints_.erase({label, properties}) > 0) {
|
||||
|
||||
auto erase_from_constraints_by_label_ = [this, label, &properties]() -> uint64_t {
|
||||
if (!constraints_by_label_.contains(label)) {
|
||||
return 1; // erase is successful if there’s nothing to erase
|
||||
}
|
||||
|
||||
const auto erase_entry_status = constraints_by_label_[label].erase(properties);
|
||||
if (!constraints_by_label_[label].empty()) {
|
||||
return erase_entry_status;
|
||||
}
|
||||
|
||||
return erase_entry_status > 0 && constraints_by_label_.erase(label) > 0;
|
||||
};
|
||||
|
||||
if (constraints_.erase({label, properties}) > 0 && erase_from_constraints_by_label_() > 0) {
|
||||
return UniqueConstraints::DeletionStatus::SUCCESS;
|
||||
}
|
||||
return UniqueConstraints::DeletionStatus::NOT_FOUND;
|
||||
@@ -327,34 +350,37 @@ std::optional<ConstraintViolation> InMemoryUniqueConstraints::Validate(const Ver
|
||||
if (vertex.deleted) {
|
||||
return std::nullopt;
|
||||
}
|
||||
for (const auto &[label_props, storage] : constraints_) {
|
||||
const auto &label = label_props.first;
|
||||
const auto &properties = label_props.second;
|
||||
if (!utils::Contains(vertex.labels, label)) {
|
||||
for (const auto &label : vertex.labels) {
|
||||
if (!constraints_by_label_.contains(label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto value_array = vertex.properties.ExtractPropertyValues(properties);
|
||||
if (!value_array) {
|
||||
continue;
|
||||
}
|
||||
auto acc = storage.access();
|
||||
auto it = acc.find_equal_or_greater(*value_array);
|
||||
for (; it != acc.end(); ++it) {
|
||||
if (*value_array < it->values) {
|
||||
break;
|
||||
for (const auto &[properties, storage] : constraints_by_label_.at(label)) {
|
||||
auto value_array = vertex.properties.ExtractPropertyValues(properties);
|
||||
|
||||
if (!value_array) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The `vertex` that is going to be committed violates a unique constraint
|
||||
// if it's different than a vertex indexed in the list of constraints and
|
||||
// has the same label and property value as the last committed version of
|
||||
// the vertex from the list.
|
||||
if (&vertex != it->vertex &&
|
||||
LastCommittedVersionHasLabelProperty(*it->vertex, label, properties, *value_array, tx, commit_timestamp)) {
|
||||
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label, properties};
|
||||
auto acc = storage->access();
|
||||
auto it = acc.find_equal_or_greater(*value_array);
|
||||
for (; it != acc.end(); ++it) {
|
||||
if (*value_array < it->values) {
|
||||
break;
|
||||
}
|
||||
|
||||
// The `vertex` that is going to be committed violates a unique constraint
|
||||
// if it's different than a vertex indexed in the list of constraints and
|
||||
// has the same label and property value as the last committed version of
|
||||
// the vertex from the list.
|
||||
if (&vertex != it->vertex &&
|
||||
LastCommittedVersionHasLabelProperty(*it->vertex, label, properties, *value_array, tx, commit_timestamp)) {
|
||||
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label, properties};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -389,6 +415,9 @@ void InMemoryUniqueConstraints::RemoveObsoleteEntries(uint64_t oldest_active_sta
|
||||
}
|
||||
}
|
||||
|
||||
void InMemoryUniqueConstraints::Clear() { constraints_.clear(); }
|
||||
void InMemoryUniqueConstraints::Clear() {
|
||||
constraints_.clear();
|
||||
constraints_by_label_.clear();
|
||||
}
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -96,6 +96,7 @@ class InMemoryUniqueConstraints : public UniqueConstraints {
|
||||
|
||||
private:
|
||||
std::map<std::pair<LabelId, std::set<PropertyId>>, utils::SkipList<Entry>> constraints_;
|
||||
std::map<LabelId, std::map<std::set<PropertyId>, utils::SkipList<Entry> *>> constraints_by_label_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -26,18 +26,22 @@ namespace memgraph::storage {
|
||||
/// that should be applied it calls the callback function with the delta that
|
||||
/// should be applied passed as a parameter to the callback. It is up to the
|
||||
/// caller to apply the deltas.
|
||||
/// @return number of deltas that were processed
|
||||
template <typename TCallback>
|
||||
inline void ApplyDeltasForRead(Transaction *transaction, const Delta *delta, View view, const TCallback &callback) {
|
||||
inline std::size_t ApplyDeltasForRead(Transaction const *transaction, const Delta *delta, View view,
|
||||
const TCallback &callback) {
|
||||
// Avoid work if no deltas or
|
||||
// IsolationLevel::READ_UNCOMMITTED, where deltas are never applied
|
||||
if (!delta || transaction->isolation_level == IsolationLevel::READ_UNCOMMITTED) return 0;
|
||||
|
||||
// if the transaction is not committed, then its deltas have transaction_id for the timestamp, otherwise they have
|
||||
// its commit timestamp set.
|
||||
// This allows the transaction to see its changes even though it's committed.
|
||||
const auto commit_timestamp = transaction->commit_timestamp
|
||||
? transaction->commit_timestamp->load(std::memory_order_acquire)
|
||||
: transaction->transaction_id.load(std::memory_order_acquire);
|
||||
std::size_t n_processed = 0;
|
||||
while (delta != nullptr) {
|
||||
auto ts = delta->timestamp->load(std::memory_order_acquire);
|
||||
auto cid = delta->command_id;
|
||||
|
||||
// For SNAPSHOT ISOLATION -> we can only see the changes which were committed before the start of the current
|
||||
// transaction
|
||||
//
|
||||
@@ -46,15 +50,16 @@ inline void ApplyDeltasForRead(Transaction *transaction, const Delta *delta, Vie
|
||||
// always higher than start or commit timestamps so we know if the timestamp is lower than the initial transaction
|
||||
// id value, that the change is committed.
|
||||
//
|
||||
// For READ UNCOMMITTED -> we accept any change.
|
||||
// For READ UNCOMMITTED -> we accept any change. (already handled above)
|
||||
auto ts = delta->timestamp->load(std::memory_order_acquire);
|
||||
if ((transaction->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION && ts < transaction->start_timestamp) ||
|
||||
(transaction->isolation_level == IsolationLevel::READ_COMMITTED && ts < kTransactionInitialId) ||
|
||||
(transaction->isolation_level == IsolationLevel::READ_UNCOMMITTED)) {
|
||||
(transaction->isolation_level == IsolationLevel::READ_COMMITTED && ts < kTransactionInitialId)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We shouldn't undo our newest changes because the user requested a NEW
|
||||
// view of the database.
|
||||
auto cid = delta->command_id;
|
||||
if (view == View::NEW && ts == commit_timestamp && cid <= transaction->command_id) {
|
||||
break;
|
||||
}
|
||||
@@ -69,10 +74,12 @@ inline void ApplyDeltasForRead(Transaction *transaction, const Delta *delta, Vie
|
||||
|
||||
// This delta must be applied, call the callback.
|
||||
callback(*delta);
|
||||
++n_processed;
|
||||
|
||||
// Move to the next delta.
|
||||
delta = delta->next.load(std::memory_order_acquire);
|
||||
}
|
||||
return n_processed;
|
||||
}
|
||||
|
||||
/// This function prepares the object for a write. It checks whether there are
|
||||
@@ -107,17 +114,22 @@ inline Delta *CreateDeleteObjectDelta(Transaction *transaction) {
|
||||
}
|
||||
|
||||
/// TODO: what if in-memory analytical
|
||||
inline Delta *CreateDeleteDeserializedObjectDelta(Transaction *transaction, std::optional<std::string> old_disk_key) {
|
||||
transaction->EnsureCommitTimestampExists();
|
||||
return &transaction->deltas.emplace_back(Delta::DeleteDeserializedObjectTag(), transaction->commit_timestamp.get(),
|
||||
old_disk_key);
|
||||
inline Delta *CreateDeleteDeserializedObjectDelta(Transaction *transaction, std::optional<std::string> old_disk_key,
|
||||
const std::string &ts) {
|
||||
// Should use utils::DecodeFixed64(ts.c_str()) once we will move to RocksDB real timestamps
|
||||
return &transaction->deltas.emplace_back(Delta::DeleteDeserializedObjectTag(), std::stoull(ts), old_disk_key);
|
||||
}
|
||||
|
||||
inline Delta *CreateDeleteDeserializedIndexObjectDelta(Transaction *transaction, std::list<Delta> &deltas,
|
||||
std::optional<std::string> old_disk_key, const uint64_t ts) {
|
||||
return &deltas.emplace_back(Delta::DeleteDeserializedObjectTag(), ts, old_disk_key);
|
||||
}
|
||||
|
||||
/// TODO: what if in-memory analytical
|
||||
inline Delta *CreateDeleteDeserializedIndexObjectDelta(Transaction *transaction, std::list<Delta> &deltas,
|
||||
std::optional<std::string> old_disk_key) {
|
||||
transaction->EnsureCommitTimestampExists();
|
||||
return &deltas.emplace_back(Delta::DeleteDeserializedObjectTag(), transaction->commit_timestamp.get(), old_disk_key);
|
||||
std::optional<std::string> old_disk_key, const std::string &ts) {
|
||||
// Should use utils::DecodeFixed64(ts.c_str()) once we will move to RocksDB real timestamps
|
||||
return CreateDeleteDeserializedIndexObjectDelta(transaction, deltas, old_disk_key, std::stoull(ts));
|
||||
}
|
||||
|
||||
/// This function creates a delta in the transaction for the object and links
|
||||
|
||||
@@ -1238,6 +1238,32 @@ bool PropertyStore::DoInitProperties(const TContainer &properties) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> PropertyStore::UpdateProperties(
|
||||
std::map<PropertyId, PropertyValue> &properties) {
|
||||
auto old_properties = Properties();
|
||||
ClearProperties();
|
||||
|
||||
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> id_old_new_change;
|
||||
id_old_new_change.reserve(properties.size() + old_properties.size());
|
||||
for (const auto &[prop_id, new_value] : properties) {
|
||||
if (!old_properties.contains(prop_id)) {
|
||||
id_old_new_change.emplace_back(std::make_tuple(prop_id, PropertyValue(), new_value));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[old_key, old_value] : old_properties) {
|
||||
auto [it, inserted] = properties.emplace(old_key, old_value);
|
||||
if (!inserted) {
|
||||
auto &new_value = it->second;
|
||||
id_old_new_change.emplace_back(std::make_tuple(it->first, old_value, new_value));
|
||||
}
|
||||
}
|
||||
|
||||
MG_ASSERT(InitProperties(properties));
|
||||
return id_old_new_change;
|
||||
}
|
||||
|
||||
template bool PropertyStore::DoInitProperties<std::map<PropertyId, PropertyValue>>(
|
||||
const std::map<PropertyId, PropertyValue> &);
|
||||
template bool PropertyStore::DoInitProperties<std::vector<std::pair<PropertyId, PropertyValue>>>(
|
||||
|
||||
@@ -91,6 +91,14 @@ class PropertyStore {
|
||||
/// @throw std::bad_alloc
|
||||
bool InitProperties(std::vector<std::pair<storage::PropertyId, storage::PropertyValue>> properties);
|
||||
|
||||
/// Update property values in property store with sent properties. Returns vector of changed
|
||||
/// properties. Each tuple inside vector consists of PropertyId of inserted property, together with old
|
||||
/// property (if existed or empty PropertyValue if didn't exist) and new property which was inserted.
|
||||
/// The time complexity of this function is O(n*log(n)):
|
||||
/// @throw std::bad_alloc
|
||||
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> UpdateProperties(
|
||||
std::map<storage::PropertyId, storage::PropertyValue> &properties);
|
||||
|
||||
/// Remove all properties and return `true` if any removal took place.
|
||||
/// `false` is returned if there were no properties to remove. The time
|
||||
/// complexity of this function is O(1).
|
||||
|
||||
65
src/storage/v2/replication/global.hpp
Normal file
65
src/storage/v2/replication/global.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "io/network/endpoint.hpp"
|
||||
#include "storage/v2/replication/enums.hpp"
|
||||
#include "utils/uuid.hpp"
|
||||
|
||||
// TODO: move to replication namespace and unify
|
||||
namespace memgraph::storage {
|
||||
|
||||
// TODO: Should be at MAIN instance level; shouldn't be connected to storage
|
||||
struct ReplicationEpoch {
|
||||
ReplicationEpoch() : id(utils::GenerateUUID()) {}
|
||||
|
||||
// UUID to distinguish different main instance runs for replication process
|
||||
// on SAME storage.
|
||||
// Multiple instances can have same storage UUID and be MAIN at the same time.
|
||||
// We cannot compare commit timestamps of those instances if one of them
|
||||
// becomes the replica of the other so we use epoch_id_ as additional
|
||||
// discriminating property.
|
||||
// Example of this:
|
||||
// We have 2 instances of the same storage, S1 and S2.
|
||||
// S1 and S2 are MAIN and accept their own commits and write them to the WAL.
|
||||
// At the moment when S1 commited a transaction with timestamp 20, and S2
|
||||
// a different transaction with timestamp 15, we change S2's role to REPLICA
|
||||
// and register it on S1.
|
||||
// Without using the epoch_id, we don't know that S1 and S2 have completely
|
||||
// different transactions, we think that the S2 is behind only by 5 commits.
|
||||
std::string id; // TODO: Move to replication level
|
||||
|
||||
// Generates a new epoch id, returning the old one
|
||||
std::string NewEpoch() { return std::exchange(id, utils::GenerateUUID()); }
|
||||
|
||||
std::string SetEpoch(std::string new_epoch) { return std::exchange(id, std::move(new_epoch)); }
|
||||
};
|
||||
|
||||
struct TimestampInfo {
|
||||
uint64_t current_timestamp_of_replica;
|
||||
uint64_t current_number_of_timestamp_behind_master;
|
||||
};
|
||||
|
||||
struct ReplicaInfo {
|
||||
std::string name;
|
||||
replication::ReplicationMode mode;
|
||||
io::network::Endpoint endpoint;
|
||||
replication::ReplicaState state;
|
||||
TimestampInfo timestamp_info;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
402
src/storage/v2/replication/replication.cpp
Normal file
402
src/storage/v2/replication/replication.cpp
Normal file
@@ -0,0 +1,402 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#include "storage/v2/replication/replication.hpp"
|
||||
#include "storage/v2/constraints/constraints.hpp"
|
||||
#include "storage/v2/durability/durability.hpp"
|
||||
#include "storage/v2/durability/snapshot.hpp"
|
||||
#include "storage/v2/durability/wal.hpp"
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
#include "storage/v2/edge_direction.hpp"
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
#include "storage/v2/storage_mode.hpp"
|
||||
#include "storage/v2/vertex_accessor.hpp"
|
||||
#include "utils/stat.hpp"
|
||||
|
||||
/// REPLICATION ///
|
||||
#include "storage/v2/replication/replication_client.hpp"
|
||||
#include "storage/v2/replication/replication_server.hpp"
|
||||
#include "storage/v2/replication/rpc.hpp"
|
||||
#include "storage/v2/storage_error.hpp"
|
||||
|
||||
#include "storage/v2/inmemory/replication/replication_client.hpp"
|
||||
#include "storage/v2/inmemory/replication/replication_server.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
using OOMExceptionEnabler = utils::MemoryTracker::OutOfMemoryExceptionEnabler;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string RegisterReplicaErrorToString(ReplicationState::RegisterReplicaError error) {
|
||||
using enum ReplicationState::RegisterReplicaError;
|
||||
switch (error) {
|
||||
case NAME_EXISTS:
|
||||
return "NAME_EXISTS";
|
||||
case END_POINT_EXISTS:
|
||||
return "END_POINT_EXISTS";
|
||||
case CONNECTION_FAILED:
|
||||
return "CONNECTION_FAILED";
|
||||
case COULD_NOT_BE_PERSISTED:
|
||||
return "COULD_NOT_BE_PERSISTED";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
storage::ReplicationState::ReplicationState(bool restore, std::filesystem::path durability_dir) {
|
||||
if (restore) {
|
||||
utils::EnsureDirOrDie(durability_dir / durability::kReplicationDirectory);
|
||||
durability_ = std::make_unique<kvstore::KVStore>(durability_dir / durability::kReplicationDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
void storage::ReplicationState::Reset() {
|
||||
replication_server_.reset();
|
||||
replication_clients_.WithLock([&](auto &clients) { clients.clear(); });
|
||||
}
|
||||
|
||||
bool storage::ReplicationState::SetMainReplicationRole(storage::Storage *storage) {
|
||||
// We don't want to generate new epoch_id and do the
|
||||
// cleanup if we're already a MAIN
|
||||
if (GetRole() == replication::ReplicationRole::MAIN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Main instance does not need replication server
|
||||
// This should be always called first so we finalize everything
|
||||
replication_server_.reset(nullptr);
|
||||
|
||||
storage->EstablishNewEpoch();
|
||||
|
||||
if (ShouldStoreAndRestoreReplicationState()) {
|
||||
// Only thing that matters here is the role saved as MAIN
|
||||
auto data = replication::ReplicationStatusToJSON(
|
||||
replication::ReplicationStatus{.name = replication::kReservedReplicationRoleName,
|
||||
.ip_address = "",
|
||||
.port = 0,
|
||||
.sync_mode = replication::ReplicationMode::SYNC,
|
||||
.replica_check_frequency = std::chrono::seconds(0),
|
||||
.ssl = std::nullopt,
|
||||
.role = replication::ReplicationRole::MAIN});
|
||||
|
||||
if (!durability_->Put(replication::kReservedReplicationRoleName, data.dump())) {
|
||||
spdlog::error("Error when saving MAIN replication role in settings.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SetRole(replication::ReplicationRole::MAIN);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool storage::ReplicationState::AppendToWalDataDefinition(const uint64_t seq_num,
|
||||
durability::StorageGlobalOperation operation, LabelId label,
|
||||
const std::set<PropertyId> &properties,
|
||||
uint64_t final_commit_timestamp) {
|
||||
bool finalized_on_all_replicas = true;
|
||||
// TODO Should we return true if not MAIN?
|
||||
if (GetRole() == replication::ReplicationRole::MAIN) {
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->StartTransactionReplication(seq_num);
|
||||
client->IfStreamingTransaction(
|
||||
[&](auto &stream) { stream.AppendOperation(operation, label, properties, final_commit_timestamp); });
|
||||
|
||||
const auto finalized = client->FinalizeTransactionReplication();
|
||||
if (client->Mode() == replication::ReplicationMode::SYNC) {
|
||||
finalized_on_all_replicas = finalized && finalized_on_all_replicas;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return finalized_on_all_replicas;
|
||||
}
|
||||
|
||||
void storage::ReplicationState::InitializeTransaction(uint64_t seq_num) {
|
||||
if (GetRole() == replication::ReplicationRole::MAIN) {
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->StartTransactionReplication(seq_num);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void storage::ReplicationState::AppendDelta(const Delta &delta, const Edge &parent, uint64_t timestamp) {
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->IfStreamingTransaction([&](auto &stream) { stream.AppendDelta(delta, parent, timestamp); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void storage::ReplicationState::AppendDelta(const Delta &delta, const Vertex &parent, uint64_t timestamp) {
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->IfStreamingTransaction([&](auto &stream) { stream.AppendDelta(delta, parent, timestamp); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool storage::ReplicationState::FinalizeTransaction(uint64_t timestamp) {
|
||||
bool finalized_on_all_replicas = true;
|
||||
replication_clients_.WithLock([&](auto &clients) {
|
||||
for (auto &client : clients) {
|
||||
client->IfStreamingTransaction([&](auto &stream) { stream.AppendTransactionEnd(timestamp); });
|
||||
const auto finalized = client->FinalizeTransactionReplication();
|
||||
|
||||
if (client->Mode() == replication::ReplicationMode::SYNC) {
|
||||
finalized_on_all_replicas = finalized && finalized_on_all_replicas;
|
||||
}
|
||||
}
|
||||
});
|
||||
return finalized_on_all_replicas;
|
||||
}
|
||||
|
||||
utils::BasicResult<ReplicationState::RegisterReplicaError> ReplicationState::RegisterReplica(
|
||||
std::string name, io::network::Endpoint endpoint, const replication::ReplicationMode replication_mode,
|
||||
const replication::RegistrationMode registration_mode, const replication::ReplicationClientConfig &config,
|
||||
Storage *storage) {
|
||||
MG_ASSERT(GetRole() == replication::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(), [&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;
|
||||
}
|
||||
|
||||
if (ShouldStoreAndRestoreReplicationState()) {
|
||||
auto data = replication::ReplicationStatusToJSON(
|
||||
replication::ReplicationStatus{.name = name,
|
||||
.ip_address = endpoint.address,
|
||||
.port = endpoint.port,
|
||||
.sync_mode = replication_mode,
|
||||
.replica_check_frequency = config.replica_check_frequency,
|
||||
.ssl = config.ssl,
|
||||
.role = replication::ReplicationRole::REPLICA});
|
||||
if (!durability_->Put(name, data.dump())) {
|
||||
spdlog::error("Error when saving replica {} in settings.", name);
|
||||
return RegisterReplicaError::COULD_NOT_BE_PERSISTED;
|
||||
}
|
||||
}
|
||||
|
||||
auto client = storage->CreateReplicationClient(std::move(name), endpoint, replication_mode, config);
|
||||
client->Start();
|
||||
|
||||
if (client->State() == replication::ReplicaState::INVALID) {
|
||||
if (replication::RegistrationMode::CAN_BE_INVALID != registration_mode) {
|
||||
return RegisterReplicaError::CONNECTION_FAILED;
|
||||
}
|
||||
|
||||
spdlog::warn("Connection failed when registering replica {}. Replica will still be registered.", client->Name());
|
||||
}
|
||||
|
||||
return replication_clients_.WithLock(
|
||||
[&](auto &clients) -> utils::BasicResult<ReplicationState::RegisterReplicaError> {
|
||||
// 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(),
|
||||
[&](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 {};
|
||||
});
|
||||
}
|
||||
|
||||
bool ReplicationState::SetReplicaRole(io::network::Endpoint endpoint,
|
||||
const replication::ReplicationServerConfig &config, Storage *storage) {
|
||||
// We don't want to restart the server if we're already a REPLICA
|
||||
if (GetRole() == replication::ReplicationRole::REPLICA) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto port = endpoint.port; // assigning because we will move the endpoint
|
||||
replication_server_ = storage->CreateReplicationServer(std::move(endpoint), config);
|
||||
bool res = replication_server_->Start();
|
||||
if (!res) {
|
||||
spdlog::error("Unable to start the replication server.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ShouldStoreAndRestoreReplicationState()) {
|
||||
// Only thing that matters here is the role saved as REPLICA and the listening port
|
||||
auto data = replication::ReplicationStatusToJSON(
|
||||
replication::ReplicationStatus{.name = replication::kReservedReplicationRoleName,
|
||||
.ip_address = "",
|
||||
.port = port,
|
||||
.sync_mode = replication::ReplicationMode::SYNC,
|
||||
.replica_check_frequency = std::chrono::seconds(0),
|
||||
.ssl = std::nullopt,
|
||||
.role = replication::ReplicationRole::REPLICA});
|
||||
|
||||
if (!durability_->Put(replication::kReservedReplicationRoleName, data.dump())) {
|
||||
spdlog::error("Error when saving REPLICA replication role in settings.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SetRole(replication::ReplicationRole::REPLICA);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReplicationState::UnregisterReplica(std::string_view name) {
|
||||
MG_ASSERT(GetRole() == replication::ReplicationRole::MAIN, "Only main instance can unregister a replica!");
|
||||
if (ShouldStoreAndRestoreReplicationState()) {
|
||||
if (!durability_->Delete(name)) {
|
||||
spdlog::error("Error when removing replica {} from settings.", name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return replication_clients_.WithLock([&](auto &clients) {
|
||||
return std::erase_if(clients, [&](const auto &client) { return client->Name() == name; });
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<replication::ReplicaState> ReplicationState::GetReplicaState(const std::string_view name) {
|
||||
return replication_clients_.WithLock([&](auto &clients) -> std::optional<replication::ReplicaState> {
|
||||
const auto client_it =
|
||||
std::find_if(clients.cbegin(), clients.cend(), [name](auto &client) { return client->Name() == name; });
|
||||
if (client_it == clients.cend()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return (*client_it)->State();
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<ReplicaInfo> ReplicationState::ReplicasInfo() {
|
||||
return replication_clients_.WithLock([](auto &clients) {
|
||||
std::vector<ReplicaInfo> replica_info;
|
||||
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->Endpoint(), client->State(), client->GetTimestampInfo()};
|
||||
});
|
||||
return replica_info;
|
||||
});
|
||||
}
|
||||
|
||||
void ReplicationState::RestoreReplicationRole(Storage *storage) {
|
||||
if (!ShouldStoreAndRestoreReplicationState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
spdlog::info("Restoring replication role.");
|
||||
uint16_t port = replication::kDefaultReplicationPort;
|
||||
|
||||
const auto replication_data = durability_->Get(replication::kReservedReplicationRoleName);
|
||||
if (!replication_data.has_value()) {
|
||||
spdlog::debug("Cannot find data needed for restore replication role in persisted metadata.");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto maybe_replication_status = replication::JSONToReplicationStatus(nlohmann::json::parse(*replication_data));
|
||||
if (!maybe_replication_status.has_value()) {
|
||||
LOG_FATAL("Cannot parse previously saved configuration of replication role {}.",
|
||||
replication::kReservedReplicationRoleName);
|
||||
}
|
||||
|
||||
const auto replication_status = *maybe_replication_status;
|
||||
if (!replication_status.role.has_value()) {
|
||||
SetRole(replication::ReplicationRole::MAIN);
|
||||
} else {
|
||||
SetRole(*replication_status.role);
|
||||
port = replication_status.port;
|
||||
}
|
||||
|
||||
if (GetRole() == replication::ReplicationRole::REPLICA) {
|
||||
io::network::Endpoint endpoint(replication::kDefaultReplicationServerIp, port);
|
||||
replication_server_ = storage->CreateReplicationServer(std::move(endpoint), {});
|
||||
bool res = replication_server_->Start();
|
||||
if (!res) {
|
||||
LOG_FATAL("Unable to start the replication server.");
|
||||
}
|
||||
}
|
||||
|
||||
spdlog::info("Replication role restored to {}.",
|
||||
GetRole() == replication::ReplicationRole::MAIN ? "MAIN" : "REPLICA");
|
||||
}
|
||||
|
||||
void ReplicationState::RestoreReplicas(Storage *storage) {
|
||||
if (!ShouldStoreAndRestoreReplicationState()) {
|
||||
return;
|
||||
}
|
||||
spdlog::info("Restoring replicas.");
|
||||
|
||||
for (const auto &[replica_name, replica_data] : *durability_) {
|
||||
spdlog::info("Restoring replica {}.", replica_name);
|
||||
|
||||
const auto maybe_replica_status = replication::JSONToReplicationStatus(nlohmann::json::parse(replica_data));
|
||||
if (!maybe_replica_status.has_value()) {
|
||||
LOG_FATAL("Cannot parse previously saved configuration of replica {}.", replica_name);
|
||||
}
|
||||
|
||||
auto replica_status = *maybe_replica_status;
|
||||
MG_ASSERT(replica_status.name == replica_name, "Expected replica name is '{}', but got '{}'", replica_status.name,
|
||||
replica_name);
|
||||
|
||||
if (replica_name == replication::kReservedReplicationRoleName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto ret =
|
||||
RegisterReplica(std::move(replica_status.name), {std::move(replica_status.ip_address), replica_status.port},
|
||||
replica_status.sync_mode, replication::RegistrationMode::CAN_BE_INVALID,
|
||||
{
|
||||
.replica_check_frequency = replica_status.replica_check_frequency,
|
||||
.ssl = replica_status.ssl,
|
||||
},
|
||||
storage);
|
||||
|
||||
if (ret.HasError()) {
|
||||
MG_ASSERT(RegisterReplicaError::CONNECTION_FAILED != ret.GetError());
|
||||
LOG_FATAL("Failure when restoring replica {}: {}.", replica_name, RegisterReplicaErrorToString(ret.GetError()));
|
||||
}
|
||||
spdlog::info("Replica {} restored.", replica_name);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr uint16_t kEpochHistoryRetention = 1000;
|
||||
|
||||
void ReplicationState::NewEpoch() {
|
||||
// Generate new epoch id and save the last one to the history.
|
||||
if (history.size() == kEpochHistoryRetention) {
|
||||
history.pop_front();
|
||||
}
|
||||
auto prevEpoch = epoch_.NewEpoch();
|
||||
history.emplace_back(std::move(prevEpoch), last_commit_timestamp_);
|
||||
}
|
||||
|
||||
void ReplicationState::AppendEpoch(std::string new_epoch) {
|
||||
auto prevEpoch = epoch_.SetEpoch(std::move(new_epoch));
|
||||
history.emplace_back(std::move(prevEpoch), last_commit_timestamp_);
|
||||
}
|
||||
|
||||
} // namespace memgraph::storage
|
||||
127
src/storage/v2/replication/replication.hpp
Normal file
127
src/storage/v2/replication/replication.hpp
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "storage/v2/inmemory/label_index.hpp"
|
||||
#include "storage/v2/inmemory/label_property_index.hpp"
|
||||
#include "storage/v2/storage.hpp"
|
||||
|
||||
/// REPLICATION ///
|
||||
#include "rpc/server.hpp"
|
||||
#include "storage/v2/replication/config.hpp"
|
||||
#include "storage/v2/replication/enums.hpp"
|
||||
#include "storage/v2/replication/replication_persistence_helper.hpp"
|
||||
#include "storage/v2/replication/rpc.hpp"
|
||||
#include "storage/v2/replication/serialization.hpp"
|
||||
|
||||
#include "storage/v2/replication/global.hpp"
|
||||
|
||||
// TODO use replication namespace
|
||||
namespace memgraph::storage {
|
||||
|
||||
class Storage;
|
||||
class ReplicationServer;
|
||||
class ReplicationClient;
|
||||
|
||||
struct ReplicationState {
|
||||
enum class RegisterReplicaError : uint8_t {
|
||||
NAME_EXISTS,
|
||||
END_POINT_EXISTS,
|
||||
CONNECTION_FAILED,
|
||||
COULD_NOT_BE_PERSISTED
|
||||
};
|
||||
|
||||
// TODO: This mirrors the logic in InMemoryConstructor; make it independent
|
||||
ReplicationState(bool restore, std::filesystem::path durability_dir);
|
||||
|
||||
// Generic API
|
||||
void Reset();
|
||||
// TODO: Just check if server exists -> you are REPLICA
|
||||
replication::ReplicationRole GetRole() const { return replication_role_.load(); }
|
||||
|
||||
bool SetMainReplicationRole(Storage *storage); // Set the instance to MAIN
|
||||
// TODO: ReplicationServer/Client uses Storage* for RPC callbacks
|
||||
bool SetReplicaRole(io::network::Endpoint endpoint, const replication::ReplicationServerConfig &config,
|
||||
Storage *storage); // Sets the instance to REPLICA
|
||||
// Generic restoration
|
||||
void RestoreReplicationRole(Storage *storage);
|
||||
|
||||
// MAIN actually doing the replication
|
||||
bool AppendToWalDataDefinition(uint64_t seq_num, durability::StorageGlobalOperation operation, LabelId label,
|
||||
const std::set<PropertyId> &properties, uint64_t final_commit_timestamp);
|
||||
void InitializeTransaction(uint64_t seq_num);
|
||||
void AppendDelta(const Delta &delta, const Vertex &parent, uint64_t timestamp);
|
||||
void AppendDelta(const Delta &delta, const Edge &parent, uint64_t timestamp);
|
||||
bool FinalizeTransaction(uint64_t timestamp);
|
||||
|
||||
// MAIN connecting to replicas
|
||||
utils::BasicResult<RegisterReplicaError> RegisterReplica(std::string name, io::network::Endpoint endpoint,
|
||||
const replication::ReplicationMode replication_mode,
|
||||
const replication::RegistrationMode registration_mode,
|
||||
const replication::ReplicationClientConfig &config,
|
||||
Storage *storage);
|
||||
bool UnregisterReplica(std::string_view name);
|
||||
|
||||
// MAIN reconnecting to replicas
|
||||
void RestoreReplicas(Storage *storage);
|
||||
|
||||
// MAIN getting info from replicas
|
||||
// TODO make into const (problem with SpinLock and WithReadLock)
|
||||
std::optional<replication::ReplicaState> GetReplicaState(std::string_view name);
|
||||
std::vector<ReplicaInfo> ReplicasInfo();
|
||||
|
||||
const ReplicationEpoch &GetEpoch() const { return epoch_; }
|
||||
ReplicationEpoch &GetEpoch() { return epoch_; }
|
||||
|
||||
// Questions:
|
||||
// - storage durability <- databases/*name*/wal and snapshots (where this for epoch_id)
|
||||
// - multi-tenant durability <- databases/.durability (there is a list of all active tenants)
|
||||
// History of the previous epoch ids.
|
||||
// Each value consists of the epoch id along the last commit belonging to that
|
||||
// epoch.
|
||||
std::deque<std::pair<std::string, uint64_t>> history;
|
||||
|
||||
// TODO: actually durability
|
||||
std::atomic<uint64_t> last_commit_timestamp_{kTimestampInitialId};
|
||||
|
||||
void NewEpoch();
|
||||
void AppendEpoch(std::string new_epoch);
|
||||
|
||||
private:
|
||||
bool ShouldStoreAndRestoreReplicationState() const { return nullptr != durability_; }
|
||||
|
||||
void SetRole(replication::ReplicationRole role) { return replication_role_.store(role); }
|
||||
|
||||
// NOTE: Server is not in MAIN it is in REPLICA
|
||||
std::unique_ptr<ReplicationServer> replication_server_{nullptr};
|
||||
|
||||
// We create ReplicationClient using unique_ptr so we can move
|
||||
// newly created client into the vector.
|
||||
// We cannot move the client directly because it contains ThreadPool
|
||||
// which cannot be moved. Also, the move is necessary because
|
||||
// we don't want to create the client directly inside the vector
|
||||
// because that would require the lock on the list putting all
|
||||
// commits (they iterate list of clients) to halt.
|
||||
// This way we can initialize client in main thread which means
|
||||
// that we can immediately notify the user if the initialization
|
||||
// failed.
|
||||
using ReplicationClientList = utils::Synchronized<std::vector<std::unique_ptr<ReplicationClient>>, utils::SpinLock>;
|
||||
ReplicationClientList replication_clients_;
|
||||
|
||||
std::atomic<replication::ReplicationRole> replication_role_{replication::ReplicationRole::MAIN};
|
||||
|
||||
std::unique_ptr<kvstore::KVStore> durability_;
|
||||
|
||||
ReplicationEpoch epoch_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <type_traits>
|
||||
|
||||
#include "storage/v2/durability/durability.hpp"
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
#include "storage/v2/replication/config.hpp"
|
||||
#include "storage/v2/replication/enums.hpp"
|
||||
#include "storage/v2/transaction.hpp"
|
||||
@@ -24,593 +25,56 @@
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
namespace {
|
||||
template <typename>
|
||||
[[maybe_unused]] inline constexpr bool always_false_v = false;
|
||||
} // namespace
|
||||
|
||||
////// ReplicationClient //////
|
||||
InMemoryStorage::ReplicationClient::ReplicationClient(std::string name, InMemoryStorage *storage,
|
||||
const io::network::Endpoint &endpoint,
|
||||
const replication::ReplicationMode mode,
|
||||
const replication::ReplicationClientConfig &config)
|
||||
: name_(std::move(name)), storage_(storage), mode_(mode) {
|
||||
spdlog::trace("Replication client started at: {}:{}", endpoint.address, endpoint.port);
|
||||
if (config.ssl) {
|
||||
rpc_context_.emplace(config.ssl->key_file, config.ssl->cert_file);
|
||||
} else {
|
||||
rpc_context_.emplace();
|
||||
}
|
||||
|
||||
|
||||
rpc_client_.emplace(endpoint, &*rpc_context_);
|
||||
TryInitializeClientSync();
|
||||
|
||||
// Help the user to get the most accurate replica state possible.
|
||||
if (config.replica_check_frequency > std::chrono::seconds(0)) {
|
||||
replica_checker_.Run("Replica Checker", config.replica_check_frequency, [&] { FrequentCheck(); });
|
||||
}
|
||||
static auto CreateClientContext(const replication::ReplicationClientConfig &config) -> communication::ClientContext {
|
||||
return (config.ssl) ? communication::ClientContext{config.ssl->key_file, config.ssl->cert_file}
|
||||
: communication::ClientContext{};
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::TryInitializeClientAsync() {
|
||||
thread_pool_.AddTask([this] {
|
||||
rpc_client_->Abort();
|
||||
this->TryInitializeClientSync();
|
||||
});
|
||||
}
|
||||
ReplicationClient::ReplicationClient(std::string name, memgraph::io::network::Endpoint endpoint,
|
||||
replication::ReplicationMode mode,
|
||||
replication::ReplicationClientConfig const &config)
|
||||
: name_{std::move(name)},
|
||||
rpc_context_{CreateClientContext(config)},
|
||||
rpc_client_{std::move(endpoint), &rpc_context_},
|
||||
replica_check_frequency_{config.replica_check_frequency},
|
||||
mode_{mode} {}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::FrequentCheck() {
|
||||
const auto is_success = std::invoke([this]() {
|
||||
try {
|
||||
auto stream{rpc_client_->Stream<replication::FrequentHeartbeatRpc>()};
|
||||
const auto response = stream.AwaitResponse();
|
||||
return response.success;
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
// States: READY, REPLICATING, RECOVERY, INVALID
|
||||
// If success && ready, replicating, recovery -> stay the same because something good is going on.
|
||||
// If success && INVALID -> [it's possible that replica came back to life] -> TryInitializeClient.
|
||||
// If fail -> [replica is not reachable at all] -> INVALID state.
|
||||
// NOTE: TryInitializeClient might return nothing if there is a branching point.
|
||||
// NOTE: The early return pattern simplified the code, but the behavior should be as explained.
|
||||
if (!is_success) {
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
return;
|
||||
}
|
||||
if (replica_state_.load() == replication::ReplicaState::INVALID) {
|
||||
TryInitializeClientAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// @throws rpc::RpcFailedException
|
||||
void InMemoryStorage::ReplicationClient::InitializeClient() {
|
||||
uint64_t current_commit_timestamp{kTimestampInitialId};
|
||||
|
||||
std::optional<std::string> epoch_id;
|
||||
{
|
||||
// epoch_id_ can be changed if we don't take this lock
|
||||
std::unique_lock engine_guard(storage_->engine_lock_);
|
||||
epoch_id.emplace(storage_->epoch_id_);
|
||||
}
|
||||
|
||||
auto stream{rpc_client_->Stream<replication::HeartbeatRpc>(storage_->last_commit_timestamp_, std::move(*epoch_id))};
|
||||
|
||||
const auto response = stream.AwaitResponse();
|
||||
std::optional<uint64_t> branching_point;
|
||||
if (response.epoch_id != storage_->epoch_id_ && response.current_commit_timestamp != kTimestampInitialId) {
|
||||
const auto &epoch_history = storage_->epoch_history_;
|
||||
const auto epoch_info_iter =
|
||||
std::find_if(epoch_history.crbegin(), epoch_history.crend(),
|
||||
[&](const auto &epoch_info) { return epoch_info.first == response.epoch_id; });
|
||||
if (epoch_info_iter == epoch_history.crend()) {
|
||||
branching_point = 0;
|
||||
} else if (epoch_info_iter->second != response.current_commit_timestamp) {
|
||||
branching_point = epoch_info_iter->second;
|
||||
}
|
||||
}
|
||||
if (branching_point) {
|
||||
spdlog::error(
|
||||
"You cannot register Replica {} to this Main because at one point "
|
||||
"Replica {} acted as the Main instance. Both the Main and Replica {} "
|
||||
"now hold unique data. Please resolve data conflicts and start the "
|
||||
"replication on a clean instance.",
|
||||
name_, name_, name_);
|
||||
return;
|
||||
}
|
||||
|
||||
current_commit_timestamp = response.current_commit_timestamp;
|
||||
spdlog::trace("Current timestamp on replica {}: {}", name_, current_commit_timestamp);
|
||||
spdlog::trace("Current timestamp on main: {}", storage_->last_commit_timestamp_.load());
|
||||
if (current_commit_timestamp == storage_->last_commit_timestamp_.load()) {
|
||||
spdlog::debug("Replica '{}' up to date", name_);
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::READY);
|
||||
} else {
|
||||
spdlog::debug("Replica '{}' is behind", name_);
|
||||
{
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::RECOVERY);
|
||||
}
|
||||
thread_pool_.AddTask([=, this] { this->RecoverReplica(current_commit_timestamp); });
|
||||
}
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::TryInitializeClientSync() {
|
||||
try {
|
||||
InitializeClient();
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
std::unique_lock client_guarde{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
spdlog::error(utils::MessageWithLink("Failed to connect to replica {} at the endpoint {}.", name_,
|
||||
rpc_client_->Endpoint(), "https://memgr.ph/replication"));
|
||||
}
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::HandleRpcFailure() {
|
||||
spdlog::error(utils::MessageWithLink("Couldn't replicate data to {}.", name_, "https://memgr.ph/replication"));
|
||||
TryInitializeClientAsync();
|
||||
}
|
||||
|
||||
replication::SnapshotRes InMemoryStorage::ReplicationClient::TransferSnapshot(const std::filesystem::path &path) {
|
||||
auto stream{rpc_client_->Stream<replication::SnapshotRpc>()};
|
||||
replication::Encoder encoder(stream.GetBuilder());
|
||||
encoder.WriteFile(path);
|
||||
return stream.AwaitResponse();
|
||||
}
|
||||
|
||||
replication::WalFilesRes InMemoryStorage::ReplicationClient::TransferWalFiles(
|
||||
const std::vector<std::filesystem::path> &wal_files) {
|
||||
MG_ASSERT(!wal_files.empty(), "Wal files list is empty!");
|
||||
auto stream{rpc_client_->Stream<replication::WalFilesRpc>(wal_files.size())};
|
||||
replication::Encoder encoder(stream.GetBuilder());
|
||||
for (const auto &wal : wal_files) {
|
||||
spdlog::debug("Sending wal file: {}", wal);
|
||||
encoder.WriteFile(wal);
|
||||
}
|
||||
|
||||
return stream.AwaitResponse();
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::StartTransactionReplication(const uint64_t current_wal_seq_num) {
|
||||
std::unique_lock guard(client_lock_);
|
||||
const auto status = replica_state_.load();
|
||||
switch (status) {
|
||||
case replication::ReplicaState::RECOVERY:
|
||||
spdlog::debug("Replica {} is behind MAIN instance", name_);
|
||||
return;
|
||||
case replication::ReplicaState::REPLICATING:
|
||||
spdlog::debug("Replica {} missed a transaction", name_);
|
||||
// We missed a transaction because we're still replicating
|
||||
// the previous transaction so we need to go to RECOVERY
|
||||
// state to catch up with the missing transaction
|
||||
// We cannot queue the recovery process here because
|
||||
// an error can happen while we're replicating the previous
|
||||
// transaction after which the client should go to
|
||||
// INVALID state before starting the recovery process
|
||||
replica_state_.store(replication::ReplicaState::RECOVERY);
|
||||
return;
|
||||
case replication::ReplicaState::INVALID:
|
||||
HandleRpcFailure();
|
||||
return;
|
||||
case replication::ReplicaState::READY:
|
||||
MG_ASSERT(!replica_stream_);
|
||||
try {
|
||||
replica_stream_.emplace(ReplicaStream{this, storage_->last_commit_timestamp_.load(), current_wal_seq_num});
|
||||
replica_state_.store(replication::ReplicaState::REPLICATING);
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
HandleRpcFailure();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::IfStreamingTransaction(
|
||||
const std::function<void(ReplicaStream &handler)> &callback) {
|
||||
// We can only check the state because it guarantees to be only
|
||||
// valid during a single transaction replication (if the assumption
|
||||
// that this and other transaction replication functions can only be
|
||||
// called from a one thread stands)
|
||||
if (replica_state_ != replication::ReplicaState::REPLICATING) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callback(*replica_stream_);
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
{
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
}
|
||||
HandleRpcFailure();
|
||||
}
|
||||
}
|
||||
|
||||
bool InMemoryStorage::ReplicationClient::FinalizeTransactionReplication() {
|
||||
// We can only check the state because it guarantees to be only
|
||||
// valid during a single transaction replication (if the assumption
|
||||
// that this and other transaction replication functions can only be
|
||||
// called from a one thread stands)
|
||||
if (replica_state_ != replication::ReplicaState::REPLICATING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode_ == replication::ReplicationMode::ASYNC) {
|
||||
thread_pool_.AddTask([this] { static_cast<void>(this->FinalizeTransactionReplicationInternal()); });
|
||||
return true;
|
||||
} else {
|
||||
return FinalizeTransactionReplicationInternal();
|
||||
}
|
||||
}
|
||||
|
||||
bool InMemoryStorage::ReplicationClient::FinalizeTransactionReplicationInternal() {
|
||||
MG_ASSERT(replica_stream_, "Missing stream for transaction deltas");
|
||||
try {
|
||||
auto response = replica_stream_->Finalize();
|
||||
replica_stream_.reset();
|
||||
std::unique_lock client_guard(client_lock_);
|
||||
if (!response.success || replica_state_ == replication::ReplicaState::RECOVERY) {
|
||||
replica_state_.store(replication::ReplicaState::RECOVERY);
|
||||
thread_pool_.AddTask([&, this] { this->RecoverReplica(response.current_commit_timestamp); });
|
||||
} else {
|
||||
replica_state_.store(replication::ReplicaState::READY);
|
||||
return true;
|
||||
}
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
replica_stream_.reset();
|
||||
{
|
||||
std::unique_lock client_guard(client_lock_);
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
}
|
||||
HandleRpcFailure();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::RecoverReplica(uint64_t replica_commit) {
|
||||
spdlog::debug("Starting replica recover");
|
||||
while (true) {
|
||||
auto file_locker = storage_->file_retainer_.AddLocker();
|
||||
|
||||
const auto steps = GetRecoverySteps(replica_commit, &file_locker);
|
||||
int i = 0;
|
||||
for (const auto &recovery_step : steps) {
|
||||
spdlog::trace("Recovering in step: {}", i++);
|
||||
try {
|
||||
std::visit(
|
||||
[&, this]<typename T>(T &&arg) {
|
||||
using StepType = std::remove_cvref_t<T>;
|
||||
if constexpr (std::is_same_v<StepType, RecoverySnapshot>) {
|
||||
spdlog::debug("Sending the latest snapshot file: {}", arg);
|
||||
auto response = TransferSnapshot(arg);
|
||||
replica_commit = response.current_commit_timestamp;
|
||||
} else if constexpr (std::is_same_v<StepType, RecoveryWals>) {
|
||||
spdlog::debug("Sending the latest wal files");
|
||||
auto response = TransferWalFiles(arg);
|
||||
replica_commit = response.current_commit_timestamp;
|
||||
spdlog::debug("Wal files successfully transferred.");
|
||||
} else if constexpr (std::is_same_v<StepType, RecoveryCurrentWal>) {
|
||||
std::unique_lock transaction_guard(storage_->engine_lock_);
|
||||
if (storage_->wal_file_ && storage_->wal_file_->SequenceNumber() == arg.current_wal_seq_num) {
|
||||
storage_->wal_file_->DisableFlushing();
|
||||
transaction_guard.unlock();
|
||||
spdlog::debug("Sending current wal file");
|
||||
replica_commit = ReplicateCurrentWal();
|
||||
storage_->wal_file_->EnableFlushing();
|
||||
} else {
|
||||
spdlog::debug("Cannot recover using current wal file");
|
||||
}
|
||||
} else {
|
||||
static_assert(always_false_v<T>, "Missing type from variant visitor");
|
||||
}
|
||||
},
|
||||
recovery_step);
|
||||
} catch (const rpc::RpcFailedException &) {
|
||||
{
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
replica_state_.store(replication::ReplicaState::INVALID);
|
||||
}
|
||||
HandleRpcFailure();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
spdlog::trace("Current timestamp on replica: {}", replica_commit);
|
||||
// To avoid the situation where we read a correct commit timestamp in
|
||||
// one thread, and after that another thread commits a different a
|
||||
// transaction and THEN we set the state to READY in the first thread,
|
||||
// we set this lock before checking the timestamp.
|
||||
// We will detect that the state is invalid during the next commit,
|
||||
// because replication::AppendDeltasRpc sends the last commit timestamp which
|
||||
// replica checks if it's the same last commit timestamp it received
|
||||
// and we will go to recovery.
|
||||
// By adding this lock, we can avoid that, and go to RECOVERY immediately.
|
||||
std::unique_lock client_guard{client_lock_};
|
||||
SPDLOG_INFO("Replica timestamp: {}", replica_commit);
|
||||
SPDLOG_INFO("Last commit: {}", storage_->last_commit_timestamp_);
|
||||
if (storage_->last_commit_timestamp_.load() == replica_commit) {
|
||||
replica_state_.store(replication::ReplicaState::READY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t InMemoryStorage::ReplicationClient::ReplicateCurrentWal() {
|
||||
const auto &wal_file = storage_->wal_file_;
|
||||
auto stream = TransferCurrentWalFile();
|
||||
stream.AppendFilename(wal_file->Path().filename());
|
||||
utils::InputFile file;
|
||||
MG_ASSERT(file.Open(storage_->wal_file_->Path()), "Failed to open current WAL file!");
|
||||
const auto [buffer, buffer_size] = wal_file->CurrentFileBuffer();
|
||||
stream.AppendSize(file.GetSize() + buffer_size);
|
||||
stream.AppendFileData(&file);
|
||||
stream.AppendBufferData(buffer, buffer_size);
|
||||
auto response = stream.Finalize();
|
||||
return response.current_commit_timestamp;
|
||||
}
|
||||
|
||||
/// This method tries to find the optimal path for recoverying a single replica.
|
||||
/// Based on the last commit transfered to replica it tries to update the
|
||||
/// replica using durability files - WALs and Snapshots. WAL files are much
|
||||
/// smaller in size as they contain only the Deltas (changes) made during the
|
||||
/// transactions while Snapshots contain all the data. For that reason we prefer
|
||||
/// WALs as much as possible. As the WAL file that is currently being updated
|
||||
/// can change during the process we ignore it as much as possible. Also, it
|
||||
/// uses the transaction lock so locking it can be really expensive. After we
|
||||
/// fetch the list of finalized WALs, we try to find the longest chain of
|
||||
/// sequential WALs, starting from the latest one, that will update the recovery
|
||||
/// with the all missed updates. If the WAL chain cannot be created, replica is
|
||||
/// behind by a lot, so we use the regular recovery process, we send the latest
|
||||
/// snapshot and all the necessary WAL files, starting from the newest WAL that
|
||||
/// contains a timestamp before the snapshot. If we registered the existence of
|
||||
/// the current WAL, we add the sequence number we read from it to the recovery
|
||||
/// process. After all the other steps are finished, if the current WAL contains
|
||||
/// the same sequence number, it's the same WAL we read while fetching the
|
||||
/// recovery steps, so we can safely send it to the replica.
|
||||
/// We assume that the property of preserving at least 1 WAL before the snapshot
|
||||
/// is satisfied as we extract the timestamp information from it.
|
||||
std::vector<InMemoryStorage::ReplicationClient::RecoveryStep> InMemoryStorage::ReplicationClient::GetRecoverySteps(
|
||||
const uint64_t replica_commit, utils::FileRetainer::FileLocker *file_locker) {
|
||||
// First check if we can recover using the current wal file only
|
||||
// otherwise save the seq_num of the current wal file
|
||||
// This lock is also necessary to force the missed transaction to finish.
|
||||
std::optional<uint64_t> current_wal_seq_num;
|
||||
std::optional<uint64_t> current_wal_from_timestamp;
|
||||
if (std::unique_lock transtacion_guard(storage_->engine_lock_); storage_->wal_file_) {
|
||||
current_wal_seq_num.emplace(storage_->wal_file_->SequenceNumber());
|
||||
current_wal_from_timestamp.emplace(storage_->wal_file_->FromTimestamp());
|
||||
}
|
||||
|
||||
auto locker_acc = file_locker->Access();
|
||||
auto wal_files = durability::GetWalFiles(storage_->wal_directory_, storage_->uuid_, current_wal_seq_num);
|
||||
MG_ASSERT(wal_files, "Wal files could not be loaded");
|
||||
|
||||
auto snapshot_files = durability::GetSnapshotFiles(storage_->snapshot_directory_, storage_->uuid_);
|
||||
std::optional<durability::SnapshotDurabilityInfo> latest_snapshot;
|
||||
if (!snapshot_files.empty()) {
|
||||
std::sort(snapshot_files.begin(), snapshot_files.end());
|
||||
latest_snapshot.emplace(std::move(snapshot_files.back()));
|
||||
}
|
||||
|
||||
std::vector<RecoveryStep> recovery_steps;
|
||||
|
||||
// No finalized WAL files were found. This means the difference is contained
|
||||
// inside the current WAL or the snapshot.
|
||||
if (wal_files->empty()) {
|
||||
if (current_wal_from_timestamp && replica_commit >= *current_wal_from_timestamp) {
|
||||
MG_ASSERT(current_wal_seq_num);
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
// Without the finalized WAL containing the current timestamp of replica,
|
||||
// we cannot know if the difference is only in the current WAL or we need
|
||||
// to send the snapshot.
|
||||
if (latest_snapshot) {
|
||||
const auto lock_success = locker_acc.AddPath(latest_snapshot->path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
recovery_steps.emplace_back(std::in_place_type_t<RecoverySnapshot>{}, std::move(latest_snapshot->path));
|
||||
}
|
||||
// if there are no finalized WAL files, snapshot left the current WAL
|
||||
// as the WAL file containing a transaction before snapshot creation
|
||||
// so we can be sure that the current WAL is present
|
||||
MG_ASSERT(current_wal_seq_num);
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
// Find the longest chain of WALs for recovery.
|
||||
// The chain consists ONLY of sequential WALs.
|
||||
auto rwal_it = wal_files->rbegin();
|
||||
|
||||
// if the last finalized WAL is before the replica commit
|
||||
// then we can recovery only from current WAL
|
||||
if (rwal_it->to_timestamp <= replica_commit) {
|
||||
MG_ASSERT(current_wal_seq_num);
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
uint64_t previous_seq_num{rwal_it->seq_num};
|
||||
for (; rwal_it != wal_files->rend(); ++rwal_it) {
|
||||
// If the difference between two consecutive wal files is not 0 or 1
|
||||
// we have a missing WAL in our chain
|
||||
if (previous_seq_num - rwal_it->seq_num > 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Find first WAL that contains up to replica commit, i.e. WAL
|
||||
// that is before the replica commit or conatins the replica commit
|
||||
// as the last committed transaction OR we managed to find the first WAL
|
||||
// file.
|
||||
if (replica_commit >= rwal_it->from_timestamp || rwal_it->seq_num == 0) {
|
||||
if (replica_commit >= rwal_it->to_timestamp) {
|
||||
// We want the WAL after because the replica already contains all the
|
||||
// commits from this WAL
|
||||
--rwal_it;
|
||||
}
|
||||
std::vector<std::filesystem::path> wal_chain;
|
||||
auto distance_from_first = std::distance(rwal_it, wal_files->rend() - 1);
|
||||
// We have managed to create WAL chain
|
||||
// We need to lock these files and add them to the chain
|
||||
for (auto result_wal_it = wal_files->begin() + distance_from_first; result_wal_it != wal_files->end();
|
||||
++result_wal_it) {
|
||||
const auto lock_success = locker_acc.AddPath(result_wal_it->path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
wal_chain.push_back(std::move(result_wal_it->path));
|
||||
}
|
||||
|
||||
recovery_steps.emplace_back(std::in_place_type_t<RecoveryWals>{}, std::move(wal_chain));
|
||||
|
||||
if (current_wal_seq_num) {
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
}
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
previous_seq_num = rwal_it->seq_num;
|
||||
}
|
||||
|
||||
MG_ASSERT(latest_snapshot, "Invalid durability state, missing snapshot");
|
||||
// We didn't manage to find a WAL chain, we need to send the latest snapshot
|
||||
// with its WALs
|
||||
const auto lock_success = locker_acc.AddPath(latest_snapshot->path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
recovery_steps.emplace_back(std::in_place_type_t<RecoverySnapshot>{}, std::move(latest_snapshot->path));
|
||||
|
||||
std::vector<std::filesystem::path> recovery_wal_files;
|
||||
auto wal_it = wal_files->begin();
|
||||
for (; wal_it != wal_files->end(); ++wal_it) {
|
||||
// Assuming recovery process is correct the snashpot should
|
||||
// always retain a single WAL that contains a transaction
|
||||
// before its creation
|
||||
if (latest_snapshot->start_timestamp < wal_it->to_timestamp) {
|
||||
if (latest_snapshot->start_timestamp < wal_it->from_timestamp) {
|
||||
MG_ASSERT(wal_it != wal_files->begin(), "Invalid durability files state");
|
||||
--wal_it;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (; wal_it != wal_files->end(); ++wal_it) {
|
||||
const auto lock_success = locker_acc.AddPath(wal_it->path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
recovery_wal_files.push_back(std::move(wal_it->path));
|
||||
}
|
||||
|
||||
// We only have a WAL before the snapshot
|
||||
if (recovery_wal_files.empty()) {
|
||||
const auto lock_success = locker_acc.AddPath(wal_files->back().path);
|
||||
MG_ASSERT(!lock_success.HasError(), "Tried to lock a nonexistant path.");
|
||||
recovery_wal_files.push_back(std::move(wal_files->back().path));
|
||||
}
|
||||
|
||||
recovery_steps.emplace_back(std::in_place_type_t<RecoveryWals>{}, std::move(recovery_wal_files));
|
||||
|
||||
if (current_wal_seq_num) {
|
||||
recovery_steps.emplace_back(RecoveryCurrentWal{*current_wal_seq_num});
|
||||
}
|
||||
|
||||
return recovery_steps;
|
||||
}
|
||||
|
||||
InMemoryStorage::TimestampInfo InMemoryStorage::ReplicationClient::GetTimestampInfo() {
|
||||
InMemoryStorage::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;
|
||||
ReplicationClient::~ReplicationClient() {
|
||||
auto endpoint = rpc_client_.Endpoint();
|
||||
spdlog::trace("Closing replication client on {}:{}", endpoint.address, endpoint.port);
|
||||
}
|
||||
|
||||
////// ReplicaStream //////
|
||||
InMemoryStorage::ReplicationClient::ReplicaStream::ReplicaStream(ReplicationClient *self,
|
||||
const uint64_t previous_commit_timestamp,
|
||||
const uint64_t current_seq_num)
|
||||
ReplicaStream::ReplicaStream(ReplicationClient *self, const uint64_t previous_commit_timestamp,
|
||||
const uint64_t current_seq_num)
|
||||
: self_(self),
|
||||
stream_(self_->rpc_client_->Stream<replication::AppendDeltasRpc>(previous_commit_timestamp, current_seq_num)) {
|
||||
stream_(self_->rpc_client_.Stream<replication::AppendDeltasRpc>(previous_commit_timestamp, current_seq_num)) {
|
||||
replication::Encoder encoder{stream_.GetBuilder()};
|
||||
encoder.WriteString(self_->storage_->epoch_id_);
|
||||
encoder.WriteString(self_->GetEpochId());
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::ReplicaStream::AppendDelta(const Delta &delta, const Vertex &vertex,
|
||||
uint64_t final_commit_timestamp) {
|
||||
void ReplicaStream::AppendDelta(const Delta &delta, const Vertex &vertex, uint64_t final_commit_timestamp) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
EncodeDelta(&encoder, self_->storage_->name_id_mapper_.get(), self_->storage_->config_.items, delta, vertex,
|
||||
final_commit_timestamp);
|
||||
auto *storage = self_->GetStorage();
|
||||
EncodeDelta(&encoder, storage->name_id_mapper_.get(), storage->config_.items, delta, vertex, final_commit_timestamp);
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::ReplicaStream::AppendDelta(const Delta &delta, const Edge &edge,
|
||||
uint64_t final_commit_timestamp) {
|
||||
void ReplicaStream::AppendDelta(const Delta &delta, const Edge &edge, uint64_t final_commit_timestamp) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
EncodeDelta(&encoder, self_->storage_->name_id_mapper_.get(), delta, edge, final_commit_timestamp);
|
||||
EncodeDelta(&encoder, self_->GetStorage()->name_id_mapper_.get(), delta, edge, final_commit_timestamp);
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::ReplicaStream::AppendTransactionEnd(uint64_t final_commit_timestamp) {
|
||||
void ReplicaStream::AppendTransactionEnd(uint64_t final_commit_timestamp) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
EncodeTransactionEnd(&encoder, final_commit_timestamp);
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::ReplicaStream::AppendOperation(durability::StorageGlobalOperation operation,
|
||||
LabelId label,
|
||||
const std::set<PropertyId> &properties,
|
||||
uint64_t timestamp) {
|
||||
void ReplicaStream::AppendOperation(durability::StorageGlobalOperation operation, LabelId label,
|
||||
const std::set<PropertyId> &properties, uint64_t timestamp) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
EncodeOperation(&encoder, self_->storage_->name_id_mapper_.get(), operation, label, properties, timestamp);
|
||||
EncodeOperation(&encoder, self_->GetStorage()->name_id_mapper_.get(), operation, label, properties, timestamp);
|
||||
}
|
||||
|
||||
replication::AppendDeltasRes InMemoryStorage::ReplicationClient::ReplicaStream::Finalize() {
|
||||
return stream_.AwaitResponse();
|
||||
}
|
||||
replication::AppendDeltasRes ReplicaStream::Finalize() { return stream_.AwaitResponse(); }
|
||||
|
||||
////// CurrentWalHandler //////
|
||||
InMemoryStorage::ReplicationClient::CurrentWalHandler::CurrentWalHandler(ReplicationClient *self)
|
||||
: self_(self), stream_(self_->rpc_client_->Stream<replication::CurrentWalRpc>()) {}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::CurrentWalHandler::AppendFilename(const std::string &filename) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
encoder.WriteString(filename);
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::CurrentWalHandler::AppendSize(const size_t size) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
encoder.WriteUint(size);
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::CurrentWalHandler::AppendFileData(utils::InputFile *file) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
encoder.WriteFileData(file);
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationClient::CurrentWalHandler::AppendBufferData(const uint8_t *buffer,
|
||||
const size_t buffer_size) {
|
||||
replication::Encoder encoder(stream_.GetBuilder());
|
||||
encoder.WriteBuffer(buffer, buffer_size);
|
||||
}
|
||||
|
||||
replication::CurrentWalRes InMemoryStorage::ReplicationClient::CurrentWalHandler::Finalize() {
|
||||
return stream_.AwaitResponse();
|
||||
}
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -11,114 +11,65 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <variant>
|
||||
|
||||
#include "rpc/client.hpp"
|
||||
#include "storage/v2/config.hpp"
|
||||
#include "storage/v2/delta.hpp"
|
||||
#include "storage/v2/durability/wal.hpp"
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
#include "storage/v2/mvcc.hpp"
|
||||
#include "storage/v2/name_id_mapper.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/replication/config.hpp"
|
||||
#include "storage/v2/replication/enums.hpp"
|
||||
#include "storage/v2/replication/global.hpp"
|
||||
#include "storage/v2/replication/rpc.hpp"
|
||||
#include "storage/v2/replication/serialization.hpp"
|
||||
#include "storage/v2/vertex.hpp"
|
||||
#include "utils/file.hpp"
|
||||
#include "utils/file_locker.hpp"
|
||||
#include "utils/spin_lock.hpp"
|
||||
#include "utils/synchronized.hpp"
|
||||
#include "utils/scheduler.hpp"
|
||||
#include "utils/thread_pool.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
class InMemoryStorage::ReplicationClient {
|
||||
class Storage;
|
||||
|
||||
class ReplicationClient;
|
||||
|
||||
// Handler used for transfering the current transaction.
|
||||
class ReplicaStream {
|
||||
public:
|
||||
ReplicationClient(std::string name, InMemoryStorage *storage, const io::network::Endpoint &endpoint,
|
||||
replication::ReplicationMode mode, const replication::ReplicationClientConfig &config = {});
|
||||
explicit ReplicaStream(ReplicationClient *self, uint64_t previous_commit_timestamp, uint64_t current_seq_num);
|
||||
|
||||
~ReplicationClient() {
|
||||
auto endpoint = rpc_client_->Endpoint();
|
||||
spdlog::trace("Closing replication client on {}:{}", endpoint.address, endpoint.port);
|
||||
}
|
||||
/// @throw rpc::RpcFailedException
|
||||
void AppendDelta(const Delta &delta, const Vertex &vertex, uint64_t final_commit_timestamp);
|
||||
|
||||
// Handler used for transfering the current transaction.
|
||||
class ReplicaStream {
|
||||
private:
|
||||
friend class ReplicationClient;
|
||||
explicit ReplicaStream(ReplicationClient *self, uint64_t previous_commit_timestamp, uint64_t current_seq_num);
|
||||
/// @throw rpc::RpcFailedException
|
||||
void AppendDelta(const Delta &delta, const Edge &edge, uint64_t final_commit_timestamp);
|
||||
|
||||
public:
|
||||
/// @throw rpc::RpcFailedException
|
||||
void AppendDelta(const Delta &delta, const Vertex &vertex, uint64_t final_commit_timestamp);
|
||||
/// @throw rpc::RpcFailedException
|
||||
void AppendTransactionEnd(uint64_t final_commit_timestamp);
|
||||
|
||||
/// @throw rpc::RpcFailedException
|
||||
void AppendDelta(const Delta &delta, const Edge &edge, uint64_t final_commit_timestamp);
|
||||
/// @throw rpc::RpcFailedException
|
||||
void AppendOperation(durability::StorageGlobalOperation operation, LabelId label,
|
||||
const std::set<PropertyId> &properties, uint64_t timestamp);
|
||||
|
||||
/// @throw rpc::RpcFailedException
|
||||
void AppendTransactionEnd(uint64_t final_commit_timestamp);
|
||||
/// @throw rpc::RpcFailedException
|
||||
replication::AppendDeltasRes Finalize();
|
||||
|
||||
/// @throw rpc::RpcFailedException
|
||||
void AppendOperation(durability::StorageGlobalOperation operation, LabelId label,
|
||||
const std::set<PropertyId> &properties, uint64_t timestamp);
|
||||
private:
|
||||
ReplicationClient *self_;
|
||||
rpc::Client::StreamHandler<replication::AppendDeltasRpc> stream_;
|
||||
};
|
||||
|
||||
private:
|
||||
/// @throw rpc::RpcFailedException
|
||||
replication::AppendDeltasRes Finalize();
|
||||
class ReplicationClient {
|
||||
friend class CurrentWalHandler;
|
||||
friend class ReplicaStream;
|
||||
|
||||
ReplicationClient *self_;
|
||||
rpc::Client::StreamHandler<replication::AppendDeltasRpc> stream_;
|
||||
};
|
||||
public:
|
||||
ReplicationClient(std::string name, memgraph::io::network::Endpoint endpoint, replication::ReplicationMode mode,
|
||||
const replication::ReplicationClientConfig &config);
|
||||
|
||||
// Handler for transfering the current WAL file whose data is
|
||||
// contained in the internal buffer and the file.
|
||||
class CurrentWalHandler {
|
||||
private:
|
||||
friend class ReplicationClient;
|
||||
explicit CurrentWalHandler(ReplicationClient *self);
|
||||
ReplicationClient(ReplicationClient const &) = delete;
|
||||
ReplicationClient &operator=(ReplicationClient const &) = delete;
|
||||
ReplicationClient(ReplicationClient &&) noexcept = delete;
|
||||
ReplicationClient &operator=(ReplicationClient &&) noexcept = delete;
|
||||
|
||||
public:
|
||||
void AppendFilename(const std::string &filename);
|
||||
virtual ~ReplicationClient();
|
||||
|
||||
void AppendSize(size_t size);
|
||||
|
||||
void AppendFileData(utils::InputFile *file);
|
||||
|
||||
void AppendBufferData(const uint8_t *buffer, size_t buffer_size);
|
||||
|
||||
/// @throw rpc::RpcFailedException
|
||||
replication::CurrentWalRes Finalize();
|
||||
|
||||
private:
|
||||
ReplicationClient *self_;
|
||||
rpc::Client::StreamHandler<replication::CurrentWalRpc> stream_;
|
||||
};
|
||||
|
||||
void StartTransactionReplication(uint64_t current_wal_seq_num);
|
||||
|
||||
// Replication clients can be removed at any point
|
||||
// so to avoid any complexity of checking if the client was removed whenever
|
||||
// we want to send part of transaction and to avoid adding some GC logic this
|
||||
// function will run a callback if, after previously callling
|
||||
// StartTransactionReplication, stream is created.
|
||||
void IfStreamingTransaction(const std::function<void(ReplicaStream &handler)> &callback);
|
||||
|
||||
// Return whether the transaction could be finalized on the replication client or not.
|
||||
[[nodiscard]] bool FinalizeTransactionReplication();
|
||||
|
||||
// Transfer the snapshot file.
|
||||
// @param path Path of the snapshot file.
|
||||
replication::SnapshotRes TransferSnapshot(const std::filesystem::path &path);
|
||||
|
||||
CurrentWalHandler TransferCurrentWalFile() { return CurrentWalHandler{this}; }
|
||||
|
||||
// Transfer the WAL files
|
||||
replication::WalFilesRes TransferWalFiles(const std::vector<std::filesystem::path> &wal_files);
|
||||
virtual void Start() = 0;
|
||||
|
||||
const auto &Name() const { return name_; }
|
||||
|
||||
@@ -126,38 +77,20 @@ class InMemoryStorage::ReplicationClient {
|
||||
|
||||
auto Mode() const { return mode_; }
|
||||
|
||||
const auto &Endpoint() const { return rpc_client_->Endpoint(); }
|
||||
auto Endpoint() const -> io::network::Endpoint const & { return rpc_client_.Endpoint(); }
|
||||
|
||||
InMemoryStorage::TimestampInfo GetTimestampInfo();
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool FinalizeTransactionReplicationInternal();
|
||||
|
||||
void RecoverReplica(uint64_t replica_commit);
|
||||
|
||||
uint64_t ReplicateCurrentWal();
|
||||
|
||||
using RecoveryWals = std::vector<std::filesystem::path>;
|
||||
struct RecoveryCurrentWal {
|
||||
uint64_t current_wal_seq_num;
|
||||
|
||||
explicit RecoveryCurrentWal(const uint64_t current_wal_seq_num) : current_wal_seq_num(current_wal_seq_num) {}
|
||||
};
|
||||
using RecoverySnapshot = std::filesystem::path;
|
||||
using RecoveryStep = std::variant<RecoverySnapshot, RecoveryWals, RecoveryCurrentWal>;
|
||||
|
||||
std::vector<RecoveryStep> GetRecoverySteps(uint64_t replica_commit, utils::FileRetainer::FileLocker *file_locker);
|
||||
|
||||
void FrequentCheck();
|
||||
void InitializeClient();
|
||||
void TryInitializeClientSync();
|
||||
void TryInitializeClientAsync();
|
||||
void HandleRpcFailure();
|
||||
virtual void StartTransactionReplication(uint64_t current_wal_seq_num) = 0;
|
||||
virtual void IfStreamingTransaction(const std::function<void(ReplicaStream &)> &callback) = 0;
|
||||
virtual auto GetEpochId() const -> std::string const & = 0; // TODO: make non-virtual once epoch is moved to storage
|
||||
virtual auto GetStorage() -> Storage * = 0;
|
||||
[[nodiscard]] virtual bool FinalizeTransactionReplication() = 0;
|
||||
virtual TimestampInfo GetTimestampInfo() = 0;
|
||||
|
||||
protected:
|
||||
std::string name_;
|
||||
InMemoryStorage *storage_;
|
||||
std::optional<communication::ClientContext> rpc_context_;
|
||||
std::optional<rpc::Client> rpc_client_;
|
||||
communication::ClientContext rpc_context_;
|
||||
rpc::Client rpc_client_;
|
||||
std::chrono::seconds replica_check_frequency_;
|
||||
|
||||
std::optional<ReplicaStream> replica_stream_;
|
||||
replication::ReplicationMode mode_{replication::ReplicationMode::SYNC};
|
||||
|
||||
@@ -9,610 +9,52 @@
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
#include "storage/v2/delta.hpp"
|
||||
#include "storage/v2/durability/durability.hpp"
|
||||
#include "storage/v2/durability/paths.hpp"
|
||||
#include "storage/v2/durability/serialization.hpp"
|
||||
#include "storage/v2/durability/snapshot.hpp"
|
||||
#include "storage/v2/durability/version.hpp"
|
||||
#include "storage/v2/durability/wal.hpp"
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
#include "storage/v2/inmemory/unique_constraints.hpp"
|
||||
#include "replication_server.hpp"
|
||||
#include "io/network/endpoint.hpp"
|
||||
#include "rpc.hpp"
|
||||
#include "storage/v2/replication/config.hpp"
|
||||
#include "storage/v2/replication/replication_server.hpp"
|
||||
#include "storage/v2/transaction.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
namespace {
|
||||
std::pair<uint64_t, durability::WalDeltaData> ReadDelta(durability::BaseDecoder *decoder) {
|
||||
try {
|
||||
auto timestamp = ReadWalDeltaHeader(decoder);
|
||||
SPDLOG_INFO(" Timestamp {}", timestamp);
|
||||
auto delta = ReadWalDeltaData(decoder);
|
||||
return {timestamp, delta};
|
||||
} catch (const slk::SlkReaderException &) {
|
||||
throw utils::BasicException("Missing data!");
|
||||
} catch (const durability::RecoveryFailure &) {
|
||||
throw utils::BasicException("Invalid data!");
|
||||
}
|
||||
};
|
||||
|
||||
auto CreateServerContext(const replication::ReplicationServerConfig &config) -> communication::ServerContext {
|
||||
return (config.ssl) ? communication::ServerContext{config.ssl->key_file, config.ssl->cert_file, config.ssl->ca_file,
|
||||
config.ssl->verify_peer}
|
||||
: communication::ServerContext{};
|
||||
}
|
||||
|
||||
// NOTE: The replication server must have a single thread for processing
|
||||
// because there is no need for more processing threads - each replica can
|
||||
// have only a single main server. Also, the single-threaded guarantee
|
||||
// simplifies the rest of the implementation.
|
||||
constexpr auto kReplictionServerThreads = 1;
|
||||
} // namespace
|
||||
|
||||
InMemoryStorage::ReplicationServer::ReplicationServer(InMemoryStorage *storage, io::network::Endpoint endpoint,
|
||||
const replication::ReplicationServerConfig &config)
|
||||
: storage_(storage), endpoint_(endpoint) {
|
||||
// Create RPC server.
|
||||
if (config.ssl) {
|
||||
rpc_server_context_.emplace(config.ssl->key_file, config.ssl->cert_file, config.ssl->ca_file,
|
||||
config.ssl->verify_peer);
|
||||
} else {
|
||||
rpc_server_context_.emplace();
|
||||
}
|
||||
// NOTE: The replication server must have a single thread for processing
|
||||
// because there is no need for more processing threads - each replica can
|
||||
// have only a single main server. Also, the single-threaded guarantee
|
||||
// simplifies the rest of the implementation.
|
||||
rpc_server_.emplace(std::move(endpoint), &*rpc_server_context_,
|
||||
/* workers_count = */ 1);
|
||||
|
||||
rpc_server_->Register<replication::HeartbeatRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received HeartbeatRpc");
|
||||
this->HeartbeatHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_->Register<replication::FrequentHeartbeatRpc>([](auto *req_reader, auto *res_builder) {
|
||||
ReplicationServer::ReplicationServer(io::network::Endpoint endpoint, const replication::ReplicationServerConfig &config)
|
||||
: rpc_server_context_{CreateServerContext(config)},
|
||||
rpc_server_{std::move(endpoint), &rpc_server_context_, kReplictionServerThreads} {
|
||||
rpc_server_.Register<replication::FrequentHeartbeatRpc>([](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received FrequentHeartbeatRpc");
|
||||
FrequentHeartbeatHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_->Register<replication::AppendDeltasRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received AppendDeltasRpc");
|
||||
this->AppendDeltasHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_->Register<replication::SnapshotRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received SnapshotRpc");
|
||||
this->SnapshotHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_->Register<replication::WalFilesRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
spdlog::debug("Received WalFilesRpc");
|
||||
this->WalFilesHandler(req_reader, res_builder);
|
||||
});
|
||||
rpc_server_->Register<replication::CurrentWalRpc>([this](auto *req_reader, auto *res_builder) {
|
||||
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();
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationServer::HeartbeatHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::HeartbeatReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
replication::HeartbeatRes res{true, storage_->last_commit_timestamp_.load(), storage_->epoch_id_};
|
||||
slk::Save(res, res_builder);
|
||||
ReplicationServer::~ReplicationServer() {
|
||||
if (rpc_server_.IsRunning()) {
|
||||
auto const &endpoint = rpc_server_.endpoint();
|
||||
spdlog::trace("Closing replication server on {}:{}", endpoint.address, endpoint.port);
|
||||
rpc_server_.Shutdown();
|
||||
}
|
||||
rpc_server_.AwaitShutdown();
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationServer::FrequentHeartbeatHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
bool ReplicationServer::Start() { return rpc_server_.Start(); }
|
||||
|
||||
void ReplicationServer::FrequentHeartbeatHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::FrequentHeartbeatReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
replication::FrequentHeartbeatRes res{true};
|
||||
slk::Save(res, res_builder);
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationServer::AppendDeltasHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::AppendDeltasReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
replication::Decoder decoder(req_reader);
|
||||
|
||||
auto maybe_epoch_id = decoder.ReadString();
|
||||
MG_ASSERT(maybe_epoch_id, "Invalid replication message");
|
||||
|
||||
if (*maybe_epoch_id != storage_->epoch_id_) {
|
||||
storage_->epoch_history_.emplace_back(std::move(storage_->epoch_id_), storage_->last_commit_timestamp_);
|
||||
storage_->epoch_id_ = std::move(*maybe_epoch_id);
|
||||
}
|
||||
|
||||
if (storage_->wal_file_) {
|
||||
if (req.seq_num > storage_->wal_file_->SequenceNumber() || *maybe_epoch_id != storage_->epoch_id_) {
|
||||
storage_->wal_file_->FinalizeWal();
|
||||
storage_->wal_file_.reset();
|
||||
storage_->wal_seq_num_ = req.seq_num;
|
||||
spdlog::trace("Finalized WAL file");
|
||||
} else {
|
||||
MG_ASSERT(storage_->wal_file_->SequenceNumber() == req.seq_num, "Invalid sequence number of current wal file");
|
||||
storage_->wal_seq_num_ = req.seq_num + 1;
|
||||
}
|
||||
} else {
|
||||
storage_->wal_seq_num_ = req.seq_num;
|
||||
}
|
||||
|
||||
if (req.previous_commit_timestamp != storage_->last_commit_timestamp_.load()) {
|
||||
// Empty the stream
|
||||
bool transaction_complete = false;
|
||||
while (!transaction_complete) {
|
||||
SPDLOG_INFO("Skipping delta");
|
||||
const auto [timestamp, delta] = ReadDelta(&decoder);
|
||||
transaction_complete = durability::IsWalDeltaDataTypeTransactionEnd(delta.type);
|
||||
}
|
||||
|
||||
replication::AppendDeltasRes res{false, storage_->last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
return;
|
||||
}
|
||||
|
||||
ReadAndApplyDelta(&decoder);
|
||||
|
||||
replication::AppendDeltasRes res{true, storage_->last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
spdlog::debug("Replication recovery from append deltas finished, replica is now up to date!");
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationServer::SnapshotHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::SnapshotReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
replication::Decoder decoder(req_reader);
|
||||
|
||||
utils::EnsureDirOrDie(storage_->snapshot_directory_);
|
||||
|
||||
const auto maybe_snapshot_path = decoder.ReadFile(storage_->snapshot_directory_);
|
||||
MG_ASSERT(maybe_snapshot_path, "Failed to load snapshot!");
|
||||
spdlog::info("Received snapshot saved to {}", *maybe_snapshot_path);
|
||||
|
||||
std::unique_lock<utils::RWLock> storage_guard(storage_->main_lock_);
|
||||
spdlog::trace("Clearing database since recovering from snapshot.");
|
||||
// Clear the database
|
||||
storage_->vertices_.clear();
|
||||
storage_->edges_.clear();
|
||||
|
||||
storage_->constraints_.existence_constraints_ = std::make_unique<ExistenceConstraints>();
|
||||
storage_->constraints_.unique_constraints_ = std::make_unique<InMemoryUniqueConstraints>();
|
||||
storage_->indices_.label_index_ =
|
||||
std::make_unique<InMemoryLabelIndex>(&storage_->indices_, &storage_->constraints_, storage_->config_);
|
||||
storage_->indices_.label_property_index_ =
|
||||
std::make_unique<InMemoryLabelPropertyIndex>(&storage_->indices_, &storage_->constraints_, storage_->config_);
|
||||
try {
|
||||
spdlog::debug("Loading snapshot");
|
||||
auto recovered_snapshot = durability::LoadSnapshot(*maybe_snapshot_path, &storage_->vertices_, &storage_->edges_,
|
||||
&storage_->epoch_history_, storage_->name_id_mapper_.get(),
|
||||
&storage_->edge_count_, storage_->config_);
|
||||
spdlog::debug("Snapshot loaded successfully");
|
||||
// If this step is present it should always be the first step of
|
||||
// the recovery so we use the UUID we read from snasphost
|
||||
storage_->uuid_ = std::move(recovered_snapshot.snapshot_info.uuid);
|
||||
storage_->epoch_id_ = std::move(recovered_snapshot.snapshot_info.epoch_id);
|
||||
const auto &recovery_info = recovered_snapshot.recovery_info;
|
||||
storage_->vertex_id_ = recovery_info.next_vertex_id;
|
||||
storage_->edge_id_ = recovery_info.next_edge_id;
|
||||
storage_->timestamp_ = std::max(storage_->timestamp_, recovery_info.next_timestamp);
|
||||
|
||||
spdlog::trace("Recovering indices and constraints from snapshot.");
|
||||
durability::RecoverIndicesAndConstraints(recovered_snapshot.indices_constraints, &storage_->indices_,
|
||||
&storage_->constraints_, &storage_->vertices_);
|
||||
} catch (const durability::RecoveryFailure &e) {
|
||||
LOG_FATAL("Couldn't load the snapshot because of: {}", e.what());
|
||||
}
|
||||
storage_guard.unlock();
|
||||
|
||||
replication::SnapshotRes res{true, storage_->last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
|
||||
spdlog::trace("Deleting old snapshot files due to snapshot recovery.");
|
||||
// Delete other durability files
|
||||
auto snapshot_files = durability::GetSnapshotFiles(storage_->snapshot_directory_, storage_->uuid_);
|
||||
for (const auto &[path, uuid, _] : snapshot_files) {
|
||||
if (path != *maybe_snapshot_path) {
|
||||
spdlog::trace("Deleting snapshot file {}", path);
|
||||
storage_->file_retainer_.DeleteFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
spdlog::trace("Deleting old WAL files due to snapshot recovery.");
|
||||
auto wal_files = durability::GetWalFiles(storage_->wal_directory_, storage_->uuid_);
|
||||
if (wal_files) {
|
||||
for (const auto &wal_file : *wal_files) {
|
||||
spdlog::trace("Deleting WAL file {}", wal_file.path);
|
||||
storage_->file_retainer_.DeleteFile(wal_file.path);
|
||||
}
|
||||
|
||||
storage_->wal_file_.reset();
|
||||
}
|
||||
spdlog::debug("Replication recovery from snapshot finished!");
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationServer::WalFilesHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::WalFilesReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
const auto wal_file_number = req.file_number;
|
||||
spdlog::debug("Received WAL files: {}", wal_file_number);
|
||||
|
||||
replication::Decoder decoder(req_reader);
|
||||
|
||||
utils::EnsureDirOrDie(storage_->wal_directory_);
|
||||
|
||||
for (auto i = 0; i < wal_file_number; ++i) {
|
||||
LoadWal(&decoder);
|
||||
}
|
||||
|
||||
replication::WalFilesRes res{true, storage_->last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
spdlog::debug("Replication recovery from WAL files ended successfully, replica is now up to date!");
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationServer::CurrentWalHandler(slk::Reader *req_reader, slk::Builder *res_builder) {
|
||||
replication::CurrentWalReq req;
|
||||
slk::Load(&req, req_reader);
|
||||
|
||||
replication::Decoder decoder(req_reader);
|
||||
|
||||
utils::EnsureDirOrDie(storage_->wal_directory_);
|
||||
|
||||
LoadWal(&decoder);
|
||||
|
||||
replication::CurrentWalRes res{true, storage_->last_commit_timestamp_.load()};
|
||||
slk::Save(res, res_builder);
|
||||
spdlog::debug("Replication recovery from current WAL ended successfully, replica is now up to date!");
|
||||
}
|
||||
|
||||
void InMemoryStorage::ReplicationServer::LoadWal(replication::Decoder *decoder) {
|
||||
const auto temp_wal_directory = std::filesystem::temp_directory_path() / "memgraph" / durability::kWalDirectory;
|
||||
utils::EnsureDir(temp_wal_directory);
|
||||
auto maybe_wal_path = decoder->ReadFile(temp_wal_directory);
|
||||
MG_ASSERT(maybe_wal_path, "Failed to load WAL!");
|
||||
spdlog::trace("Received WAL saved to {}", *maybe_wal_path);
|
||||
try {
|
||||
auto wal_info = durability::ReadWalInfo(*maybe_wal_path);
|
||||
if (wal_info.seq_num == 0) {
|
||||
storage_->uuid_ = wal_info.uuid;
|
||||
}
|
||||
|
||||
if (wal_info.epoch_id != storage_->epoch_id_) {
|
||||
storage_->epoch_history_.emplace_back(wal_info.epoch_id, storage_->last_commit_timestamp_);
|
||||
storage_->epoch_id_ = std::move(wal_info.epoch_id);
|
||||
}
|
||||
|
||||
if (storage_->wal_file_) {
|
||||
if (storage_->wal_file_->SequenceNumber() != wal_info.seq_num) {
|
||||
storage_->wal_file_->FinalizeWal();
|
||||
storage_->wal_seq_num_ = wal_info.seq_num;
|
||||
storage_->wal_file_.reset();
|
||||
spdlog::trace("WAL file {} finalized successfully", *maybe_wal_path);
|
||||
}
|
||||
} else {
|
||||
storage_->wal_seq_num_ = wal_info.seq_num;
|
||||
}
|
||||
spdlog::trace("Loading WAL deltas from {}", *maybe_wal_path);
|
||||
durability::Decoder wal;
|
||||
const auto version = wal.Initialize(*maybe_wal_path, durability::kWalMagic);
|
||||
spdlog::debug("WAL file {} loaded successfully", *maybe_wal_path);
|
||||
if (!version) throw durability::RecoveryFailure("Couldn't read WAL magic and/or version!");
|
||||
if (!durability::IsVersionSupported(*version)) throw durability::RecoveryFailure("Invalid WAL version!");
|
||||
wal.SetPosition(wal_info.offset_deltas);
|
||||
|
||||
for (size_t i = 0; i < wal_info.num_deltas;) {
|
||||
i += ReadAndApplyDelta(&wal);
|
||||
}
|
||||
|
||||
spdlog::debug("Replication from current WAL successful!");
|
||||
} catch (const durability::RecoveryFailure &e) {
|
||||
LOG_FATAL("Couldn't recover WAL deltas from {} because of: {}", *maybe_wal_path, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void InMemoryStorage::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);
|
||||
}
|
||||
|
||||
InMemoryStorage::ReplicationServer::~ReplicationServer() {
|
||||
if (rpc_server_) {
|
||||
spdlog::trace("Closing replication server on {}:{}", endpoint_.address, endpoint_.port);
|
||||
rpc_server_->Shutdown();
|
||||
rpc_server_->AwaitShutdown();
|
||||
}
|
||||
}
|
||||
uint64_t InMemoryStorage::ReplicationServer::ReadAndApplyDelta(durability::BaseDecoder *decoder) {
|
||||
auto edge_acc = storage_->edges_.access();
|
||||
auto vertex_acc = storage_->vertices_.access();
|
||||
|
||||
std::optional<std::pair<uint64_t, std::unique_ptr<storage::Storage::Accessor>>> commit_timestamp_and_accessor;
|
||||
auto get_transaction = [this, &commit_timestamp_and_accessor](uint64_t commit_timestamp) {
|
||||
if (!commit_timestamp_and_accessor) {
|
||||
commit_timestamp_and_accessor.emplace(commit_timestamp, storage_->Access(std::optional<IsolationLevel>{}));
|
||||
} else if (commit_timestamp_and_accessor->first != commit_timestamp) {
|
||||
throw utils::BasicException("Received more than one transaction!");
|
||||
}
|
||||
// TODO: Rethink this if we would reuse ReplicationServer for on disk storage.
|
||||
if (auto *inmemoryAcc =
|
||||
static_cast<storage::InMemoryStorage::InMemoryAccessor *>(commit_timestamp_and_accessor->second.get())) {
|
||||
return inmemoryAcc;
|
||||
}
|
||||
throw utils::BasicException("Received transaction for not supported storage!");
|
||||
};
|
||||
|
||||
uint64_t applied_deltas = 0;
|
||||
auto max_commit_timestamp = storage_->last_commit_timestamp_.load();
|
||||
|
||||
for (bool transaction_complete = false; !transaction_complete; ++applied_deltas) {
|
||||
const auto [timestamp, delta] = ReadDelta(decoder);
|
||||
if (timestamp > max_commit_timestamp) {
|
||||
max_commit_timestamp = timestamp;
|
||||
}
|
||||
|
||||
transaction_complete = durability::IsWalDeltaDataTypeTransactionEnd(delta.type);
|
||||
|
||||
if (timestamp < storage_->timestamp_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SPDLOG_INFO(" Delta {}", applied_deltas);
|
||||
switch (delta.type) {
|
||||
case durability::WalDeltaData::Type::VERTEX_CREATE: {
|
||||
spdlog::trace(" Create vertex {}", delta.vertex_create_delete.gid.AsUint());
|
||||
auto transaction = get_transaction(timestamp);
|
||||
transaction->CreateVertex(delta.vertex_create_delete.gid);
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::VERTEX_DELETE: {
|
||||
spdlog::trace(" Delete vertex {}", delta.vertex_create_delete.gid.AsUint());
|
||||
auto transaction = get_transaction(timestamp);
|
||||
auto vertex = transaction->FindVertex(delta.vertex_create_delete.gid, storage::View::NEW);
|
||||
if (!vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = transaction->DeleteVertex(&*vertex);
|
||||
if (ret.HasError() || !ret.GetValue()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::VERTEX_ADD_LABEL: {
|
||||
spdlog::trace(" Vertex {} add label {}", delta.vertex_add_remove_label.gid.AsUint(),
|
||||
delta.vertex_add_remove_label.label);
|
||||
auto transaction = get_transaction(timestamp);
|
||||
auto vertex = transaction->FindVertex(delta.vertex_add_remove_label.gid, storage::View::NEW);
|
||||
if (!vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = vertex->AddLabel(transaction->NameToLabel(delta.vertex_add_remove_label.label));
|
||||
if (ret.HasError() || !ret.GetValue()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::VERTEX_REMOVE_LABEL: {
|
||||
spdlog::trace(" Vertex {} remove label {}", delta.vertex_add_remove_label.gid.AsUint(),
|
||||
delta.vertex_add_remove_label.label);
|
||||
auto transaction = get_transaction(timestamp);
|
||||
auto vertex = transaction->FindVertex(delta.vertex_add_remove_label.gid, storage::View::NEW);
|
||||
if (!vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = vertex->RemoveLabel(transaction->NameToLabel(delta.vertex_add_remove_label.label));
|
||||
if (ret.HasError() || !ret.GetValue()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::VERTEX_SET_PROPERTY: {
|
||||
spdlog::trace(" Vertex {} set property {} to {}", delta.vertex_edge_set_property.gid.AsUint(),
|
||||
delta.vertex_edge_set_property.property, delta.vertex_edge_set_property.value);
|
||||
auto transaction = get_transaction(timestamp);
|
||||
auto vertex = transaction->FindVertex(delta.vertex_edge_set_property.gid, storage::View::NEW);
|
||||
if (!vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = vertex->SetProperty(transaction->NameToProperty(delta.vertex_edge_set_property.property),
|
||||
delta.vertex_edge_set_property.value);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EDGE_CREATE: {
|
||||
spdlog::trace(" Create edge {} of type {} from vertex {} to vertex {}",
|
||||
delta.edge_create_delete.gid.AsUint(), delta.edge_create_delete.edge_type,
|
||||
delta.edge_create_delete.from_vertex.AsUint(), delta.edge_create_delete.to_vertex.AsUint());
|
||||
auto transaction = get_transaction(timestamp);
|
||||
auto from_vertex = transaction->FindVertex(delta.edge_create_delete.from_vertex, storage::View::NEW);
|
||||
if (!from_vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto to_vertex = transaction->FindVertex(delta.edge_create_delete.to_vertex, storage::View::NEW);
|
||||
if (!to_vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto edge = transaction->CreateEdge(&*from_vertex, &*to_vertex,
|
||||
transaction->NameToEdgeType(delta.edge_create_delete.edge_type),
|
||||
delta.edge_create_delete.gid);
|
||||
if (edge.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EDGE_DELETE: {
|
||||
spdlog::trace(" Delete edge {} of type {} from vertex {} to vertex {}",
|
||||
delta.edge_create_delete.gid.AsUint(), delta.edge_create_delete.edge_type,
|
||||
delta.edge_create_delete.from_vertex.AsUint(), delta.edge_create_delete.to_vertex.AsUint());
|
||||
auto transaction = get_transaction(timestamp);
|
||||
auto from_vertex = transaction->FindVertex(delta.edge_create_delete.from_vertex, storage::View::NEW);
|
||||
if (!from_vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto to_vertex = transaction->FindVertex(delta.edge_create_delete.to_vertex, storage::View::NEW);
|
||||
if (!to_vertex) throw utils::BasicException("Invalid transaction!");
|
||||
auto edges = from_vertex->OutEdges(
|
||||
storage::View::NEW, {transaction->NameToEdgeType(delta.edge_create_delete.edge_type)}, &*to_vertex);
|
||||
if (edges.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
if (edges->size() != 1) throw utils::BasicException("Invalid transaction!");
|
||||
auto &edge = (*edges)[0];
|
||||
auto ret = transaction->DeleteEdge(&edge);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EDGE_SET_PROPERTY: {
|
||||
spdlog::trace(" Edge {} set property {} to {}", delta.vertex_edge_set_property.gid.AsUint(),
|
||||
delta.vertex_edge_set_property.property, delta.vertex_edge_set_property.value);
|
||||
if (!storage_->config_.items.properties_on_edges)
|
||||
throw utils::BasicException(
|
||||
"Can't set properties on edges because properties on edges "
|
||||
"are disabled!");
|
||||
|
||||
auto transaction = get_transaction(timestamp);
|
||||
|
||||
// The following block of code effectively implements `FindEdge` and
|
||||
// yields an accessor that is only valid for managing the edge's
|
||||
// properties.
|
||||
auto edge = edge_acc.find(delta.vertex_edge_set_property.gid);
|
||||
if (edge == edge_acc.end()) throw utils::BasicException("Invalid transaction!");
|
||||
// The edge visibility check must be done here manually because we
|
||||
// don't allow direct access to the edges through the public API.
|
||||
{
|
||||
bool is_visible = true;
|
||||
Delta *delta = nullptr;
|
||||
{
|
||||
std::lock_guard<utils::SpinLock> guard(edge->lock);
|
||||
is_visible = !edge->deleted;
|
||||
delta = edge->delta;
|
||||
}
|
||||
ApplyDeltasForRead(&transaction->transaction_, delta, View::NEW, [&is_visible](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
is_visible = true;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
is_visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!is_visible) throw utils::BasicException("Invalid transaction!");
|
||||
}
|
||||
EdgeRef edge_ref(&*edge);
|
||||
// Here we create an edge accessor that we will use to get the
|
||||
// properties of the edge. The accessor is created with an invalid
|
||||
// type and invalid from/to pointers because we don't know them
|
||||
// here, but that isn't an issue because we won't use that part of
|
||||
// the API here.
|
||||
auto ea = EdgeAccessor{edge_ref,
|
||||
EdgeTypeId::FromUint(0UL),
|
||||
nullptr,
|
||||
nullptr,
|
||||
&transaction->transaction_,
|
||||
&storage_->indices_,
|
||||
&storage_->constraints_,
|
||||
storage_->config_.items};
|
||||
|
||||
auto ret = ea.SetProperty(transaction->NameToProperty(delta.vertex_edge_set_property.property),
|
||||
delta.vertex_edge_set_property.value);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
|
||||
case durability::WalDeltaData::Type::TRANSACTION_END: {
|
||||
spdlog::trace(" Transaction end");
|
||||
if (!commit_timestamp_and_accessor || commit_timestamp_and_accessor->first != timestamp)
|
||||
throw utils::BasicException("Invalid data!");
|
||||
auto ret = commit_timestamp_and_accessor->second->Commit(commit_timestamp_and_accessor->first);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
commit_timestamp_and_accessor = std::nullopt;
|
||||
break;
|
||||
}
|
||||
|
||||
case durability::WalDeltaData::Type::LABEL_INDEX_CREATE: {
|
||||
spdlog::trace(" Create label index on :{}", delta.operation_label.label);
|
||||
// Need to send the timestamp
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage_->CreateIndex(storage_->NameToLabel(delta.operation_label.label), timestamp).HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::LABEL_INDEX_DROP: {
|
||||
spdlog::trace(" Drop label index on :{}", delta.operation_label.label);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage_->DropIndex(storage_->NameToLabel(delta.operation_label.label), timestamp).HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::LABEL_PROPERTY_INDEX_CREATE: {
|
||||
spdlog::trace(" Create label+property index on :{} ({})", delta.operation_label_property.label,
|
||||
delta.operation_label_property.property);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage_
|
||||
->CreateIndex(storage_->NameToLabel(delta.operation_label_property.label),
|
||||
storage_->NameToProperty(delta.operation_label_property.property), timestamp)
|
||||
.HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::LABEL_PROPERTY_INDEX_DROP: {
|
||||
spdlog::trace(" Drop label+property index on :{} ({})", delta.operation_label_property.label,
|
||||
delta.operation_label_property.property);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage_
|
||||
->DropIndex(storage_->NameToLabel(delta.operation_label_property.label),
|
||||
storage_->NameToProperty(delta.operation_label_property.property), timestamp)
|
||||
.HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EXISTENCE_CONSTRAINT_CREATE: {
|
||||
spdlog::trace(" Create existence constraint on :{} ({})", delta.operation_label_property.label,
|
||||
delta.operation_label_property.property);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
auto ret = storage_->CreateExistenceConstraint(
|
||||
storage_->NameToLabel(delta.operation_label_property.label),
|
||||
storage_->NameToProperty(delta.operation_label_property.property), timestamp);
|
||||
if (ret.HasError()) throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::EXISTENCE_CONSTRAINT_DROP: {
|
||||
spdlog::trace(" Drop existence constraint on :{} ({})", delta.operation_label_property.label,
|
||||
delta.operation_label_property.property);
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
if (storage_
|
||||
->DropExistenceConstraint(storage_->NameToLabel(delta.operation_label_property.label),
|
||||
storage_->NameToProperty(delta.operation_label_property.property), timestamp)
|
||||
.HasError())
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::UNIQUE_CONSTRAINT_CREATE: {
|
||||
std::stringstream ss;
|
||||
utils::PrintIterable(ss, delta.operation_label_properties.properties);
|
||||
spdlog::trace(" Create unique constraint on :{} ({})", delta.operation_label_properties.label, ss.str());
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
std::set<PropertyId> properties;
|
||||
for (const auto &prop : delta.operation_label_properties.properties) {
|
||||
properties.emplace(storage_->NameToProperty(prop));
|
||||
}
|
||||
auto ret = storage_->CreateUniqueConstraint(storage_->NameToLabel(delta.operation_label_properties.label),
|
||||
properties, timestamp);
|
||||
if (!ret.HasValue() || ret.GetValue() != UniqueConstraints::CreationStatus::SUCCESS)
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
case durability::WalDeltaData::Type::UNIQUE_CONSTRAINT_DROP: {
|
||||
std::stringstream ss;
|
||||
utils::PrintIterable(ss, delta.operation_label_properties.properties);
|
||||
spdlog::trace(" Drop unique constraint on :{} ({})", delta.operation_label_properties.label, ss.str());
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid transaction!");
|
||||
std::set<PropertyId> properties;
|
||||
for (const auto &prop : delta.operation_label_properties.properties) {
|
||||
properties.emplace(storage_->NameToProperty(prop));
|
||||
}
|
||||
auto ret = storage_->DropUniqueConstraint(storage_->NameToLabel(delta.operation_label_properties.label),
|
||||
properties, timestamp);
|
||||
if (ret.HasError() || ret.GetValue() != UniqueConstraints::DeletionStatus::SUCCESS)
|
||||
throw utils::BasicException("Invalid transaction!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (commit_timestamp_and_accessor) throw utils::BasicException("Invalid data!");
|
||||
|
||||
storage_->last_commit_timestamp_ = max_commit_timestamp;
|
||||
|
||||
spdlog::debug("Applied {} deltas", applied_deltas);
|
||||
return applied_deltas;
|
||||
}
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -13,40 +13,28 @@
|
||||
|
||||
#include "rpc/server.hpp"
|
||||
#include "slk/streams.hpp"
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
#include "storage/v2/replication/replication_client.hpp"
|
||||
#include "storage/v2/replication/config.hpp"
|
||||
#include "storage/v2/replication/global.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
class InMemoryStorage::ReplicationServer {
|
||||
class ReplicationServer {
|
||||
public:
|
||||
explicit ReplicationServer(InMemoryStorage *storage, io::network::Endpoint endpoint,
|
||||
const replication::ReplicationServerConfig &config);
|
||||
explicit ReplicationServer(io::network::Endpoint endpoint, const replication::ReplicationServerConfig &config);
|
||||
ReplicationServer(const ReplicationServer &) = delete;
|
||||
ReplicationServer(ReplicationServer &&) = delete;
|
||||
ReplicationServer &operator=(const ReplicationServer &) = delete;
|
||||
ReplicationServer &operator=(ReplicationServer &&) = delete;
|
||||
|
||||
~ReplicationServer();
|
||||
virtual ~ReplicationServer();
|
||||
|
||||
private:
|
||||
// RPC handlers
|
||||
void HeartbeatHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
bool Start();
|
||||
|
||||
protected:
|
||||
static void FrequentHeartbeatHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
void AppendDeltasHandler(slk::Reader *req_reader, slk::Builder *res_builder);
|
||||
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);
|
||||
|
||||
std::optional<communication::ServerContext> rpc_server_context_;
|
||||
std::optional<rpc::Server> rpc_server_;
|
||||
|
||||
InMemoryStorage *storage_;
|
||||
io::network::Endpoint endpoint_;
|
||||
communication::ServerContext rpc_server_context_;
|
||||
rpc::Server rpc_server_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -120,6 +120,9 @@ std::optional<uint64_t> Storage::Accessor::GetTransactionId() const {
|
||||
return {};
|
||||
}
|
||||
|
||||
void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; }
|
||||
void Storage::Accessor::AdvanceCommand() {
|
||||
transaction_.manyDeltasCache.Clear(); // TODO: Just invalidate the View::OLD cache, NEW should still be fine
|
||||
++transaction_.command_id;
|
||||
}
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
#include "storage/v2/indices/indices.hpp"
|
||||
#include "storage/v2/mvcc.hpp"
|
||||
#include "storage/v2/replication/config.hpp"
|
||||
#include "storage/v2/replication/enums.hpp"
|
||||
#include "storage/v2/replication/replication_client.hpp"
|
||||
#include "storage/v2/replication/replication_server.hpp"
|
||||
#include "storage/v2/storage_error.hpp"
|
||||
#include "storage/v2/storage_mode.hpp"
|
||||
#include "storage/v2/vertices_iterable.hpp"
|
||||
@@ -72,7 +76,7 @@ class Storage {
|
||||
Storage &operator=(const Storage &) = delete;
|
||||
Storage &operator=(Storage &&) = delete;
|
||||
|
||||
virtual ~Storage() {}
|
||||
virtual ~Storage() = default;
|
||||
|
||||
const std::string &id() const { return id_; }
|
||||
|
||||
@@ -217,6 +221,7 @@ class Storage {
|
||||
StorageMode GetStorageMode() const;
|
||||
|
||||
virtual void FreeMemory(std::unique_lock<utils::RWLock> main_guard) = 0;
|
||||
|
||||
void FreeMemory() { FreeMemory({}); }
|
||||
|
||||
virtual std::unique_ptr<Accessor> Access(std::optional<IsolationLevel> override_isolation_level) = 0;
|
||||
@@ -277,6 +282,17 @@ class Storage {
|
||||
|
||||
virtual Transaction CreateTransaction(IsolationLevel isolation_level, StorageMode storage_mode) = 0;
|
||||
|
||||
virtual void EstablishNewEpoch() = 0;
|
||||
|
||||
virtual auto CreateReplicationClient(std::string name, io::network::Endpoint endpoint,
|
||||
replication::ReplicationMode mode,
|
||||
replication::ReplicationClientConfig const &config)
|
||||
-> std::unique_ptr<ReplicationClient> = 0;
|
||||
|
||||
virtual auto CreateReplicationServer(io::network::Endpoint endpoint,
|
||||
replication::ReplicationServerConfig const &config)
|
||||
-> std::unique_ptr<ReplicationServer> = 0;
|
||||
|
||||
// Main storage lock.
|
||||
// Accessors take a shared lock when starting, so it is possible to block
|
||||
// creation of new accessors by taking a unique lock. This is used when doing
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/storage_mode.hpp"
|
||||
#include "storage/v2/vertex.hpp"
|
||||
#include "storage/v2/vertex_info_cache.hpp"
|
||||
#include "storage/v2/view.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
@@ -49,7 +50,8 @@ struct Transaction {
|
||||
deltas(std::move(other.deltas)),
|
||||
must_abort(other.must_abort),
|
||||
isolation_level(other.isolation_level),
|
||||
storage_mode(other.storage_mode) {}
|
||||
storage_mode(other.storage_mode),
|
||||
manyDeltasCache{std::move(other.manyDeltasCache)} {}
|
||||
|
||||
Transaction(const Transaction &) = delete;
|
||||
Transaction &operator=(const Transaction &) = delete;
|
||||
@@ -75,6 +77,11 @@ struct Transaction {
|
||||
bool must_abort;
|
||||
IsolationLevel isolation_level;
|
||||
StorageMode storage_mode;
|
||||
|
||||
// A cache which is consistent to the current transaction_id + command_id.
|
||||
// Used to speedup getting info about a vertex when there is a long delta
|
||||
// chain involved in rebuilding that info.
|
||||
mutable VertexInfoCache manyDeltasCache;
|
||||
};
|
||||
|
||||
inline bool operator==(const Transaction &first, const Transaction &second) {
|
||||
|
||||
@@ -12,20 +12,25 @@
|
||||
#include "storage/v2/vertex_accessor.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include "storage/v2/edge_accessor.hpp"
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/indices/indices.hpp"
|
||||
#include "storage/v2/mvcc.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/result.hpp"
|
||||
#include "storage/v2/vertex_info_cache.hpp"
|
||||
#include "storage/v2/vertex_info_helpers.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/memory_tracker.hpp"
|
||||
#include "utils/variant_helpers.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
namespace detail {
|
||||
namespace {
|
||||
std::pair<bool, bool> IsVisible(Vertex *vertex, Transaction *transaction, View view) {
|
||||
std::pair<bool, bool> IsVisible(Vertex const *vertex, Transaction const *transaction, View view) {
|
||||
bool exists = true;
|
||||
bool deleted = false;
|
||||
Delta *delta = nullptr;
|
||||
@@ -34,31 +39,39 @@ std::pair<bool, bool> IsVisible(Vertex *vertex, Transaction *transaction, View v
|
||||
deleted = vertex->deleted;
|
||||
delta = vertex->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction, delta, view, [&](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
exists = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
|
||||
if (useCache) {
|
||||
auto const &cache = transaction->manyDeltasCache;
|
||||
auto existsRes = cache.GetExists(view, vertex);
|
||||
auto deletedRes = cache.GetDeleted(view, vertex);
|
||||
if (existsRes && deletedRes) return {*existsRes, *deletedRes};
|
||||
}
|
||||
});
|
||||
|
||||
auto const n_processed = ApplyDeltasForRead(transaction, delta, view, [&](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex, exists);
|
||||
cache.StoreDeleted(view, vertex, deleted);
|
||||
}
|
||||
}
|
||||
|
||||
return {exists, deleted};
|
||||
}
|
||||
} // namespace
|
||||
} // namespace detail
|
||||
|
||||
std::optional<VertexAccessor> VertexAccessor::Create(Vertex *vertex, Transaction *transaction, Indices *indices,
|
||||
@@ -89,6 +102,7 @@ Result<bool> VertexAccessor::AddLabel(LabelId label) {
|
||||
/// TODO: some by pointers, some by reference => not good, make it better
|
||||
constraints_->unique_constraints_->UpdateOnAddLabel(label, *vertex_, transaction_->start_timestamp);
|
||||
indices_->UpdateOnAddLabel(label, vertex_, *transaction_);
|
||||
transaction_->manyDeltasCache.Invalidate(vertex_, label);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -104,12 +118,13 @@ Result<bool> VertexAccessor::RemoveLabel(LabelId label) {
|
||||
if (it == vertex_->labels.end()) return false;
|
||||
|
||||
CreateAndLinkDelta(transaction_, vertex_, Delta::AddLabelTag(), label);
|
||||
std::swap(*it, *vertex_->labels.rbegin());
|
||||
*it = vertex_->labels.back();
|
||||
vertex_->labels.pop_back();
|
||||
|
||||
/// TODO: some by pointers, some by reference => not good, make it better
|
||||
constraints_->unique_constraints_->UpdateOnRemoveLabel(label, *vertex_, transaction_->start_timestamp);
|
||||
indices_->UpdateOnRemoveLabel(label, vertex_, *transaction_);
|
||||
transaction_->manyDeltasCache.Invalidate(vertex_, label);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -125,39 +140,37 @@ Result<bool> VertexAccessor::HasLabel(LabelId label, View view) const {
|
||||
has_label = std::find(vertex_->labels.begin(), vertex_->labels.end(), label) != vertex_->labels.end();
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &has_label, label](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::REMOVE_LABEL: {
|
||||
if (delta.label == label) {
|
||||
MG_ASSERT(has_label, "Invalid database state!");
|
||||
has_label = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL: {
|
||||
if (delta.label == label) {
|
||||
MG_ASSERT(!has_label, "Invalid database state!");
|
||||
has_label = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
exists = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction_->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction_->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction_->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, vertex_, for_deleted_); resError) return *resError;
|
||||
if (auto resLabel = cache.GetHasLabel(view, vertex_, label); resLabel) return {resLabel.value()};
|
||||
}
|
||||
});
|
||||
|
||||
auto const n_processed = ApplyDeltasForRead(transaction_, delta, view, [&, label](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
HasLabel_ActionMethod(has_label, label)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction_->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex_, exists);
|
||||
cache.StoreDeleted(view, vertex_, deleted);
|
||||
cache.StoreHasLabel(view, vertex_, label, has_label);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) return Error::NONEXISTENT_OBJECT;
|
||||
if (!for_deleted_ && deleted) return Error::DELETED_OBJECT;
|
||||
return has_label;
|
||||
@@ -174,40 +187,37 @@ Result<std::vector<LabelId>> VertexAccessor::Labels(View view) const {
|
||||
labels = vertex_->labels;
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &labels](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::REMOVE_LABEL: {
|
||||
// Remove the label because we don't see the addition.
|
||||
auto it = std::find(labels.begin(), labels.end(), delta.label);
|
||||
MG_ASSERT(it != labels.end(), "Invalid database state!");
|
||||
std::swap(*it, *labels.rbegin());
|
||||
labels.pop_back();
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL: {
|
||||
// Add the label because we don't see the removal.
|
||||
auto it = std::find(labels.begin(), labels.end(), delta.label);
|
||||
MG_ASSERT(it == labels.end(), "Invalid database state!");
|
||||
labels.push_back(delta.label);
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
exists = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction_->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction_->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction_->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, vertex_, for_deleted_); resError) return *resError;
|
||||
if (auto resLabels = cache.GetLabels(view, vertex_); resLabels) return {*resLabels};
|
||||
}
|
||||
});
|
||||
|
||||
auto const n_processed = ApplyDeltasForRead(transaction_, delta, view, [&](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
Labels_ActionMethod(labels)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction_->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex_, exists);
|
||||
cache.StoreDeleted(view, vertex_, deleted);
|
||||
cache.StoreLabels(view, vertex_, labels);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) return Error::NONEXISTENT_OBJECT;
|
||||
if (!for_deleted_ && deleted) return Error::DELETED_OBJECT;
|
||||
return std::move(labels);
|
||||
@@ -233,6 +243,7 @@ Result<PropertyValue> VertexAccessor::SetProperty(PropertyId property, const Pro
|
||||
vertex_->properties.SetProperty(property, value);
|
||||
|
||||
indices_->UpdateOnSetProperty(property, value, vertex_, *transaction_);
|
||||
transaction_->manyDeltasCache.Invalidate(vertex_, property);
|
||||
|
||||
return std::move(current_value);
|
||||
}
|
||||
@@ -249,11 +260,32 @@ Result<bool> VertexAccessor::InitProperties(const std::map<storage::PropertyId,
|
||||
for (const auto &[property, value] : properties) {
|
||||
CreateAndLinkDelta(transaction_, vertex_, Delta::SetPropertyTag(), property, PropertyValue());
|
||||
indices_->UpdateOnSetProperty(property, value, vertex_, *transaction_);
|
||||
transaction_->manyDeltasCache.Invalidate(vertex_, property);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Result<std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>>> VertexAccessor::UpdateProperties(
|
||||
std::map<storage::PropertyId, storage::PropertyValue> &properties) const {
|
||||
utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception;
|
||||
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
|
||||
|
||||
if (!PrepareForWrite(transaction_, vertex_)) return Error::SERIALIZATION_ERROR;
|
||||
|
||||
if (vertex_->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
auto id_old_new_change = vertex_->properties.UpdateProperties(properties);
|
||||
|
||||
for (auto &[id, old_value, new_value] : id_old_new_change) {
|
||||
indices_->UpdateOnSetProperty(id, new_value, vertex_, *transaction_);
|
||||
CreateAndLinkDelta(transaction_, vertex_, Delta::SetPropertyTag(), id, std::move(old_value));
|
||||
transaction_->manyDeltasCache.Invalidate(vertex_, id);
|
||||
}
|
||||
|
||||
return id_old_new_change;
|
||||
}
|
||||
|
||||
Result<std::map<PropertyId, PropertyValue>> VertexAccessor::ClearProperties() {
|
||||
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
|
||||
|
||||
@@ -262,9 +294,10 @@ Result<std::map<PropertyId, PropertyValue>> VertexAccessor::ClearProperties() {
|
||||
if (vertex_->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
auto properties = vertex_->properties.Properties();
|
||||
for (const auto &property : properties) {
|
||||
CreateAndLinkDelta(transaction_, vertex_, Delta::SetPropertyTag(), property.first, property.second);
|
||||
indices_->UpdateOnSetProperty(property.first, PropertyValue(), vertex_, *transaction_);
|
||||
for (const auto &[property, value] : properties) {
|
||||
CreateAndLinkDelta(transaction_, vertex_, Delta::SetPropertyTag(), property, value);
|
||||
indices_->UpdateOnSetProperty(property, PropertyValue(), vertex_, *transaction_);
|
||||
transaction_->manyDeltasCache.Invalidate(vertex_, property);
|
||||
}
|
||||
|
||||
vertex_->properties.ClearProperties();
|
||||
@@ -283,32 +316,38 @@ Result<PropertyValue> VertexAccessor::GetProperty(PropertyId property, View view
|
||||
value = vertex_->properties.GetProperty(property);
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &value, property](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::SET_PROPERTY: {
|
||||
if (delta.property.key == property) {
|
||||
value = delta.property.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
exists = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction_->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction_->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction_->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, vertex_, for_deleted_); resError) return *resError;
|
||||
if (auto resProperty = cache.GetProperty(view, vertex_, property); resProperty) return {*resProperty};
|
||||
}
|
||||
});
|
||||
|
||||
auto const n_processed =
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &value, property](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
PropertyValue_ActionMethod(value, property)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction_->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex_, exists);
|
||||
cache.StoreDeleted(view, vertex_, deleted);
|
||||
cache.StoreProperty(view, vertex_, property, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) return Error::NONEXISTENT_OBJECT;
|
||||
if (!for_deleted_ && deleted) return Error::DELETED_OBJECT;
|
||||
return std::move(value);
|
||||
@@ -325,41 +364,38 @@ Result<std::map<PropertyId, PropertyValue>> VertexAccessor::Properties(View view
|
||||
properties = vertex_->properties.Properties();
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &properties](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::SET_PROPERTY: {
|
||||
auto it = properties.find(delta.property.key);
|
||||
if (it != properties.end()) {
|
||||
if (delta.property.value.IsNull()) {
|
||||
// remove the property
|
||||
properties.erase(it);
|
||||
} else {
|
||||
// set the value
|
||||
it->second = delta.property.value;
|
||||
}
|
||||
} else if (!delta.property.value.IsNull()) {
|
||||
properties.emplace(delta.property.key, delta.property.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
exists = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction_->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction_->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction_->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, vertex_, for_deleted_); resError) return *resError;
|
||||
if (auto resProperties = cache.GetProperties(view, vertex_); resProperties) return {*resProperties};
|
||||
}
|
||||
});
|
||||
|
||||
auto const n_processed =
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &properties](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
Properties_ActionMethod(properties)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction_->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex_, exists);
|
||||
cache.StoreDeleted(view, vertex_, deleted);
|
||||
cache.StoreProperties(view, vertex_, properties);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) return Error::NONEXISTENT_OBJECT;
|
||||
if (!for_deleted_ && deleted) return Error::DELETED_OBJECT;
|
||||
return std::move(properties);
|
||||
@@ -368,90 +404,100 @@ Result<std::map<PropertyId, PropertyValue>> VertexAccessor::Properties(View view
|
||||
Result<std::vector<EdgeAccessor>> VertexAccessor::InEdges(View view, const std::vector<EdgeTypeId> &edge_types,
|
||||
const VertexAccessor *destination) const {
|
||||
MG_ASSERT(!destination || destination->transaction_ == transaction_, "Invalid accessor!");
|
||||
|
||||
using edge_store = std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>>;
|
||||
|
||||
// We return EdgeAccessors, this method with wrap the results in EdgeAccessors
|
||||
auto const build_result = [this](edge_store const &edges) -> std::vector<EdgeAccessor> {
|
||||
auto ret = std::vector<EdgeAccessor>{};
|
||||
ret.reserve(edges.size());
|
||||
for (auto const &[edge_type, from_vertex, edge] : edges) {
|
||||
ret.emplace_back(edge, edge_type, from_vertex, vertex_, transaction_, indices_, constraints_, config_);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto const *destination_vertex = destination ? destination->vertex_ : nullptr;
|
||||
|
||||
bool exists = true;
|
||||
bool deleted = false;
|
||||
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> in_edges;
|
||||
auto in_edges = edge_store{};
|
||||
Delta *delta = nullptr;
|
||||
{
|
||||
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
|
||||
deleted = vertex_->deleted;
|
||||
// TODO: a better filter copy
|
||||
if (edge_types.empty() && !destination) {
|
||||
in_edges = vertex_->in_edges;
|
||||
} else {
|
||||
for (const auto &item : vertex_->in_edges) {
|
||||
const auto &[edge_type, from_vertex, edge] = item;
|
||||
if (destination && from_vertex != destination->vertex_) continue;
|
||||
for (const auto &[edge_type, from_vertex, edge] : vertex_->in_edges) {
|
||||
if (destination && from_vertex != destination_vertex) continue;
|
||||
if (!edge_types.empty() && std::find(edge_types.begin(), edge_types.end(), edge_type) == edge_types.end())
|
||||
continue;
|
||||
in_edges.push_back(item);
|
||||
in_edges.emplace_back(edge_type, from_vertex, edge);
|
||||
}
|
||||
}
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
ApplyDeltasForRead(
|
||||
transaction_, delta, view, [&exists, &deleted, &in_edges, &edge_types, &destination](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::ADD_IN_EDGE: {
|
||||
if (destination && delta.vertex_edge.vertex != destination->vertex_) break;
|
||||
if (!edge_types.empty() &&
|
||||
std::find(edge_types.begin(), edge_types.end(), delta.vertex_edge.edge_type) == edge_types.end())
|
||||
break;
|
||||
// Add the edge because we don't see the removal.
|
||||
std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{delta.vertex_edge.edge_type, delta.vertex_edge.vertex,
|
||||
delta.vertex_edge.edge};
|
||||
auto it = std::find(in_edges.begin(), in_edges.end(), link);
|
||||
MG_ASSERT(it == in_edges.end(), "Invalid database state!");
|
||||
in_edges.push_back(link);
|
||||
break;
|
||||
}
|
||||
case Delta::Action::REMOVE_IN_EDGE: {
|
||||
if (destination && delta.vertex_edge.vertex != destination->vertex_) break;
|
||||
if (!edge_types.empty() &&
|
||||
std::find(edge_types.begin(), edge_types.end(), delta.vertex_edge.edge_type) == edge_types.end())
|
||||
break;
|
||||
// Remove the label because we don't see the addition.
|
||||
std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{delta.vertex_edge.edge_type, delta.vertex_edge.vertex,
|
||||
delta.vertex_edge.edge};
|
||||
auto it = std::find(in_edges.begin(), in_edges.end(), link);
|
||||
MG_ASSERT(it != in_edges.end(), "Invalid database state!");
|
||||
std::swap(*it, *in_edges.rbegin());
|
||||
in_edges.pop_back();
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
exists = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction_->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction_->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction_->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, vertex_, for_deleted_); resError) return *resError;
|
||||
if (auto resInEdges = cache.GetInEdges(view, vertex_, destination_vertex, edge_types); resInEdges)
|
||||
return {build_result(*resInEdges)};
|
||||
}
|
||||
|
||||
auto const n_processed = ApplyDeltasForRead(
|
||||
transaction_, delta, view,
|
||||
[&exists, &deleted, &in_edges, &edge_types, &destination_vertex](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
Edges_ActionMethod<EdgeDirection::IN>(in_edges, edge_types, destination_vertex)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction_->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex_, exists);
|
||||
cache.StoreDeleted(view, vertex_, deleted);
|
||||
cache.StoreInEdges(view, vertex_, destination_vertex, edge_types, in_edges);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) return Error::NONEXISTENT_OBJECT;
|
||||
if (deleted) return Error::DELETED_OBJECT;
|
||||
std::vector<EdgeAccessor> ret;
|
||||
ret.reserve(in_edges.size());
|
||||
for (const auto &item : in_edges) {
|
||||
const auto &[edge_type, from_vertex, edge] = item;
|
||||
ret.emplace_back(edge, edge_type, from_vertex, vertex_, transaction_, indices_, constraints_, config_);
|
||||
}
|
||||
return std::move(ret);
|
||||
return build_result(in_edges);
|
||||
}
|
||||
|
||||
Result<std::vector<EdgeAccessor>> VertexAccessor::OutEdges(View view, const std::vector<EdgeTypeId> &edge_types,
|
||||
const VertexAccessor *destination) const {
|
||||
MG_ASSERT(!destination || destination->transaction_ == transaction_, "Invalid accessor!");
|
||||
|
||||
using edge_store = std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>>;
|
||||
|
||||
auto const build_result = [this](edge_store const &out_edges) {
|
||||
auto ret = std::vector<EdgeAccessor>{};
|
||||
ret.reserve(out_edges.size());
|
||||
for (const auto &[edge_type, to_vertex, edge] : out_edges) {
|
||||
ret.emplace_back(edge, edge_type, vertex_, to_vertex, transaction_, indices_, constraints_, config_);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto const *dst_vertex = destination ? destination->vertex_ : nullptr;
|
||||
|
||||
bool exists = true;
|
||||
bool deleted = false;
|
||||
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> out_edges;
|
||||
auto out_edges = edge_store{};
|
||||
Delta *delta = nullptr;
|
||||
{
|
||||
std::lock_guard<utils::SpinLock> guard(vertex_->lock);
|
||||
@@ -459,72 +505,51 @@ Result<std::vector<EdgeAccessor>> VertexAccessor::OutEdges(View view, const std:
|
||||
if (edge_types.empty() && !destination) {
|
||||
out_edges = vertex_->out_edges;
|
||||
} else {
|
||||
for (const auto &item : vertex_->out_edges) {
|
||||
const auto &[edge_type, to_vertex, edge] = item;
|
||||
if (destination && to_vertex != destination->vertex_) continue;
|
||||
for (const auto &[edge_type, to_vertex, edge] : vertex_->out_edges) {
|
||||
if (destination && to_vertex != dst_vertex) continue;
|
||||
if (!edge_types.empty() && std::find(edge_types.begin(), edge_types.end(), edge_type) == edge_types.end())
|
||||
continue;
|
||||
out_edges.push_back(item);
|
||||
out_edges.emplace_back(edge_type, to_vertex, edge);
|
||||
}
|
||||
}
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
ApplyDeltasForRead(
|
||||
transaction_, delta, view, [&exists, &deleted, &out_edges, &edge_types, &destination](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::ADD_OUT_EDGE: {
|
||||
if (destination && delta.vertex_edge.vertex != destination->vertex_) break;
|
||||
if (!edge_types.empty() &&
|
||||
std::find(edge_types.begin(), edge_types.end(), delta.vertex_edge.edge_type) == edge_types.end())
|
||||
break;
|
||||
// Add the edge because we don't see the removal.
|
||||
std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{delta.vertex_edge.edge_type, delta.vertex_edge.vertex,
|
||||
delta.vertex_edge.edge};
|
||||
auto it = std::find(out_edges.begin(), out_edges.end(), link);
|
||||
MG_ASSERT(it == out_edges.end(), "Invalid database state!");
|
||||
out_edges.push_back(link);
|
||||
break;
|
||||
}
|
||||
case Delta::Action::REMOVE_OUT_EDGE: {
|
||||
if (destination && delta.vertex_edge.vertex != destination->vertex_) break;
|
||||
if (!edge_types.empty() &&
|
||||
std::find(edge_types.begin(), edge_types.end(), delta.vertex_edge.edge_type) == edge_types.end())
|
||||
break;
|
||||
// Remove the label because we don't see the addition.
|
||||
std::tuple<EdgeTypeId, Vertex *, EdgeRef> link{delta.vertex_edge.edge_type, delta.vertex_edge.vertex,
|
||||
delta.vertex_edge.edge};
|
||||
auto it = std::find(out_edges.begin(), out_edges.end(), link);
|
||||
MG_ASSERT(it != out_edges.end(), "Invalid database state!");
|
||||
std::swap(*it, *out_edges.rbegin());
|
||||
out_edges.pop_back();
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
exists = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction_->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction_->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction_->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, vertex_, for_deleted_); resError) return *resError;
|
||||
if (auto resOutEdges = cache.GetOutEdges(view, vertex_, dst_vertex, edge_types); resOutEdges)
|
||||
return {build_result(*resOutEdges)};
|
||||
}
|
||||
|
||||
auto const n_processed = ApplyDeltasForRead(
|
||||
transaction_, delta, view, [&exists, &deleted, &out_edges, &edge_types, &dst_vertex](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
Edges_ActionMethod<EdgeDirection::OUT>(out_edges, edge_types, dst_vertex)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction_->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex_, exists);
|
||||
cache.StoreDeleted(view, vertex_, deleted);
|
||||
cache.StoreOutEdges(view, vertex_, dst_vertex, edge_types, out_edges);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) return Error::NONEXISTENT_OBJECT;
|
||||
if (deleted) return Error::DELETED_OBJECT;
|
||||
std::vector<EdgeAccessor> ret;
|
||||
ret.reserve(out_edges.size());
|
||||
for (const auto &item : out_edges) {
|
||||
const auto &[edge_type, to_vertex, edge] = item;
|
||||
ret.emplace_back(edge, edge_type, vertex_, to_vertex, transaction_, indices_, constraints_, config_);
|
||||
}
|
||||
return std::move(ret);
|
||||
return build_result(out_edges);
|
||||
}
|
||||
|
||||
Result<size_t> VertexAccessor::InDegree(View view) const {
|
||||
@@ -538,29 +563,38 @@ Result<size_t> VertexAccessor::InDegree(View view) const {
|
||||
degree = vertex_->in_edges.size();
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, °ree](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
++degree;
|
||||
break;
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
--degree;
|
||||
break;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
exists = false;
|
||||
break;
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
deleted = false;
|
||||
break;
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
break;
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction_->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction_->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction_->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, vertex_, for_deleted_); resError) return *resError;
|
||||
if (auto resInDegree = cache.GetInDegree(view, vertex_); resInDegree) return {*resInDegree};
|
||||
}
|
||||
});
|
||||
|
||||
auto const n_processed =
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, °ree](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
Degree_ActionMethod<EdgeDirection::IN>(degree)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction_->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex_, exists);
|
||||
cache.StoreDeleted(view, vertex_, deleted);
|
||||
cache.StoreInDegree(view, vertex_, degree);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) return Error::NONEXISTENT_OBJECT;
|
||||
if (!for_deleted_ && deleted) return Error::DELETED_OBJECT;
|
||||
return degree;
|
||||
@@ -577,29 +611,38 @@ Result<size_t> VertexAccessor::OutDegree(View view) const {
|
||||
degree = vertex_->out_edges.size();
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, °ree](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
case Delta::Action::ADD_OUT_EDGE:
|
||||
++degree;
|
||||
break;
|
||||
case Delta::Action::REMOVE_OUT_EDGE:
|
||||
--degree;
|
||||
break;
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
case Delta::Action::DELETE_OBJECT:
|
||||
exists = false;
|
||||
break;
|
||||
case Delta::Action::RECREATE_OBJECT:
|
||||
deleted = false;
|
||||
break;
|
||||
case Delta::Action::ADD_LABEL:
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
case Delta::Action::ADD_IN_EDGE:
|
||||
case Delta::Action::REMOVE_IN_EDGE:
|
||||
break;
|
||||
|
||||
// Checking cache has a cost, only do it if we have any deltas
|
||||
// if we have no deltas then what we already have from the vertex is correct.
|
||||
if (delta && transaction_->isolation_level != IsolationLevel::READ_UNCOMMITTED) {
|
||||
// IsolationLevel::READ_COMMITTED would be tricky to propagate invalidation to
|
||||
// so for now only cache for IsolationLevel::SNAPSHOT_ISOLATION
|
||||
auto const useCache = transaction_->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION;
|
||||
if (useCache) {
|
||||
auto const &cache = transaction_->manyDeltasCache;
|
||||
if (auto resError = HasError(view, cache, vertex_, for_deleted_); resError) return *resError;
|
||||
if (auto resOutDegree = cache.GetOutDegree(view, vertex_); resOutDegree) return {*resOutDegree};
|
||||
}
|
||||
});
|
||||
|
||||
auto const n_processed =
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, °ree](const Delta &delta) {
|
||||
// clang-format off
|
||||
DeltaDispatch(delta, utils::ChainedOverloaded{
|
||||
Deleted_ActionMethod(deleted),
|
||||
Exists_ActionMethod(exists),
|
||||
Degree_ActionMethod<EdgeDirection::OUT>(degree)
|
||||
});
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
if (useCache && n_processed >= FLAGS_delta_chain_cache_threshold) {
|
||||
auto &cache = transaction_->manyDeltasCache;
|
||||
cache.StoreExists(view, vertex_, exists);
|
||||
cache.StoreDeleted(view, vertex_, deleted);
|
||||
cache.StoreOutDegree(view, vertex_, degree);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) return Error::NONEXISTENT_OBJECT;
|
||||
if (!for_deleted_ && deleted) return Error::DELETED_OBJECT;
|
||||
return degree;
|
||||
|
||||
@@ -73,6 +73,9 @@ class VertexAccessor final {
|
||||
/// @throw std::bad_alloc
|
||||
Result<bool> InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties);
|
||||
|
||||
Result<std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>>> UpdateProperties(
|
||||
std::map<storage::PropertyId, storage::PropertyValue> &properties) const;
|
||||
|
||||
/// Remove all properties and return the values of the removed properties.
|
||||
/// @throw std::bad_alloc
|
||||
Result<std::map<PropertyId, PropertyValue>> ClearProperties();
|
||||
|
||||
196
src/storage/v2/vertex_info_cache.cpp
Normal file
196
src/storage/v2/vertex_info_cache.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
#include "storage/v2/vertex_info_cache.hpp"
|
||||
|
||||
#include "storage/v2/property_value.hpp"
|
||||
|
||||
#include "utils/flag_validation.hpp"
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_VALIDATED_uint64(delta_chain_cache_threshold, 128,
|
||||
"The threshold for when to cache long delta chains. This is used for heavy read + write "
|
||||
"workloads where repeated processing of delta chains can become costly.",
|
||||
{ return value > 0; });
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
/// Helpers to reduce typo errors
|
||||
template <typename Ret, typename Func, typename... Keys>
|
||||
auto FetchHelper(VertexInfoCache const &caches, Func &&getCache, View view, Keys &&...keys)
|
||||
-> std::optional<std::conditional_t<std::is_trivially_copyable_v<Ret>, Ret, std::reference_wrapper<Ret const>>> {
|
||||
auto const &cache = (view == View::OLD) ? getCache(caches.old_) : getCache(caches.new_);
|
||||
// check empty first, cheaper than the relative cost of doing an actual hash + find
|
||||
if (cache.empty()) return std::nullopt;
|
||||
|
||||
// defer building the key, maybe a cost at construction
|
||||
using key_type = typename std::remove_cvref_t<decltype(cache)>::key_type;
|
||||
auto const it = cache.find(key_type{std::forward<Keys>(keys)...});
|
||||
if (it == cache.end()) return std::nullopt;
|
||||
|
||||
if constexpr (std::is_trivially_copyable_v<Ret>) {
|
||||
return {it->second};
|
||||
} else {
|
||||
// ensure we return a reference
|
||||
return std::cref(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Value, typename Func, typename... Keys>
|
||||
void Store(Value &&value, VertexInfoCache &caches, Func &&getCache, View view, Keys &&...keys) {
|
||||
auto &cache = (view == View::OLD) ? getCache(caches.old_) : getCache(caches.new_);
|
||||
using key_type = typename std::remove_cvref_t<decltype(cache)>::key_type;
|
||||
cache.emplace(key_type{std::forward<Keys>(keys)...}, std::forward<Value>(value));
|
||||
}
|
||||
|
||||
VertexInfoCache::VertexInfoCache() = default;
|
||||
VertexInfoCache::~VertexInfoCache() = default;
|
||||
VertexInfoCache::VertexInfoCache(VertexInfoCache &&) noexcept = default;
|
||||
VertexInfoCache &VertexInfoCache::operator=(VertexInfoCache &&) noexcept = default;
|
||||
|
||||
auto VertexInfoCache::GetExists(View view, Vertex const *vertex) const -> std::optional<bool> {
|
||||
return FetchHelper<bool>(*this, std::mem_fn(&Caches::existsCache_), view, vertex);
|
||||
}
|
||||
void VertexInfoCache::StoreExists(View view, Vertex const *vertex, bool res) {
|
||||
Store(res, *this, std::mem_fn(&Caches::existsCache_), view, vertex);
|
||||
}
|
||||
auto VertexInfoCache::GetDeleted(View view, Vertex const *vertex) const -> std::optional<bool> {
|
||||
return FetchHelper<bool>(*this, std::mem_fn(&Caches::deletedCache_), view, vertex);
|
||||
}
|
||||
void VertexInfoCache::StoreDeleted(View view, Vertex const *vertex, bool res) {
|
||||
Store(res, *this, std::mem_fn(&Caches::deletedCache_), view, vertex);
|
||||
}
|
||||
|
||||
void VertexInfoCache::Invalidate(Vertex const *vertex) {
|
||||
new_.existsCache_.erase(vertex);
|
||||
new_.deletedCache_.erase(vertex);
|
||||
new_.labelCache_.erase(vertex);
|
||||
new_.propertiesCache_.erase(vertex);
|
||||
new_.inDegreeCache_.erase(vertex);
|
||||
new_.outDegreeCache_.erase(vertex);
|
||||
|
||||
// aggressive cache invalidation, TODO: be smarter
|
||||
new_.hasLabelCache_.clear();
|
||||
new_.propertyValueCache_.clear();
|
||||
new_.inEdgesCache_.clear();
|
||||
new_.outEdgesCache_.clear();
|
||||
}
|
||||
|
||||
auto VertexInfoCache::GetLabels(View view, Vertex const *vertex) const
|
||||
-> std::optional<std::reference_wrapper<std::vector<LabelId> const>> {
|
||||
return FetchHelper<std::vector<LabelId>>(*this, std::mem_fn(&VertexInfoCache::Caches::labelCache_), view, vertex);
|
||||
}
|
||||
void VertexInfoCache::StoreLabels(View view, Vertex const *vertex, const std::vector<LabelId> &res) {
|
||||
Store(res, *this, std::mem_fn(&Caches::labelCache_), view, vertex);
|
||||
}
|
||||
auto VertexInfoCache::GetHasLabel(View view, Vertex const *vertex, LabelId label) const -> std::optional<bool> {
|
||||
return FetchHelper<bool>(*this, std::mem_fn(&Caches::hasLabelCache_), view, vertex, label);
|
||||
}
|
||||
void VertexInfoCache::StoreHasLabel(View view, Vertex const *vertex, LabelId label, bool res) {
|
||||
Store(res, *this, std::mem_fn(&Caches::hasLabelCache_), view, vertex, label);
|
||||
}
|
||||
void VertexInfoCache::Invalidate(Vertex const *vertex, LabelId label) {
|
||||
new_.labelCache_.erase(vertex);
|
||||
new_.hasLabelCache_.erase(std::tuple{vertex, label});
|
||||
}
|
||||
|
||||
auto VertexInfoCache::GetProperty(View view, Vertex const *vertex, PropertyId property) const
|
||||
-> std::optional<std::reference_wrapper<PropertyValue const>> {
|
||||
return FetchHelper<PropertyValue>(*this, std::mem_fn(&Caches::propertyValueCache_), view, vertex, property);
|
||||
}
|
||||
void VertexInfoCache::StoreProperty(View view, Vertex const *vertex, PropertyId property, PropertyValue value) {
|
||||
Store(std::move(value), *this, std::mem_fn(&Caches::propertyValueCache_), view, vertex, property);
|
||||
}
|
||||
auto VertexInfoCache::GetProperties(View view, Vertex const *vertex) const
|
||||
-> std::optional<std::reference_wrapper<std::map<PropertyId, PropertyValue> const>> {
|
||||
return FetchHelper<std::map<PropertyId, PropertyValue>>(*this, std::mem_fn(&Caches::propertiesCache_), view, vertex);
|
||||
}
|
||||
void VertexInfoCache::StoreProperties(View view, Vertex const *vertex, std::map<PropertyId, PropertyValue> properties) {
|
||||
Store(std::move(properties), *this, std::mem_fn(&Caches::propertiesCache_), view, vertex);
|
||||
}
|
||||
void VertexInfoCache::Invalidate(Vertex const *vertex, PropertyId property_key) {
|
||||
new_.propertiesCache_.erase(vertex);
|
||||
new_.propertyValueCache_.erase(std::tuple{vertex, property_key});
|
||||
}
|
||||
|
||||
auto VertexInfoCache::GetInEdges(View view, Vertex const *src_vertex, Vertex const *dst_vertex,
|
||||
const std::vector<EdgeTypeId> &edge_types) const
|
||||
-> std::optional<std::reference_wrapper<EdgeStore const>> {
|
||||
return FetchHelper<EdgeStore>(*this, std::mem_fn(&Caches::inEdgesCache_), view, src_vertex, dst_vertex, edge_types);
|
||||
}
|
||||
void VertexInfoCache::StoreInEdges(View view, Vertex const *src_vertex, Vertex const *dst_vertex,
|
||||
std::vector<EdgeTypeId> edge_types, EdgeStore in_edges) {
|
||||
Store(std::move(in_edges), *this, std::mem_fn(&Caches::inEdgesCache_), view, src_vertex, dst_vertex,
|
||||
std::move(edge_types));
|
||||
}
|
||||
auto VertexInfoCache::GetOutEdges(View view, Vertex const *src_vertex, Vertex const *dst_vertex,
|
||||
const std::vector<EdgeTypeId> &edge_types) const
|
||||
-> std::optional<std::reference_wrapper<const EdgeStore>> {
|
||||
return FetchHelper<EdgeStore>(*this, std::mem_fn(&Caches::outEdgesCache_), view, src_vertex, dst_vertex, edge_types);
|
||||
}
|
||||
void VertexInfoCache::StoreOutEdges(View view, Vertex const *src_vertex, Vertex const *dst_vertex,
|
||||
std::vector<EdgeTypeId> edge_types, EdgeStore out_edges) {
|
||||
Store(std::move(out_edges), *this, std::mem_fn(&Caches::outEdgesCache_), view, src_vertex, dst_vertex,
|
||||
std::move(edge_types));
|
||||
}
|
||||
|
||||
auto VertexInfoCache::GetInDegree(View view, Vertex const *vertex) const -> std::optional<std::size_t> {
|
||||
return FetchHelper<std::size_t>(*this, std::mem_fn(&Caches::inDegreeCache_), view, vertex);
|
||||
}
|
||||
|
||||
void VertexInfoCache::StoreInDegree(View view, Vertex const *vertex, std::size_t in_degree) {
|
||||
Store(in_degree, *this, std::mem_fn(&Caches::inDegreeCache_), view, vertex);
|
||||
}
|
||||
|
||||
auto VertexInfoCache::GetOutDegree(View view, Vertex const *vertex) const -> std::optional<std::size_t> {
|
||||
return FetchHelper<std::size_t>(*this, std::mem_fn(&Caches::outDegreeCache_), view, vertex);
|
||||
}
|
||||
|
||||
void VertexInfoCache::StoreOutDegree(View view, Vertex const *vertex, std::size_t out_degree) {
|
||||
Store(out_degree, *this, std::mem_fn(&Caches::outDegreeCache_), view, vertex);
|
||||
}
|
||||
|
||||
void VertexInfoCache::Invalidate(Vertex const *vertex, EdgeTypeId /*unused*/, EdgeDirection direction) {
|
||||
// EdgeTypeId is currently unused but could be used to be more precise in future
|
||||
if (direction == EdgeDirection::IN) {
|
||||
new_.inDegreeCache_.erase(vertex);
|
||||
new_.inEdgesCache_.clear(); // TODO: be more precise
|
||||
} else {
|
||||
new_.outDegreeCache_.erase(vertex);
|
||||
new_.outEdgesCache_.clear(); // TODO: be more precise
|
||||
}
|
||||
}
|
||||
|
||||
void VertexInfoCache::Clear() {
|
||||
old_.Clear();
|
||||
new_.Clear();
|
||||
}
|
||||
|
||||
void VertexInfoCache::Caches::Clear() {
|
||||
existsCache_.clear();
|
||||
deletedCache_.clear();
|
||||
hasLabelCache_.clear();
|
||||
propertyValueCache_.clear();
|
||||
labelCache_.clear();
|
||||
propertiesCache_.clear();
|
||||
inEdgesCache_.clear();
|
||||
outEdgesCache_.clear();
|
||||
inDegreeCache_.clear();
|
||||
outDegreeCache_.clear();
|
||||
}
|
||||
|
||||
VertexInfoCache::EdgeKey::EdgeKey(Vertex const *src_vertex, Vertex const *dst_vertex,
|
||||
std::vector<EdgeTypeId> edge_types)
|
||||
: src_vertex_{src_vertex}, dst_vertex_{dst_vertex}, edge_types_{std::move(edge_types)} {
|
||||
// needed for a canonical form
|
||||
std::sort(edge_types_.begin(), edge_types_.end());
|
||||
}
|
||||
} // namespace memgraph::storage
|
||||
169
src/storage/v2/vertex_info_cache.hpp
Normal file
169
src/storage/v2/vertex_info_cache.hpp
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "storage/v2/edge_direction.hpp"
|
||||
#include "storage/v2/edge_ref.hpp"
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/view.hpp"
|
||||
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <tuple>
|
||||
|
||||
DECLARE_uint64(delta_chain_cache_threshold);
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
namespace detail {
|
||||
// TODO: move to a general C++ utility header
|
||||
template <typename T>
|
||||
using optref = std::optional<std::reference_wrapper<T>>;
|
||||
} // namespace detail
|
||||
|
||||
// forward declarations
|
||||
struct Vertex;
|
||||
struct Transaction;
|
||||
class PropertyValue;
|
||||
|
||||
/** For vertices with long delta chains, its possible that its expensive
|
||||
* to rebuild state for the relevant transaction. This cache is used to
|
||||
* store information that would be expensive to repeatedly rebuild.
|
||||
*
|
||||
* The cache is assumed to be used in reference to a given:
|
||||
* - transaction_id
|
||||
* - command_id
|
||||
* - only for View::OLD
|
||||
*/
|
||||
struct VertexInfoCache final {
|
||||
VertexInfoCache();
|
||||
~VertexInfoCache();
|
||||
|
||||
// By design would be a mistake to copy the cache
|
||||
VertexInfoCache(VertexInfoCache const &) = delete;
|
||||
VertexInfoCache &operator=(VertexInfoCache const &) = delete;
|
||||
|
||||
VertexInfoCache(VertexInfoCache &&) noexcept;
|
||||
VertexInfoCache &operator=(VertexInfoCache &&) noexcept;
|
||||
|
||||
auto GetExists(View view, Vertex const *vertex) const -> std::optional<bool>;
|
||||
|
||||
void StoreExists(View view, Vertex const *vertex, bool res);
|
||||
|
||||
auto GetDeleted(View view, Vertex const *vertex) const -> std::optional<bool>;
|
||||
|
||||
void StoreDeleted(View view, Vertex const *vertex, bool res);
|
||||
|
||||
void Invalidate(Vertex const *vertex);
|
||||
|
||||
auto GetLabels(View view, Vertex const *vertex) const -> detail::optref<std::vector<LabelId> const>;
|
||||
|
||||
void StoreLabels(View view, Vertex const *vertex, std::vector<LabelId> const &res);
|
||||
|
||||
auto GetHasLabel(View view, Vertex const *vertex, LabelId label) const -> std::optional<bool>;
|
||||
|
||||
void StoreHasLabel(View view, Vertex const *vertex, LabelId label, bool res);
|
||||
|
||||
void Invalidate(Vertex const *vertex, LabelId label);
|
||||
|
||||
auto GetProperty(View view, Vertex const *vertex, PropertyId property) const -> detail::optref<PropertyValue const>;
|
||||
|
||||
void StoreProperty(View view, Vertex const *vertex, PropertyId property, PropertyValue value);
|
||||
|
||||
auto GetProperties(View view, Vertex const *vertex) const
|
||||
-> detail::optref<std::map<PropertyId, PropertyValue> const>;
|
||||
|
||||
void StoreProperties(View view, Vertex const *vertex, std::map<PropertyId, PropertyValue> properties);
|
||||
|
||||
void Invalidate(Vertex const *vertex, PropertyId property_key);
|
||||
|
||||
using EdgeStore = std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>>;
|
||||
|
||||
auto GetInEdges(View view, Vertex const *src_vertex, Vertex const *dst_vertex,
|
||||
const std::vector<EdgeTypeId> &edge_types) const -> detail::optref<const EdgeStore>;
|
||||
|
||||
void StoreInEdges(View view, Vertex const *src_vertex, Vertex const *dst_vertex, std::vector<EdgeTypeId> edge_types,
|
||||
EdgeStore in_edges);
|
||||
|
||||
auto GetOutEdges(View view, Vertex const *src_vertex, Vertex const *dst_vertex,
|
||||
const std::vector<EdgeTypeId> &edge_types) const -> detail::optref<const EdgeStore>;
|
||||
|
||||
void StoreOutEdges(View view, Vertex const *src_vertex, Vertex const *dst_vertex, std::vector<EdgeTypeId> edge_types,
|
||||
EdgeStore out_edges);
|
||||
|
||||
auto GetInDegree(View view, Vertex const *vertex) const -> std::optional<std::size_t>;
|
||||
|
||||
void StoreInDegree(View view, Vertex const *vertex, std::size_t in_degree);
|
||||
|
||||
auto GetOutDegree(View view, Vertex const *vertex) const -> std::optional<std::size_t>;
|
||||
|
||||
void StoreOutDegree(View view, Vertex const *vertex, std::size_t out_degree);
|
||||
|
||||
void Invalidate(Vertex const *vertex, EdgeTypeId /*unused*/, EdgeDirection direction);
|
||||
|
||||
void Clear();
|
||||
|
||||
private:
|
||||
/// Note: not a tuple because need a canonical form for the edge types
|
||||
struct EdgeKey {
|
||||
EdgeKey(Vertex const *src_vertex, Vertex const *dst_vertex, std::vector<EdgeTypeId> edge_types);
|
||||
|
||||
friend bool operator==(EdgeKey const &, EdgeKey const &) = default;
|
||||
|
||||
friend bool operator==(EdgeKey const &lhs, std::tuple<Vertex const *, EdgeTypeId> const &rhs) {
|
||||
return lhs.src_vertex_ == std::get<0>(rhs) &&
|
||||
std::find(lhs.edge_types_.begin(), lhs.edge_types_.end(), std::get<1>(rhs)) != lhs.edge_types_.end();
|
||||
}
|
||||
|
||||
template <typename H>
|
||||
friend H AbslHashValue(H h, EdgeKey const &key) {
|
||||
return H::combine(std::move(h), key.src_vertex_, key.dst_vertex_, key.edge_types_);
|
||||
}
|
||||
|
||||
private:
|
||||
Vertex const *src_vertex_;
|
||||
Vertex const *dst_vertex_;
|
||||
std::vector<EdgeTypeId> edge_types_; // TODO: use a SBO set type
|
||||
};
|
||||
|
||||
struct Caches {
|
||||
void Clear();
|
||||
|
||||
template <typename K, typename V>
|
||||
using map = absl::flat_hash_map<K, V>;
|
||||
|
||||
// TODO: Why are exists + deleted separate, is there a good reason?
|
||||
map<Vertex const *, bool> existsCache_;
|
||||
map<Vertex const *, bool> deletedCache_;
|
||||
map<std::tuple<Vertex const *, LabelId>, bool> hasLabelCache_;
|
||||
map<std::tuple<Vertex const *, PropertyId>, PropertyValue> propertyValueCache_;
|
||||
map<Vertex const *, std::vector<LabelId>> labelCache_;
|
||||
map<Vertex const *, std::map<PropertyId, PropertyValue>> propertiesCache_;
|
||||
// TODO: nest keys (edge_types) -> (src+dst) -> EdgeStore
|
||||
map<EdgeKey, EdgeStore> inEdgesCache_;
|
||||
// TODO: nest keys (edge_types) -> (src+dst) -> EdgeStore
|
||||
map<EdgeKey, EdgeStore> outEdgesCache_;
|
||||
map<Vertex const *, size_t> inDegreeCache_;
|
||||
map<Vertex const *, size_t> outDegreeCache_;
|
||||
};
|
||||
Caches old_;
|
||||
Caches new_;
|
||||
|
||||
// Helpers
|
||||
template <typename Ret, typename Func, typename... Keys>
|
||||
friend auto FetchHelper(VertexInfoCache const &caches, Func &&getCache, View view, Keys &&...keys)
|
||||
-> std::optional<std::conditional_t<std::is_trivially_copyable_v<Ret>, Ret, std::reference_wrapper<Ret const>>>;
|
||||
|
||||
template <typename Value, typename Func, typename... Keys>
|
||||
friend void Store(Value &&value, VertexInfoCache &caches, Func &&getCache, View view, Keys &&...keys);
|
||||
};
|
||||
} // namespace memgraph::storage
|
||||
15
src/storage/v2/vertex_info_cache_fwd.hpp
Normal file
15
src/storage/v2/vertex_info_cache_fwd.hpp
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
namespace memgraph::storage {
|
||||
struct VertexInfoCache;
|
||||
}
|
||||
209
src/storage/v2/vertex_info_helpers.hpp
Normal file
209
src/storage/v2/vertex_info_helpers.hpp
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright 2023 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.
|
||||
#pragma once
|
||||
|
||||
#include "storage/v2/delta.hpp"
|
||||
#include "storage/v2/edge_direction.hpp"
|
||||
#include "storage/v2/vertex_info_cache.hpp"
|
||||
#include "utils/variant_helpers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
namespace memgraph::storage {
|
||||
|
||||
template <Delta::Action>
|
||||
struct DeltaAction_tag {};
|
||||
|
||||
template <Delta::Action A, typename Method>
|
||||
struct ActionMethodImpl : Method {
|
||||
// Uses tag dispatch to ensure method is only called for the correct action
|
||||
void operator()(DeltaAction_tag<A> /*unused*/, Delta const &delta) { Method::operator()(delta); }
|
||||
};
|
||||
|
||||
template <Delta::Action A, typename Method>
|
||||
inline auto ActionMethod(Method &&func) {
|
||||
return ActionMethodImpl<A, Method>{std::forward<Method>(func)};
|
||||
}
|
||||
|
||||
/// Converts runtime Delta::Action into compile time tag, this allows us to dispatch to the correct overload
|
||||
template <typename Func>
|
||||
inline void DeltaDispatch(Delta const &delta, Func &&func) {
|
||||
// clang-format off
|
||||
#define dispatch(E) case E: return func(DeltaAction_tag<E>{}, delta); // NOLINT
|
||||
// clang-format on
|
||||
switch (delta.action) {
|
||||
using enum Delta::Action;
|
||||
dispatch(DELETE_DESERIALIZED_OBJECT);
|
||||
dispatch(DELETE_OBJECT);
|
||||
dispatch(RECREATE_OBJECT);
|
||||
dispatch(SET_PROPERTY);
|
||||
dispatch(ADD_LABEL);
|
||||
dispatch(REMOVE_LABEL);
|
||||
dispatch(ADD_IN_EDGE);
|
||||
dispatch(ADD_OUT_EDGE);
|
||||
dispatch(REMOVE_IN_EDGE);
|
||||
dispatch(REMOVE_OUT_EDGE);
|
||||
}
|
||||
#undef dispatch
|
||||
}
|
||||
|
||||
inline auto Exists_ActionMethod(bool &exists) {
|
||||
using enum Delta::Action;
|
||||
// clang-format off
|
||||
return utils::Overloaded{
|
||||
ActionMethod<DELETE_DESERIALIZED_OBJECT>([&](Delta const & /*unused*/) { exists = false; }),
|
||||
ActionMethod<DELETE_OBJECT>([&](Delta const & /*unused*/) { exists = false; })
|
||||
};
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
inline auto Deleted_ActionMethod(bool &deleted) {
|
||||
using enum Delta::Action;
|
||||
return ActionMethod<RECREATE_OBJECT>([&](Delta const & /*unused*/) { deleted = false; });
|
||||
}
|
||||
|
||||
inline auto HasLabel_ActionMethod(bool &has_label, LabelId label) {
|
||||
using enum Delta::Action;
|
||||
// clang-format off
|
||||
return utils::Overloaded{
|
||||
ActionMethod<REMOVE_LABEL>([&, label](Delta const &delta) {
|
||||
if (delta.label == label) {
|
||||
MG_ASSERT(has_label, "Invalid database state!");
|
||||
has_label = false;
|
||||
}
|
||||
}),
|
||||
ActionMethod<ADD_LABEL>([&, label](Delta const &delta) {
|
||||
if (delta.label == label) {
|
||||
MG_ASSERT(!has_label, "Invalid database state!");
|
||||
has_label = true;
|
||||
}
|
||||
})
|
||||
};
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
inline auto Labels_ActionMethod(std::vector<LabelId> &labels) {
|
||||
using enum Delta::Action;
|
||||
// clang-format off
|
||||
return utils::Overloaded{
|
||||
ActionMethod<REMOVE_LABEL>([&](Delta const &delta) {
|
||||
auto it = std::find(labels.begin(), labels.end(), delta.label);
|
||||
DMG_ASSERT(it != labels.end(), "Invalid database state!");
|
||||
*it = labels.back();
|
||||
labels.pop_back();
|
||||
}),
|
||||
ActionMethod<ADD_LABEL>([&](Delta const &delta) {
|
||||
DMG_ASSERT(std::find(labels.begin(), labels.end(), delta.label) == labels.end(), "Invalid database state!");
|
||||
labels.emplace_back(delta.label);
|
||||
})
|
||||
};
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
inline auto PropertyValue_ActionMethod(PropertyValue &value, PropertyId property) {
|
||||
using enum Delta::Action;
|
||||
return ActionMethod<SET_PROPERTY>([&, property](Delta const &delta) {
|
||||
if (delta.property.key == property) {
|
||||
value = delta.property.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inline auto PropertyValueMatch_ActionMethod(bool &match, PropertyId property, PropertyValue const &value) {
|
||||
using enum Delta::Action;
|
||||
return ActionMethod<SET_PROPERTY>([&, property](Delta const &delta) {
|
||||
if (delta.property.key == property) match = (value == delta.property.value);
|
||||
});
|
||||
}
|
||||
|
||||
inline auto Properties_ActionMethod(std::map<PropertyId, PropertyValue> &properties) {
|
||||
using enum Delta::Action;
|
||||
return ActionMethod<SET_PROPERTY>([&](Delta const &delta) {
|
||||
auto it = properties.find(delta.property.key);
|
||||
if (it != properties.end()) {
|
||||
if (delta.property.value.IsNull()) {
|
||||
// remove the property
|
||||
properties.erase(it);
|
||||
} else {
|
||||
// set the value
|
||||
it->second = delta.property.value;
|
||||
}
|
||||
} else if (!delta.property.value.IsNull()) {
|
||||
properties.emplace(delta.property.key, delta.property.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
template <EdgeDirection dir>
|
||||
inline auto Edges_ActionMethod(std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> &edges,
|
||||
std::vector<EdgeTypeId> const &edge_types, Vertex const *destination) {
|
||||
auto const predicate = [&, destination](Delta const &delta) {
|
||||
if (destination && delta.vertex_edge.vertex != destination) return false;
|
||||
if (!edge_types.empty() &&
|
||||
std::find(edge_types.begin(), edge_types.end(), delta.vertex_edge.edge_type) == edge_types.end())
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
using enum Delta::Action;
|
||||
return utils::Overloaded{
|
||||
ActionMethod <(dir == EdgeDirection::IN) ? ADD_IN_EDGE : ADD_OUT_EDGE> (
|
||||
[&, predicate](Delta const &delta) {
|
||||
if (!predicate(delta)) return;
|
||||
// Add the edge because we don't see the removal.
|
||||
auto link = std::tuple{delta.vertex_edge.edge_type, delta.vertex_edge.vertex, delta.vertex_edge.edge};
|
||||
DMG_ASSERT((std::find(edges.begin(), edges.end(), link) == edges.end()), "Invalid database state!");
|
||||
edges.push_back(link);
|
||||
}
|
||||
),
|
||||
ActionMethod <(dir == EdgeDirection::IN) ? REMOVE_IN_EDGE : REMOVE_OUT_EDGE> (
|
||||
[&, predicate](Delta const &delta) {
|
||||
if (!predicate(delta)) return;
|
||||
// Remove the label because we don't see the addition.
|
||||
auto it = std::find(edges.begin(), edges.end(),
|
||||
std::tuple{delta.vertex_edge.edge_type, delta.vertex_edge.vertex, delta.vertex_edge.edge});
|
||||
DMG_ASSERT(it != edges.end(), "Invalid database state!");
|
||||
*it = edges.back();
|
||||
edges.pop_back();
|
||||
}
|
||||
)
|
||||
};
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
template <EdgeDirection dir>
|
||||
inline auto Degree_ActionMethod(size_t °ree) {
|
||||
using enum Delta::Action;
|
||||
// clang-format off
|
||||
return utils::Overloaded{
|
||||
ActionMethod <(dir == EdgeDirection::IN) ? ADD_IN_EDGE : ADD_OUT_EDGE> (
|
||||
[&](Delta const &/*unused*/) { ++degree; }
|
||||
),
|
||||
ActionMethod <(dir == EdgeDirection::IN) ? REMOVE_IN_EDGE : REMOVE_OUT_EDGE> (
|
||||
[&](Delta const &/*unused*/) { --degree; }
|
||||
),
|
||||
};
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
inline auto HasError(View view, VertexInfoCache const &cache, Vertex const *vertex, bool for_deleted)
|
||||
-> std::optional<Error> {
|
||||
if (auto resExists = cache.GetExists(view, vertex); resExists && !resExists.value()) return Error::NONEXISTENT_OBJECT;
|
||||
if (!for_deleted) {
|
||||
if (auto resDeleted = cache.GetDeleted(view, vertex); resDeleted && resDeleted.value())
|
||||
return Error::DELETED_OBJECT;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace memgraph::storage
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 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
|
||||
@@ -11,10 +11,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
/// Indicator for obtaining the state before or after a transaction & command.
|
||||
enum class View {
|
||||
enum class View : uint8_t {
|
||||
OLD,
|
||||
NEW,
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user