Compare commits
1 Commits
master
...
dani-tweig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec85bf4e24 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -2,4 +2,3 @@
|
||||
*.hh diff=cpp
|
||||
*.svg binary
|
||||
docs/_static/api/js/* binary
|
||||
pgo/profiles/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
@@ -1,5 +1,5 @@
|
||||
# AUTH
|
||||
auth/* @nuivall
|
||||
auth/* @nuivall @ptrsmrn @KrzaQ
|
||||
|
||||
# CACHE
|
||||
row_cache* @tgrabiec
|
||||
@@ -25,15 +25,15 @@ compaction/* @raphaelsc
|
||||
transport/*
|
||||
|
||||
# CQL QUERY LANGUAGE
|
||||
cql3/* @tgrabiec @nuivall
|
||||
cql3/* @tgrabiec @nuivall @ptrsmrn @KrzaQ
|
||||
|
||||
# COUNTERS
|
||||
counters* @nuivall
|
||||
tests/counter_test* @nuivall
|
||||
counters* @nuivall @ptrsmrn @KrzaQ
|
||||
tests/counter_test* @nuivall @ptrsmrn @KrzaQ
|
||||
|
||||
# DOCS
|
||||
docs/* @annastuchlik @tzach
|
||||
docs/alternator @annastuchlik @tzach @nyh
|
||||
docs/alternator @annastuchlik @tzach @nyh @nuivall @ptrsmrn @KrzaQ
|
||||
|
||||
# GOSSIP
|
||||
gms/* @tgrabiec @asias @kbr-scylla
|
||||
@@ -57,6 +57,7 @@ repair/* @tgrabiec @asias
|
||||
|
||||
# SCHEMA MANAGEMENT
|
||||
db/schema_tables* @tgrabiec
|
||||
db/legacy_schema_migrator* @tgrabiec
|
||||
service/migration* @tgrabiec
|
||||
schema* @tgrabiec
|
||||
|
||||
@@ -73,8 +74,8 @@ streaming/* @tgrabiec @asias
|
||||
service/storage_service.* @tgrabiec @asias
|
||||
|
||||
# ALTERNATOR
|
||||
alternator/* @nyh
|
||||
test/alternator/* @nyh
|
||||
alternator/* @nyh @nuivall @ptrsmrn @KrzaQ
|
||||
test/alternator/* @nyh @nuivall @ptrsmrn @KrzaQ
|
||||
|
||||
# HINTED HANDOFF
|
||||
db/hints/* @piodul @vladzcloudius @eliransin
|
||||
|
||||
97
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
97
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,86 +1,15 @@
|
||||
name: "Report a bug"
|
||||
description: "File a bug report."
|
||||
title: "[Bug]: "
|
||||
type: "bug"
|
||||
labels: bug
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: "This is Scylla's bug tracker, to be used for reporting bugs only.
|
||||
This is Scylla's bug tracker, to be used for reporting bugs only.
|
||||
If you have a question about Scylla, and not a bug, please ask it in
|
||||
our forum at https://forum.scylladb.com/ or in our slack channel https://slack.scylladb.com/ "
|
||||
options:
|
||||
- label: I have read the disclaimer above and am reporting a suspected malfunction in Scylla.
|
||||
required: true
|
||||
our mailing-list at scylladb-dev@googlegroups.com or in our slack channel.
|
||||
|
||||
- type: input
|
||||
id: product-version
|
||||
attributes:
|
||||
label: product version
|
||||
description: Scylla version (or git commit hash)
|
||||
placeholder: ex. scylla-6.1.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: cluster-size
|
||||
attributes:
|
||||
label: Cluster Size
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: OS
|
||||
placeholder: RHEL/CentOS/Ubuntu/AWS AMI
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-data
|
||||
attributes:
|
||||
label: Additional Environmental Data
|
||||
#description:
|
||||
placeholder: Add additional data
|
||||
value: "Platform (physical/VM/cloud instance type/docker):\n
|
||||
Hardware: sockets= cores= hyperthreading= memory=\n
|
||||
Disks: (SSD/HDD, count)"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: reproducer-steps
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
placeholder: Describe how to reproduce the problem
|
||||
value: "The steps to reproduce the problem are:"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: the-problem
|
||||
attributes:
|
||||
label: What is the problem?
|
||||
placeholder: Describe the problem you found
|
||||
value: "The problem is that"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Expected behavior?
|
||||
placeholder: Describe what should have happened
|
||||
value: "I expected that "
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- [] I have read the disclaimer above, and I am reporting a suspected malfunction in Scylla.
|
||||
|
||||
*Installation details*
|
||||
Scylla version (or git commit hash):
|
||||
Cluster size:
|
||||
OS (RHEL/CentOS/Ubuntu/AWS AMI):
|
||||
|
||||
*Hardware details (for performance issues)* Delete if unneeded
|
||||
Platform (physical/VM/cloud instance type/docker):
|
||||
Hardware: sockets= cores= hyperthreading= memory=
|
||||
Disks: (SSD/HDD, count)
|
||||
|
||||
101
.github/copilot-instructions.md
vendored
101
.github/copilot-instructions.md
vendored
@@ -1,101 +0,0 @@
|
||||
# ScyllaDB Development Instructions
|
||||
|
||||
## Project Context
|
||||
High-performance distributed NoSQL database. Core values: performance, correctness, readability.
|
||||
|
||||
## Build System
|
||||
|
||||
### Modern Build (configure.py + ninja)
|
||||
```bash
|
||||
# Configure (run once per mode, or when switching modes)
|
||||
./configure.py --mode=<mode> # mode: dev, debug, release, sanitize
|
||||
|
||||
# Build everything
|
||||
ninja <mode>-build # e.g., ninja dev-build
|
||||
|
||||
# Build Scylla binary only (sufficient for Python integration tests)
|
||||
ninja build/<mode>/scylla
|
||||
|
||||
# Build specific test
|
||||
ninja build/<mode>/test/boost/<test_name>
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### C++ Unit Tests
|
||||
```bash
|
||||
# Run all tests in a file
|
||||
./test.py --mode=<mode> test/<suite>/<test_name>.cc
|
||||
|
||||
# Run a single test case from a file
|
||||
./test.py --mode=<mode> test/<suite>/<test_name>.cc::<test_case_name>
|
||||
|
||||
# Examples
|
||||
./test.py --mode=dev test/boost/memtable_test.cc
|
||||
./test.py --mode=dev test/raft/raft_server_test.cc::test_check_abort_on_client_api
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- Use full path with `.cc` extension (e.g., `test/boost/test_name.cc`, not `boost/test_name`)
|
||||
- To run a single test case, append `::<test_case_name>` to the file path
|
||||
- If you encounter permission issues with cgroup metric gathering, add `--no-gather-metrics` flag
|
||||
|
||||
**Rebuilding Tests:**
|
||||
- test.py does NOT automatically rebuild when test source files are modified
|
||||
- Many tests are part of composite binaries (e.g., `combined_tests` in test/boost contains multiple test files)
|
||||
- To find which binary contains a test, check `configure.py` in the repository root (primary source) or `test/<suite>/CMakeLists.txt`
|
||||
- To rebuild a specific test binary: `ninja build/<mode>/test/<suite>/<binary_name>`
|
||||
- Examples:
|
||||
- `ninja build/dev/test/boost/combined_tests` (contains group0_voter_calculator_test.cc and others)
|
||||
- `ninja build/dev/test/raft/replication_test` (standalone Raft test)
|
||||
|
||||
### Python Integration Tests
|
||||
```bash
|
||||
# Only requires Scylla binary (full build usually not needed)
|
||||
ninja build/<mode>/scylla
|
||||
|
||||
# Run all tests in a file
|
||||
./test.py --mode=<mode> test/<suite>/<test_name>.py
|
||||
|
||||
# Run a single test case from a file
|
||||
./test.py --mode=<mode> test/<suite>/<test_name>.py::<test_function_name>
|
||||
|
||||
# Run all tests in a directory
|
||||
./test.py --mode=<mode> test/<suite>/
|
||||
|
||||
# Examples
|
||||
./test.py --mode=dev test/alternator/
|
||||
./test.py --mode=dev test/cluster/test_raft_voters.py::test_raft_limited_voters_retain_coordinator
|
||||
./test.py --mode=dev test/cqlpy/test_json.py
|
||||
|
||||
# Optional flags
|
||||
./test.py --mode=dev test/cluster/test_raft_no_quorum.py -v # Verbose output
|
||||
./test.py --mode=dev test/cluster/test_raft_no_quorum.py --repeat 5 # Repeat test 5 times
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- Use full path with `.py` extension (e.g., `test/cluster/test_raft_no_quorum.py`, not `cluster/test_raft_no_quorum`)
|
||||
- To run a single test case, append `::<test_function_name>` to the file path
|
||||
- Add `-v` for verbose output
|
||||
- Add `--repeat <num>` to repeat a test multiple times
|
||||
- After modifying C++ source files, only rebuild the Scylla binary for Python tests - building the entire repository is unnecessary
|
||||
|
||||
## Code Philosophy
|
||||
- Performance matters in hot paths (data read/write, inner loops)
|
||||
- Self-documenting code through clear naming
|
||||
- Comments explain "why", not "what"
|
||||
- Prefer standard library over custom implementations
|
||||
- Strive for simplicity and clarity, add complexity only when clearly justified
|
||||
- Question requests: don't blindly implement requests - evaluate trade-offs, identify issues, and suggest better alternatives when appropriate
|
||||
- Consider different approaches, weigh pros and cons, and recommend the best fit for the specific context
|
||||
|
||||
## Test Philosophy
|
||||
- Performance matters. Tests should run as quickly as possible. Sleeps in the code are highly discouraged and should be avoided, to reduce run time and flakiness.
|
||||
- Stability matters. Tests should be stable. New tests should be executed 100 times at least to ensure they pass 100 out of 100 times. (use --repeat 100 --max-failures 1 when running it)
|
||||
- Unit tests should ideally test one thing and one thing only.
|
||||
- Tests for bug fixes should run before the fix - and show the failure and after the fix - and show they now pass.
|
||||
- Tests for bug fixes should have in their comments which bug fixes (GitHub or JIRA issue) they test.
|
||||
- Tests in debug are always slower, so if needed, reduce number of iterations, rows, data used, cycles, etc. in debug mode.
|
||||
- Tests should strive to be repeatable, and not use random input that will make their results unpredictable.
|
||||
- Tests should consume as little resources as possible. Prefer running tests on a single node if it is sufficient, for example.
|
||||
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -1,6 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "uv"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/docs"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
115
.github/instructions/cpp.instructions.md
vendored
115
.github/instructions/cpp.instructions.md
vendored
@@ -1,115 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.{cc,hh}"
|
||||
---
|
||||
|
||||
# C++ Guidelines
|
||||
|
||||
**Important:** Always match the style and conventions of existing code in the file and directory.
|
||||
|
||||
## Memory Management
|
||||
- Prefer stack allocation whenever possible
|
||||
- Use `std::unique_ptr` by default for dynamic allocations
|
||||
- `new`/`delete` are forbidden (use RAII)
|
||||
- Use `seastar::lw_shared_ptr` or `seastar::shared_ptr` for shared ownership within same shard
|
||||
- Use `seastar::foreign_ptr` for cross-shard sharing
|
||||
- Avoid `std::shared_ptr` except when interfacing with external C++ APIs
|
||||
- Avoid raw pointers except for non-owning references or C API interop
|
||||
|
||||
## Seastar Asynchronous Programming
|
||||
- Use `seastar::future<T>` for all async operations
|
||||
- Prefer coroutines (`co_await`, `co_return`) over `.then()` chains for readability
|
||||
- Coroutines are preferred over `seastar::do_with()` for managing temporary state
|
||||
- In hot paths where futures are ready, continuations may be more efficient than coroutines
|
||||
- Chain futures with `.then()`, don't block with `.get()` (unless in `seastar::thread` context)
|
||||
- All I/O must be asynchronous (no blocking calls)
|
||||
- Use `seastar::gate` for shutdown coordination
|
||||
- Use `seastar::semaphore` for resource limiting (not `std::mutex`)
|
||||
- Break long loops with `maybe_yield()` to avoid reactor stalls
|
||||
|
||||
## Coroutines
|
||||
```cpp
|
||||
seastar::future<T> func() {
|
||||
auto result = co_await async_operation();
|
||||
co_return result;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
- Throw exceptions for errors (futures propagate them automatically)
|
||||
- In data path: avoid exceptions, use `std::expected` (or `boost::outcome`) instead
|
||||
- Use standard exceptions (`std::runtime_error`, `std::invalid_argument`)
|
||||
- Database-specific: throw appropriate schema/query exceptions
|
||||
|
||||
## Performance
|
||||
- Pass large objects by `const&` or `&&` (move semantics)
|
||||
- Use `std::string_view` for non-owning string references
|
||||
- Avoid copies: prefer move semantics
|
||||
- Use `utils::chunked_vector` instead of `std::vector` for large allocations (>128KB)
|
||||
- Minimize dynamic allocations in hot paths
|
||||
|
||||
## Database-Specific Types
|
||||
- Use `schema_ptr` for schema references
|
||||
- Use `mutation` and `mutation_partition` for data modifications
|
||||
- Use `partition_key` and `clustering_key` for keys
|
||||
- Use `api::timestamp_type` for database timestamps
|
||||
- Use `gc_clock` for garbage collection timing
|
||||
|
||||
## Style
|
||||
- C++23 standard (prefer modern features, especially coroutines)
|
||||
- Use `auto` when type is obvious from RHS
|
||||
- Avoid `auto` when it obscures the type
|
||||
- Use range-based for loops: `for (const auto& item : container)`
|
||||
- Use standard algorithms when they clearly simplify code (e.g., replacing 10-line loops)
|
||||
- Avoid chaining multiple algorithms if a straightforward loop is clearer
|
||||
- Mark functions and variables `const` whenever possible
|
||||
- Use scoped enums: `enum class` (not unscoped `enum`)
|
||||
|
||||
## Headers
|
||||
- Use `#pragma once`
|
||||
- Include order: own header, C++ std, Seastar, Boost, project headers
|
||||
- Forward declare when possible
|
||||
- Never `using namespace` in headers (exception: `using namespace seastar` is globally available via `seastarx.hh`)
|
||||
|
||||
## Documentation
|
||||
- Public APIs require clear documentation
|
||||
- Implementation details should be self-evident from code
|
||||
- Use `///` or Doxygen `/** */` for public documentation, `//` for implementation notes - follow the existing style
|
||||
|
||||
## Naming
|
||||
- `snake_case` for most identifiers (classes, functions, variables, namespaces)
|
||||
- Template parameters: `CamelCase` (e.g., `template<typename ValueType>`)
|
||||
- Member variables: prefix with `_` (e.g., `int _count;`)
|
||||
- Structs (value-only): no `_` prefix on members
|
||||
- Constants and `constexpr`: `snake_case` (e.g., `static constexpr int max_size = 100;`)
|
||||
- Files: `.hh` for headers, `.cc` for source
|
||||
|
||||
## Formatting
|
||||
- 4 spaces indentation, never tabs
|
||||
- Opening braces on same line as control structure (except namespaces)
|
||||
- Space after keywords: `if (`, `while (`, `return `
|
||||
- Whitespace around operators matches precedence: `*a + *b` not `* a+* b`
|
||||
- Line length: keep reasonable (<160 chars), use continuation lines with double indent if needed
|
||||
- Brace all nested scopes, even single statements
|
||||
- Minimal patches: only format code you modify, never reformat entire files
|
||||
|
||||
## Logging
|
||||
- Use structured logging with appropriate levels: DEBUG, INFO, WARN, ERROR
|
||||
- Include context in log messages (e.g., request IDs)
|
||||
- Never log sensitive data (credentials, PII)
|
||||
|
||||
## Forbidden
|
||||
- `malloc`/`free`
|
||||
- `printf` family (use logging or fmt)
|
||||
- Raw pointers for ownership
|
||||
- `using namespace` in headers
|
||||
- Blocking operations: `std::sleep`, `std::read`, `std::mutex` (use Seastar equivalents)
|
||||
- `std::atomic` (reserved for very special circumstances only)
|
||||
- Macros (use `inline`, `constexpr`, or templates instead)
|
||||
|
||||
## Testing
|
||||
When modifying existing code, follow TDD: create/update test first, then implement.
|
||||
- Examine existing tests for style and structure
|
||||
- Use Boost.Test framework
|
||||
- Use `SEASTAR_THREAD_TEST_CASE` for Seastar asynchronous tests
|
||||
- Aim for high code coverage, especially for new features and bug fixes
|
||||
- Maintain bisectability: all tests must pass in every commit. Mark failing tests with `BOOST_FAIL()` or similar, then fix in subsequent commit
|
||||
51
.github/instructions/python.instructions.md
vendored
51
.github/instructions/python.instructions.md
vendored
@@ -1,51 +0,0 @@
|
||||
---
|
||||
applyTo: "**/*.py"
|
||||
---
|
||||
|
||||
# Python Guidelines
|
||||
|
||||
**Important:** Match existing code style. Some directories (like `test/cqlpy` and `test/alternator`) prefer simplicity over type hints and docstrings.
|
||||
|
||||
## Style
|
||||
- Follow PEP 8
|
||||
- Use type hints for function signatures (unless directory style omits them)
|
||||
- Use f-strings for formatting
|
||||
- Line length: 160 characters max
|
||||
- 4 spaces for indentation
|
||||
|
||||
## Imports
|
||||
Order: standard library, third-party, local imports
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from cassandra.cluster import Cluster
|
||||
|
||||
from test.utils import setup_keyspace
|
||||
```
|
||||
|
||||
Never use `from module import *`
|
||||
|
||||
## Documentation
|
||||
All public functions/classes need docstrings (unless the current directory conventions omit them):
|
||||
```python
|
||||
def my_function(arg1: str, arg2: int) -> bool:
|
||||
"""
|
||||
Brief summary of function purpose.
|
||||
|
||||
Args:
|
||||
arg1: Description of first argument.
|
||||
arg2: Description of second argument.
|
||||
|
||||
Returns:
|
||||
Description of return value.
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
- Maintain bisectability: all tests must pass in every commit
|
||||
- Mark currently-failing tests with `@pytest.mark.xfail`, unmark when fixed
|
||||
- Use descriptive names that convey intent
|
||||
- Docstrings/comments should explain what the test verifies and why, and if it reproduces a specific issue or how it fits into the larger test suite
|
||||
117
.github/scripts/auto-backport.py
vendored
117
.github/scripts/auto-backport.py
vendored
@@ -29,11 +29,10 @@ def parse_args():
|
||||
parser.add_argument('--commits', default=None, type=str, help='Range of promoted commits.')
|
||||
parser.add_argument('--pull-request', type=int, help='Pull request number to be backported')
|
||||
parser.add_argument('--head-commit', type=str, required=is_pull_request(), help='The HEAD of target branch after the pull request specified by --pull-request is merged')
|
||||
parser.add_argument('--github-event', type=str, help='Get GitHub event type')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def create_pull_request(repo, new_branch_name, base_branch_name, pr, backport_pr_title, commits, is_draft, is_collaborator):
|
||||
def create_pull_request(repo, new_branch_name, base_branch_name, pr, backport_pr_title, commits, is_draft=False):
|
||||
pr_body = f'{pr.body}\n\n'
|
||||
for commit in commits:
|
||||
pr_body += f'- (cherry picked from commit {commit})\n\n'
|
||||
@@ -47,29 +46,12 @@ def create_pull_request(repo, new_branch_name, base_branch_name, pr, backport_pr
|
||||
draft=is_draft
|
||||
)
|
||||
logging.info(f"Pull request created: {backport_pr.html_url}")
|
||||
labels_to_add = []
|
||||
priority_labels = {"P0", "P1"}
|
||||
parent_pr_labels = [label.name for label in pr.labels]
|
||||
for label in priority_labels:
|
||||
if label in parent_pr_labels:
|
||||
labels_to_add.append(label)
|
||||
labels_to_add.append("force_on_cloud")
|
||||
logging.info(f"Adding {label} and force_on_cloud labels from parent PR to backport PR")
|
||||
break # Only apply the highest priority label
|
||||
|
||||
if is_collaborator:
|
||||
backport_pr.add_to_assignees(pr.user)
|
||||
backport_pr.add_to_assignees(pr.user)
|
||||
if is_draft:
|
||||
labels_to_add.append("conflicts")
|
||||
pr_comment = f"@{pr.user.login} - This PR was marked as draft because it has conflicts\n"
|
||||
pr_comment += "Please resolve them and remove the 'conflicts' label. The PR will be made ready for review automatically."
|
||||
backport_pr.add_to_labels("conflicts")
|
||||
pr_comment = f"@{pr.user} - This PR was marked as draft because it has conflicts\n"
|
||||
pr_comment += "Please resolve them and mark this PR as ready for review"
|
||||
backport_pr.create_issue_comment(pr_comment)
|
||||
|
||||
# Apply all labels at once if we have any
|
||||
if labels_to_add:
|
||||
backport_pr.add_to_labels(*labels_to_add)
|
||||
logging.info(f"Added labels to backport PR: {labels_to_add}")
|
||||
|
||||
logging.info(f"Assigned PR to original author: {pr.user}")
|
||||
return backport_pr
|
||||
except GithubException as e:
|
||||
@@ -84,8 +66,7 @@ def get_pr_commits(repo, pr, stable_branch, start_commit=None):
|
||||
if pr.merged:
|
||||
merge_commit = repo.get_commit(pr.merge_commit_sha)
|
||||
if len(merge_commit.parents) > 1: # Check if this merge commit includes multiple commits
|
||||
for commit in pr.get_commits():
|
||||
commits.append(commit.sha)
|
||||
commits.append(pr.merge_commit_sha)
|
||||
else:
|
||||
if start_commit:
|
||||
promoted_commits = repo.compare(start_commit, stable_branch).commits
|
||||
@@ -110,7 +91,18 @@ def get_pr_commits(repo, pr, stable_branch, start_commit=None):
|
||||
return commits
|
||||
|
||||
|
||||
def backport(repo, pr, version, commits, backport_base_branch, is_collaborator):
|
||||
def create_pr_comment_and_remove_label(pr, comment_body):
|
||||
labels = pr.get_labels()
|
||||
pattern = re.compile(r"backport/\d+\.\d+$")
|
||||
for label in labels:
|
||||
if pattern.match(label.name):
|
||||
print(f"Removing label: {label.name}")
|
||||
comment_body += f'- {label.name}\n'
|
||||
pr.remove_from_labels(label)
|
||||
pr.create_issue_comment(comment_body)
|
||||
|
||||
|
||||
def backport(repo, pr, version, commits, backport_base_branch):
|
||||
new_branch_name = f'backport/{pr.number}/to-{version}'
|
||||
backport_pr_title = f'[Backport {version}] {pr.title}'
|
||||
repo_url = f'https://scylladbbot:{github_token}@github.com/{repo.full_name}.git'
|
||||
@@ -122,51 +114,33 @@ def backport(repo, pr, version, commits, backport_base_branch, is_collaborator):
|
||||
is_draft = False
|
||||
for commit in commits:
|
||||
try:
|
||||
repo_local.git.cherry_pick(commit, '-x')
|
||||
repo_local.git.cherry_pick(commit, '-m1', '-x')
|
||||
except GitCommandError as e:
|
||||
logging.warning(f'Cherry-pick conflict on commit {commit}: {e}')
|
||||
is_draft = True
|
||||
repo_local.git.add(A=True)
|
||||
repo_local.git.cherry_pick('--continue')
|
||||
# Check if the branch already exists in the remote fork
|
||||
remote_refs = repo_local.git.ls_remote('--heads', fork_repo, new_branch_name)
|
||||
if not remote_refs:
|
||||
# Branch does not exist, create it with a regular push
|
||||
repo_local.git.push(fork_repo, new_branch_name)
|
||||
create_pull_request(repo, new_branch_name, backport_base_branch, pr, backport_pr_title, commits,
|
||||
is_draft, is_collaborator)
|
||||
else:
|
||||
logging.info(f"Remote branch {new_branch_name} already exists in fork. Skipping push.")
|
||||
repo_local.git.push(fork_repo, new_branch_name, force=True)
|
||||
create_pull_request(repo, new_branch_name, backport_base_branch, pr, backport_pr_title, commits,
|
||||
is_draft=is_draft)
|
||||
|
||||
except GitCommandError as e:
|
||||
logging.warning(f"GitCommandError: {e}")
|
||||
|
||||
|
||||
def with_github_keyword_prefix(repo, pr):
|
||||
# GitHub issue pattern: #123, scylladb/scylladb#123, or full GitHub URLs
|
||||
github_pattern = rf"(?:fix(?:|es|ed))\s*:?\s*(?:(?:(?:{repo.full_name})?#)|https://github\.com/{repo.full_name}/issues/)(\d+)"
|
||||
|
||||
# JIRA issue pattern: PKG-92 or https://scylladb.atlassian.net/browse/PKG-92
|
||||
jira_pattern = r"(?:fix(?:|es|ed))\s*:?\s*(?:(?:https://scylladb\.atlassian\.net/browse/)?([A-Z]+-\d+))"
|
||||
|
||||
# Check PR body for GitHub issues
|
||||
github_match = re.findall(github_pattern, pr.body, re.IGNORECASE)
|
||||
# Check PR body for JIRA issues
|
||||
jira_match = re.findall(jira_pattern, pr.body, re.IGNORECASE)
|
||||
|
||||
match = github_match or jira_match
|
||||
|
||||
if match:
|
||||
pattern = rf"(?:fix(?:|es|ed))\s*:?\s*(?:(?:(?:{repo.full_name})?#)|https://github\.com/{repo.full_name}/issues/)(\d+)"
|
||||
match = re.findall(pattern, pr.body, re.IGNORECASE)
|
||||
if not match:
|
||||
print(f'No valid close reference for {pr.number}')
|
||||
comment = f':warning: @{pr.user.login} PR body does not contain a Fixes reference to an issue '
|
||||
comment += ' and can not be backported\n\n'
|
||||
comment += 'The following labels were removed:\n'
|
||||
create_pr_comment_and_remove_label(pr, comment)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
for commit in pr.get_commits():
|
||||
github_match = re.findall(github_pattern, commit.commit.message, re.IGNORECASE)
|
||||
jira_match = re.findall(jira_pattern, commit.commit.message, re.IGNORECASE)
|
||||
if github_match or jira_match:
|
||||
print(f'{pr.number} has a valid close reference in commit message {commit.sha}')
|
||||
return True
|
||||
|
||||
print(f'No valid close reference for {pr.number}')
|
||||
return False
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
@@ -187,7 +161,6 @@ def main():
|
||||
scylladbbot_repo = g.get_repo(fork_repo_name)
|
||||
closed_prs = []
|
||||
start_commit = None
|
||||
is_collaborator = True
|
||||
|
||||
if args.commits:
|
||||
start_commit, end_commit = args.commits.split('..')
|
||||
@@ -212,33 +185,21 @@ def main():
|
||||
if not backport_labels:
|
||||
print(f'no backport label: {pr.number}')
|
||||
continue
|
||||
if not with_github_keyword_prefix(repo, pr) and args.github_event != 'unlabeled':
|
||||
comment = f''':warning: @{pr.user.login} PR body or PR commits do not contain a Fixes reference to an issue and can not be backported
|
||||
please update PR body with a valid ref to an issue. Then remove `scylladbbot/backport_error` label to re-trigger the backport process
|
||||
'''
|
||||
pr.create_issue_comment(comment)
|
||||
pr.add_to_labels("scylladbbot/backport_error")
|
||||
if args.commits and not with_github_keyword_prefix(repo, pr):
|
||||
continue
|
||||
if not repo.private and not scylladbbot_repo.has_in_collaborators(pr.user.login):
|
||||
logging.info(f"Sending an invite to {pr.user.login} to become a collaborator to {scylladbbot_repo.full_name} ")
|
||||
scylladbbot_repo.add_to_collaborators(pr.user.login)
|
||||
comment = f''':warning: @{pr.user.login} you have been added as collaborator to scylladbbot fork
|
||||
Please check your inbox and approve the invitation, otherwise you will not be able to edit PR branch when needed
|
||||
'''
|
||||
# When a pull request is pending for backport but its author is not yet a collaborator of "scylladbbot",
|
||||
# we attach a "scylladbbot/backport_error" label to the PR.
|
||||
# This prevents the workflow from proceeding with the backport process
|
||||
# until the author has been granted proper permissions
|
||||
# the author should remove the label manually to re-trigger the backport workflow.
|
||||
pr.add_to_labels("scylladbbot/backport_error")
|
||||
pr.create_issue_comment(comment)
|
||||
is_collaborator = False
|
||||
comment = f':warning: @{pr.user.login} you have been added as collaborator to scylladbbot fork '
|
||||
comment += f'Please check your inbox and approve the invitation, once it is done, please add the backport labels again\n'
|
||||
create_pr_comment_and_remove_label(pr, comment)
|
||||
continue
|
||||
commits = get_pr_commits(repo, pr, stable_branch, start_commit)
|
||||
logging.info(f"Found PR #{pr.number} with commit {commits} and the following labels: {backport_labels}")
|
||||
for backport_label in backport_labels:
|
||||
version = backport_label.replace('backport/', '')
|
||||
backport_base_branch = backport_label.replace('backport/', backport_branch)
|
||||
backport(repo, pr, version, commits, backport_base_branch, is_collaborator)
|
||||
backport(repo, pr, version, commits, backport_base_branch)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
81
.github/scripts/check-license.py
vendored
81
.github/scripts/check-license.py
vendored
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2024-present ScyllaDB
|
||||
#
|
||||
#
|
||||
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
#
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parses command-line arguments."""
|
||||
parser = argparse.ArgumentParser(description='Check license headers in files')
|
||||
parser.add_argument('--files', required=True, nargs="+", type=Path,
|
||||
help='List of files to check')
|
||||
parser.add_argument('--license', required=True,
|
||||
help='License to check for')
|
||||
parser.add_argument('--check-lines', type=int, default=10,
|
||||
help='Number of lines to check (default: %(default)s)')
|
||||
parser.add_argument('--extensions', required=True, nargs="+",
|
||||
help='List of file extensions to check')
|
||||
parser.add_argument('--verbose', action='store_true',
|
||||
help='Print verbose output (default: %(default)s)')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def should_check_file(file_path: Path, allowed_extensions: Set[str]) -> bool:
|
||||
return file_path.suffix in allowed_extensions
|
||||
|
||||
|
||||
def check_license_header(file_path: Path, license_header: str, check_lines: int) -> bool:
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for _ in range(check_lines):
|
||||
line = f.readline()
|
||||
if license_header in line:
|
||||
return True
|
||||
return False
|
||||
except (UnicodeDecodeError, StopIteration):
|
||||
# Handle files that can't be read as text or have fewer lines
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
if not args.files:
|
||||
print("No files to check")
|
||||
return 0
|
||||
|
||||
num_errors = 0
|
||||
|
||||
for file_path in args.files:
|
||||
# Skip non-existent files
|
||||
if not file_path.exists():
|
||||
continue
|
||||
|
||||
# Skip files with non-matching extensions
|
||||
if not should_check_file(file_path, args.extensions):
|
||||
print(f"ℹ️ Skipping file with unchecked extension: {file_path}")
|
||||
continue
|
||||
|
||||
# Check license header
|
||||
if check_license_header(file_path, args.license, args.check_lines):
|
||||
if args.verbose:
|
||||
print(f"✅ License header found in: {file_path}")
|
||||
else:
|
||||
print(f"❌ Missing license header in: {file_path}")
|
||||
num_errors += 1
|
||||
|
||||
if num_errors > 0:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
20
.github/scripts/sync_labels.py
vendored
20
.github/scripts/sync_labels.py
vendored
@@ -30,13 +30,8 @@ def copy_labels_from_linked_issues(repo, pr_number):
|
||||
try:
|
||||
issue = repo.get_issue(int(issue_number))
|
||||
for label in issue.labels:
|
||||
# Copy ALL labels from issues to PR when PR is opened
|
||||
pr.add_to_labels(label.name)
|
||||
print(f"Copied label '{label.name}' from issue #{issue_number} to PR #{pr_number}")
|
||||
if label.name in ['P0', 'P1']:
|
||||
pr.add_to_labels('force_on_cloud')
|
||||
print(f"Added force_on_cloud label to PR #{pr_number} due to {label.name} label")
|
||||
print(f"All labels from issue #{issue_number} copied to PR #{pr_number}")
|
||||
print(f"Labels from issue #{issue_number} copied to PR #{pr_number}")
|
||||
except Exception as e:
|
||||
print(f"Error processing issue #{issue_number}: {e}")
|
||||
|
||||
@@ -79,22 +74,9 @@ def sync_labels(repo, number, label, action, is_issue=False):
|
||||
target = repo.get_issue(int(pr_or_issue_number))
|
||||
if action == 'labeled':
|
||||
target.add_to_labels(label)
|
||||
if label in ['P0', 'P1'] and is_issue:
|
||||
# Only add force_on_cloud to PRs when P0/P1 is added to an issue
|
||||
target.add_to_labels('force_on_cloud')
|
||||
print(f"Added 'force_on_cloud' label to PR #{pr_or_issue_number} due to {label} label")
|
||||
print(f"Label '{label}' successfully added.")
|
||||
elif action == 'unlabeled':
|
||||
target.remove_from_labels(label)
|
||||
if label in ['P0', 'P1'] and is_issue:
|
||||
# Check if any other P0/P1 labels remain before removing force_on_cloud
|
||||
remaining_priority_labels = [l.name for l in target.labels if l.name in ['P0', 'P1']]
|
||||
if not remaining_priority_labels:
|
||||
try:
|
||||
target.remove_from_labels('force_on_cloud')
|
||||
print(f"Removed 'force_on_cloud' label from PR #{pr_or_issue_number} as no P0/P1 labels remain")
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not remove force_on_cloud label: {e}")
|
||||
print(f"Label '{label}' successfully removed.")
|
||||
elif action == 'opened':
|
||||
copy_labels_from_linked_issues(repo, number)
|
||||
|
||||
16
.github/seastar-bad-include.json
vendored
16
.github/seastar-bad-include.json
vendored
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "seastar-bad-include",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+):(\\d+):(.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"message": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
34
.github/workflows/add-label-when-promoted.yaml
vendored
34
.github/workflows/add-label-when-promoted.yaml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
- branch-*.*
|
||||
- enterprise
|
||||
pull_request_target:
|
||||
types: [labeled, unlabeled]
|
||||
types: [labeled]
|
||||
branches: [master, next, enterprise]
|
||||
|
||||
jobs:
|
||||
@@ -53,31 +53,19 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.AUTO_BACKPORT_TOKEN }}
|
||||
run: python .github/scripts/auto-backport.py --repo ${{ github.repository }} --base-branch ${{ github.ref }} --commits ${{ github.event.before }}..${{ github.sha }}
|
||||
- name: Check if a valid backport label exists and no backport_error
|
||||
env:
|
||||
LABELS_JSON: ${{ toJson(github.event.pull_request.labels) }}
|
||||
- name: Check if label starts with 'backport/' and contains digits
|
||||
id: check_label
|
||||
run: |
|
||||
labels_json="$LABELS_JSON"
|
||||
echo "Checking labels:"
|
||||
echo "$labels_json" | jq -r '.[].name'
|
||||
|
||||
# Check if a valid backport label exists
|
||||
if echo "$labels_json" | jq -e 'any(.[] | .name; test("backport/[0-9]+\\.[0-9]+$"))' > /dev/null; then
|
||||
# Ensure scylladbbot/backport_error is NOT present
|
||||
if ! echo "$labels_json" | jq -e '.[] | select(.name == "scylladbbot/backport_error")' > /dev/null; then
|
||||
echo "A matching backport label was found and no backport_error label exists."
|
||||
echo "ready_for_backport=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
else
|
||||
echo "The label 'scylladbbot/backport_error' is present, invalidating backport."
|
||||
fi
|
||||
label_name="${{ github.event.label.name }}"
|
||||
if [[ "$label_name" =~ ^backport/[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Label matches backport/X.X pattern."
|
||||
echo "backport_label=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No matching backport label found."
|
||||
echo "Label does not match the required pattern."
|
||||
echo "backport_label=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "ready_for_backport=false" >> "$GITHUB_OUTPUT"
|
||||
- name: Run auto-backport.py when PR is closed
|
||||
if: ${{ github.event_name == 'pull_request_target' && steps.check_label.outputs.ready_for_backport == 'true' && github.event.pull_request.state == 'closed' }}
|
||||
- name: Run auto-backport.py when label was added
|
||||
if: ${{ github.event_name == 'pull_request_target' && steps.check_label.outputs.backport_label == 'true' && github.event.pull_request.state == 'closed' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.AUTO_BACKPORT_TOKEN }}
|
||||
run: python .github/scripts/auto-backport.py --repo ${{ github.repository }} --base-branch ${{ github.ref }} --pull-request ${{ github.event.pull_request.number }} --head-commit ${{ github.event.pull_request.base.sha }} --github-event ${{ github.event.action }}
|
||||
run: python .github/scripts/auto-backport.py --repo ${{ github.repository }} --base-branch ${{ github.ref }} --pull-request ${{ github.event.pull_request.number }} --head-commit ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
@@ -8,9 +8,6 @@ on:
|
||||
jobs:
|
||||
check-fixes-prefix:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Check PR body for "Fixes" prefix patterns
|
||||
uses: actions/github-script@v7
|
||||
@@ -21,7 +18,7 @@ jobs:
|
||||
|
||||
// Regular expression pattern to check for "Fixes" prefix
|
||||
// Adjusted to dynamically insert the repository full name
|
||||
const pattern = `Fixes:? ((?:#|${repo.replace('/', '\\/')}#|https://github\\.com/${repo.replace('/', '\\/')}/issues/)(\\d+)|(?:https://scylladb\\.atlassian\\.net/browse/)?([A-Z]+-\\d+))`;
|
||||
const pattern = `Fixes:? (?:#|${repo.replace('/', '\\/')}#|https://github\\.com/${repo.replace('/', '\\/')}/issues/)(\\d+)`;
|
||||
const regex = new RegExp(pattern);
|
||||
|
||||
if (!regex.test(body)) {
|
||||
|
||||
53
.github/workflows/call_backport_with_jira.yaml
vendored
53
.github/workflows/call_backport_with_jira.yaml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Backport with Jira Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next-*.*
|
||||
- branch-*.*
|
||||
pull_request_target:
|
||||
types: [labeled, closed]
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
- next-*.*
|
||||
- branch-*.*
|
||||
|
||||
jobs:
|
||||
backport-on-push:
|
||||
if: github.event_name == 'push'
|
||||
uses: scylladb/github-automation/.github/workflows/backport-with-jira.yaml@main
|
||||
with:
|
||||
event_type: 'push'
|
||||
base_branch: ${{ github.ref }}
|
||||
commits: ${{ github.event.before }}..${{ github.sha }}
|
||||
secrets:
|
||||
gh_token: ${{ secrets.AUTO_BACKPORT_TOKEN }}
|
||||
jira_auth: ${{ secrets.USER_AND_KEY_FOR_JIRA_AUTOMATION }}
|
||||
|
||||
backport-on-label:
|
||||
if: github.event_name == 'pull_request_target' && github.event.action == 'labeled'
|
||||
uses: scylladb/github-automation/.github/workflows/backport-with-jira.yaml@main
|
||||
with:
|
||||
event_type: 'labeled'
|
||||
base_branch: refs/heads/${{ github.event.pull_request.base.ref }}
|
||||
pull_request_number: ${{ github.event.pull_request.number }}
|
||||
head_commit: ${{ github.event.pull_request.base.sha }}
|
||||
label_name: ${{ github.event.label.name }}
|
||||
pr_state: ${{ github.event.pull_request.state }}
|
||||
secrets:
|
||||
gh_token: ${{ secrets.AUTO_BACKPORT_TOKEN }}
|
||||
jira_auth: ${{ secrets.USER_AND_KEY_FOR_JIRA_AUTOMATION }}
|
||||
|
||||
backport-chain:
|
||||
if: github.event_name == 'pull_request_target' && github.event.action == 'closed' && github.event.pull_request.merged == true
|
||||
uses: scylladb/github-automation/.github/workflows/backport-with-jira.yaml@main
|
||||
with:
|
||||
event_type: 'chain'
|
||||
base_branch: refs/heads/${{ github.event.pull_request.base.ref }}
|
||||
pull_request_number: ${{ github.event.pull_request.number }}
|
||||
pr_body: ${{ github.event.pull_request.body }}
|
||||
secrets:
|
||||
gh_token: ${{ secrets.AUTO_BACKPORT_TOKEN }}
|
||||
jira_auth: ${{ secrets.USER_AND_KEY_FOR_JIRA_AUTOMATION }}
|
||||
18
.github/workflows/call_jira_sync.yml
vendored
18
.github/workflows/call_jira_sync.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Sync Jira Based on PR Events
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, ready_for_review, review_requested, labeled, unlabeled, closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
jira-sync:
|
||||
uses: scylladb/github-automation/.github/workflows/main_pr_events_jira_sync.yml@main
|
||||
with:
|
||||
caller_action: ${{ github.event.action }}
|
||||
secrets:
|
||||
caller_jira_auth: ${{ secrets.USER_AND_KEY_FOR_JIRA_AUTOMATION }}
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Sync Jira Based on PR Milestone Events
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [milestoned, demilestoned]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
jira-sync-milestone-set:
|
||||
if: github.event.action == 'milestoned'
|
||||
uses: scylladb/github-automation/.github/workflows/main_jira_sync_pr_milestone_set.yml@main
|
||||
secrets:
|
||||
caller_jira_auth: ${{ secrets.USER_AND_KEY_FOR_JIRA_AUTOMATION }}
|
||||
|
||||
jira-sync-milestone-removed:
|
||||
if: github.event.action == 'demilestoned'
|
||||
uses: scylladb/github-automation/.github/workflows/main_jira_sync_pr_milestone_removed.yml@main
|
||||
secrets:
|
||||
caller_jira_auth: ${{ secrets.USER_AND_KEY_FOR_JIRA_AUTOMATION }}
|
||||
@@ -1,14 +0,0 @@
|
||||
name: Call Jira release creation for new milestone
|
||||
|
||||
on:
|
||||
milestone:
|
||||
types: [created, closed]
|
||||
|
||||
jobs:
|
||||
sync-milestone-to-jira:
|
||||
uses: scylladb/github-automation/.github/workflows/main_sync_milestone_to_jira_release.yml@main
|
||||
with:
|
||||
# Comma-separated list of Jira project keys
|
||||
jira_project_keys: "SCYLLADB,CUSTOMER,SMI,RELENG,VECTOR"
|
||||
secrets:
|
||||
caller_jira_auth: ${{ secrets.USER_AND_KEY_FOR_JIRA_AUTOMATION }}
|
||||
@@ -1,13 +0,0 @@
|
||||
name: validate_pr_author_email
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
validate_pr_author_email:
|
||||
uses: scylladb/github-automation/.github/workflows/validate_pr_author_email.yml@main
|
||||
|
||||
52
.github/workflows/check-license-header.yaml
vendored
52
.github/workflows/check-license-header.yaml
vendored
@@ -1,52 +0,0 @@
|
||||
name: License Header Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
HEADER_CHECK_LINES: 10
|
||||
LICENSE: "LicenseRef-ScyllaDB-Source-Available-1.0"
|
||||
CHECKED_EXTENSIONS: ".cc .hh .py"
|
||||
|
||||
jobs:
|
||||
check-license-headers:
|
||||
name: Check License Headers
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
run: |
|
||||
# Get list of added files comparing with base branch
|
||||
echo "files=$(git diff --name-only --diff-filter=A ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | tr '\n' ' ')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check license headers
|
||||
if: steps.changed-files.outputs.files != ''
|
||||
run: |
|
||||
.github/scripts/check-license.py \
|
||||
--files ${{ steps.changed-files.outputs.files }} \
|
||||
--license "${{ env.LICENSE }}" \
|
||||
--check-lines "${{ env.HEADER_CHECK_LINES }}" \
|
||||
--extensions ${{ env.CHECKED_EXTENSIONS }}
|
||||
|
||||
- name: Comment on PR if check fails
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const license = '${{ env.LICENSE }}';
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `❌ License header check failed. Please ensure all new files include the header within the first ${{ env.HEADER_CHECK_LINES }} lines:\n\`\`\`\n${license}\n\`\`\`\nSee action logs for details.`
|
||||
});
|
||||
2
.github/workflows/clang-nightly.yaml
vendored
2
.github/workflows/clang-nightly.yaml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
# use the development branch explicitly
|
||||
CLANG_VERSION: 21
|
||||
CLANG_VERSION: 20
|
||||
BUILD_DIR: build
|
||||
|
||||
permissions: {}
|
||||
|
||||
1
.github/workflows/clang-tidy.yaml
vendored
1
.github/workflows/clang-tidy.yaml
vendored
@@ -34,7 +34,6 @@ jobs:
|
||||
name: Run clang-tidy
|
||||
needs:
|
||||
- read-toolchain
|
||||
if: "${{ needs.read-toolchain.result == 'success' }}"
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ needs.read-toolchain.outputs.image }}
|
||||
steps:
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
name: Close issues created by Scylla associates
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
comment-and-close:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Comment and close if author email is scylladb.com
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const actor = context.actor;
|
||||
|
||||
// Get user data (only public email is available)
|
||||
const { data: user } = await github.rest.users.getByUsername({
|
||||
username: actor,
|
||||
});
|
||||
|
||||
const email = user.email || "";
|
||||
console.log(`Actor: ${actor}, public email: ${email || "<none>"}`);
|
||||
|
||||
// Only continue if email exists and ends with @scylladb.com
|
||||
if (!email || !email.toLowerCase().endsWith("@scylladb.com")) {
|
||||
console.log("User is not a scylladb.com email (or email not public); skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const issue_number = issue.number;
|
||||
|
||||
const body = "Issues in this repository are closed automatically. Scylla associates should use Jira to manage issues.\nPlease move this issue to Jira https://scylladb.atlassian.net/jira/software/c/projects/SCYLLADB/list";
|
||||
|
||||
// Add the comment
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body,
|
||||
});
|
||||
|
||||
console.log(`Comment added to #${issue_number}`);
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned"
|
||||
});
|
||||
|
||||
console.log(`Issue #${issue_number} closed.`);
|
||||
2
.github/workflows/codespell.yaml
vendored
2
.github/workflows/codespell.yaml
vendored
@@ -13,5 +13,5 @@ jobs:
|
||||
- uses: codespell-project/actions-codespell@master
|
||||
with:
|
||||
only_warn: 1
|
||||
ignore_words_list: "ans,datas,fo,ser,ue,crate,nd,reenable,strat,stap,te,raison,iif,tread"
|
||||
ignore_words_list: "ans,datas,fo,ser,ue,crate,nd,reenable,strat,stap,te,raison"
|
||||
skip: "./.git,./build,./tools,*.js,*.lock,./test,./licenses,./redis/lolwut.cc,*.svg"
|
||||
|
||||
143
.github/workflows/conflict_reminder.yaml
vendored
143
.github/workflows/conflict_reminder.yaml
vendored
@@ -1,16 +1,9 @@
|
||||
name: Notify PR Authors of Conflicts
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'branch-*'
|
||||
schedule:
|
||||
- cron: '0 10 * * 1' # Runs every Monday at 10:00am
|
||||
- cron: '0 10 * * 1,4' # Runs every Monday and Thursday at 10:00am
|
||||
workflow_dispatch: # Manual trigger for testing
|
||||
|
||||
jobs:
|
||||
notify_conflict_prs:
|
||||
@@ -21,134 +14,32 @@ jobs:
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
console.log("Starting conflict reminder script...");
|
||||
// Print trigger event
|
||||
if (process.env.GITHUB_EVENT_NAME) {
|
||||
console.log(`Workflow triggered by: ${process.env.GITHUB_EVENT_NAME}`);
|
||||
} else {
|
||||
console.log("Could not determine workflow trigger event.");
|
||||
}
|
||||
const isPushEvent = process.env.GITHUB_EVENT_NAME === 'push';
|
||||
console.log(`isPushEvent: ${isPushEvent}`);
|
||||
const twoMonthsAgo = new Date();
|
||||
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100
|
||||
});
|
||||
console.log(`Fetched ${prs.length} open PRs`);
|
||||
const recentPrs = prs.filter(pr => new Date(pr.created_at) >= twoMonthsAgo);
|
||||
const validBaseBranches = ['master'];
|
||||
const branchPrefix = 'branch-';
|
||||
const oneWeekAgo = new Date();
|
||||
const conflictLabel = 'conflicts';
|
||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||
console.log(`One week ago: ${oneWeekAgo.toISOString()}`);
|
||||
|
||||
for (const pr of recentPrs) {
|
||||
console.log(`Checking PR #${pr.number} on base branch '${pr.base.ref}'`);
|
||||
const isBranchX = pr.base.ref.startsWith(branchPrefix);
|
||||
const isMaster = validBaseBranches.includes(pr.base.ref);
|
||||
if (!(isBranchX || isMaster)) {
|
||||
console.log(`PR #${pr.number} skipped: base branch is not 'master' or does not start with '${branchPrefix}'`);
|
||||
continue;
|
||||
}
|
||||
const threeDaysAgo = new Date();
|
||||
const conflictLabel = 'conflicts';
|
||||
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
|
||||
for (const pr of prs) {
|
||||
if (!pr.base.ref.startsWith(branchPrefix)) continue;
|
||||
const hasConflictLabel = pr.labels.some(label => label.name === conflictLabel);
|
||||
if (!hasConflictLabel) continue;
|
||||
const updatedDate = new Date(pr.updated_at);
|
||||
console.log(`PR #${pr.number} last updated at: ${updatedDate.toISOString()}`);
|
||||
if (!isPushEvent && updatedDate >= oneWeekAgo) {
|
||||
console.log(`PR #${pr.number} skipped: updated within last week`);
|
||||
continue;
|
||||
}
|
||||
if (pr.assignee === null) {
|
||||
console.log(`PR #${pr.number} skipped: no assignee`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch PR details to check mergeability
|
||||
let { data: prDetails } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
});
|
||||
console.log(`PR #${pr.number} mergeable: ${prDetails.mergeable}`);
|
||||
|
||||
// Wait and re-fetch if mergeable is null
|
||||
if (prDetails.mergeable === null) {
|
||||
console.log(`PR #${pr.number} mergeable is null, waiting 2 seconds and retrying...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // wait 2 seconds
|
||||
prDetails = (await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
})).data;
|
||||
console.log(`PR #${pr.number} mergeable after retry: ${prDetails.mergeable}`);
|
||||
}
|
||||
|
||||
if (prDetails.mergeable === false) {
|
||||
const hasConflictLabel = pr.labels.some(label => label.name === conflictLabel);
|
||||
console.log(`PR #${pr.number} has conflict label: ${hasConflictLabel}`);
|
||||
|
||||
// Fetch comments to check for existing notifications
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
if (updatedDate >= threeDaysAgo) continue;
|
||||
if (pr.assignee === null) continue;
|
||||
const assignee = pr.assignee.login;
|
||||
if (assignee) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
per_page: 100,
|
||||
body: `@${assignee}, this PR has been open with conflicts. Please resolve the conflicts so we can merge it.`,
|
||||
});
|
||||
|
||||
// Find last notification comment from the bot
|
||||
const notificationPrefix = `@${pr.assignee.login}, this PR has merge conflicts with the base branch.`;
|
||||
const lastNotification = comments
|
||||
.filter(c =>
|
||||
c.user.type === "Bot" &&
|
||||
c.body.startsWith(notificationPrefix)
|
||||
)
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
|
||||
|
||||
// Check if we should skip notification based on recent notification
|
||||
let shouldSkipNotification = false;
|
||||
if (lastNotification) {
|
||||
const lastNotified = new Date(lastNotification.created_at);
|
||||
if (lastNotified >= oneWeekAgo) {
|
||||
console.log(`PR #${pr.number} skipped: last notification was less than 1 week ago`);
|
||||
shouldSkipNotification = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional check for push events on draft PRs with conflict labels
|
||||
if (
|
||||
isPushEvent &&
|
||||
pr.draft === true &&
|
||||
hasConflictLabel &&
|
||||
shouldSkipNotification
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasConflictLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: [conflictLabel],
|
||||
});
|
||||
console.log(`Added 'conflicts' label to PR #${pr.number}`);
|
||||
}
|
||||
|
||||
const assignee = pr.assignee.login;
|
||||
if (assignee && !shouldSkipNotification) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: `@${assignee}, this PR has merge conflicts with the base branch. Please resolve the conflicts so we can merge it.`,
|
||||
});
|
||||
console.log(`Notified @${assignee} for PR #${pr.number}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`PR #${pr.number} is mergeable, no action needed.`);
|
||||
}
|
||||
console.log(`Notified @${assignee} for PR #${pr.number}`);
|
||||
}
|
||||
}
|
||||
console.log(`Total PRs checked: ${prs.length}`);
|
||||
|
||||
8
.github/workflows/docs-pages.yaml
vendored
8
.github/workflows/docs-pages.yaml
vendored
@@ -18,10 +18,6 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -33,9 +29,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
python-version: "3.10"
|
||||
- name: Set up env
|
||||
run: make -C docs FLAG="${{ env.FLAG }}" setupenv
|
||||
- name: Build docs
|
||||
|
||||
7
.github/workflows/docs-pr.yaml
vendored
7
.github/workflows/docs-pr.yaml
vendored
@@ -2,9 +2,6 @@ name: "Docs / Build PR"
|
||||
# For more information,
|
||||
# see https://sphinx-theme.scylladb.com/stable/deployment/production.html#available-workflows
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FLAG: ${{ github.repository == 'scylladb/scylla-enterprise' && 'enterprise' || 'opensource' }}
|
||||
|
||||
@@ -29,9 +26,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
python-version: "3.10"
|
||||
- name: Set up env
|
||||
run: make -C docs FLAG="${{ env.FLAG }}" setupenv
|
||||
- name: Build docs
|
||||
|
||||
37
.github/workflows/docs-validate-metrics.yml
vendored
37
.github/workflows/docs-validate-metrics.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: Docs / Validate metrics
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- enterprise
|
||||
paths:
|
||||
- '**/*.cc'
|
||||
- 'scripts/metrics-config.yml'
|
||||
- 'scripts/get_description.py'
|
||||
- 'docs/_ext/scylladb_metrics.py'
|
||||
|
||||
jobs:
|
||||
validate-metrics:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check metrics documentation coverage
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install PyYAML
|
||||
|
||||
- name: Validate metrics
|
||||
run: python3 scripts/get_description.py --validate -c scripts/metrics-config.yml
|
||||
29
.github/workflows/iwyu.yaml
vendored
29
.github/workflows/iwyu.yaml
vendored
@@ -11,11 +11,9 @@ env:
|
||||
CLEANER_OUTPUT_PATH: build/clang-include-cleaner.log
|
||||
# the "idl" subdirectory does not contain C++ source code. the .hh files in it are
|
||||
# supposed to be processed by idl-compiler.py, so we don't check them using the cleaner
|
||||
CLEANER_DIRS: test/unit exceptions alternator api auth cdc compaction db dht gms index lang message mutation mutation_writer node_ops raft redis replica service
|
||||
SEASTAR_BAD_INCLUDE_OUTPUT_PATH: build/seastar-bad-include.log
|
||||
CLEANER_DIRS: test/unit exceptions alternator api auth cdc compaction db dht gms index lang message mutation
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
permissions: {}
|
||||
|
||||
# cancel the in-progress run upon a repush
|
||||
concurrency:
|
||||
@@ -35,6 +33,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- run: |
|
||||
sudo dnf -y install clang-tools-extra
|
||||
- name: Generate compilation database
|
||||
run: |
|
||||
cmake \
|
||||
@@ -80,24 +80,7 @@ jobs:
|
||||
done
|
||||
- run: |
|
||||
echo "::remove-matcher owner=clang-include-cleaner::"
|
||||
- run: |
|
||||
echo "::add-matcher::.github/seastar-bad-include.json"
|
||||
- name: check for seastar includes
|
||||
run: |
|
||||
git -c safe.directory="$PWD" \
|
||||
grep -nE '#include +"seastar/' \
|
||||
| tee "$SEASTAR_BAD_INCLUDE_OUTPUT_PATH"
|
||||
- run: |
|
||||
echo "::remove-matcher owner=seastar-bad-include::"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Logs
|
||||
path: |
|
||||
${{ env.CLEANER_OUTPUT_PATH }}
|
||||
${{ env.SEASTAR_BAD_INCLUDE_OUTPUT_PATH }}
|
||||
- name: fail if seastar headers are included as an internal library
|
||||
run: |
|
||||
if [ -s "$SEASTAR_BAD_INCLUDE_OUTPUT_PATH" ]; then
|
||||
echo "::error::Found #include \"seastar/ in the source code. Use angle brackets instead."
|
||||
exit 1
|
||||
fi
|
||||
name: Logs (clang-include-cleaner)
|
||||
path: "./${{ env.CLEANER_OUTPUT_PATH }}"
|
||||
|
||||
29
.github/workflows/make-pr-ready-for-review.yaml
vendored
29
.github/workflows/make-pr-ready-for-review.yaml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Mark PR as Ready When Conflicts Label is Removed
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- unlabeled
|
||||
|
||||
env:
|
||||
DEFAULT_BRANCH: 'master'
|
||||
|
||||
jobs:
|
||||
mark-ready:
|
||||
if: github.event.label.name == 'conflicts'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ env.DEFAULT_BRANCH }}
|
||||
token: ${{ secrets.AUTO_BACKPORT_TOKEN }}
|
||||
fetch-depth: 1
|
||||
- name: Mark pull request as ready for review
|
||||
run: gh pr ready "${{ github.event.pull_request.number }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.AUTO_BACKPORT_TOKEN }}
|
||||
@@ -13,12 +13,10 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for label to be added
|
||||
run: sleep 1m
|
||||
- uses: mheap/github-action-required-labels@v5
|
||||
with:
|
||||
mode: minimum
|
||||
count: 1
|
||||
labels: "backport/none\nbackport/\\d{4}\\.\\d+\nbackport/\\d+\\.\\d+"
|
||||
labels: "backport/none\nbackport/\\d.\\d"
|
||||
use_regex: true
|
||||
add_comment: false
|
||||
|
||||
2
.github/workflows/read-toolchain.yaml
vendored
2
.github/workflows/read-toolchain.yaml
vendored
@@ -10,8 +10,6 @@ on:
|
||||
jobs:
|
||||
read-toolchain:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
image: ${{ steps.read.outputs.image }}
|
||||
steps:
|
||||
|
||||
7
.github/workflows/seastar.yaml
vendored
7
.github/workflows/seastar.yaml
vendored
@@ -15,13 +15,10 @@ env:
|
||||
BUILD_DIR: build
|
||||
|
||||
jobs:
|
||||
read-toolchain:
|
||||
uses: ./.github/workflows/read-toolchain.yaml
|
||||
build-with-the-latest-seastar:
|
||||
needs:
|
||||
- read-toolchain
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ needs.read-toolchain.outputs.image }}
|
||||
# be consistent with tools/toolchain/image
|
||||
container: scylladb/scylla-toolchain:fedora-40-20240621
|
||||
strategy:
|
||||
matrix:
|
||||
build_type:
|
||||
|
||||
4
.github/workflows/sync-labels.yaml
vendored
4
.github/workflows/sync-labels.yaml
vendored
@@ -37,13 +37,13 @@ jobs:
|
||||
run: python .github/scripts/sync_labels.py --repo ${{ github.repository }} --number ${{ github.event.number }} --action ${{ github.event.action }}
|
||||
|
||||
- name: Pull request labeled or unlabeled event
|
||||
if: github.event_name == 'pull_request_target' && (startsWith(github.event.label.name, 'backport/') || github.event.label.name == 'P0' || github.event.label.name == 'P1')
|
||||
if: github.event_name == 'pull_request_target' && startsWith(github.event.label.name, 'backport/')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: python .github/scripts/sync_labels.py --repo ${{ github.repository }} --number ${{ github.event.number }} --action ${{ github.event.action }} --label ${{ github.event.label.name }}
|
||||
|
||||
- name: Issue labeled or unlabeled event
|
||||
if: github.event_name == 'issues' && (startsWith(github.event.label.name, 'backport/') || github.event.label.name == 'P0' || github.event.label.name == 'P1')
|
||||
if: github.event_name == 'issues' && startsWith(github.event.label.name, 'backport/')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: python .github/scripts/sync_labels.py --repo ${{ github.repository }} --number ${{ github.event.issue.number }} --action ${{ github.event.action }} --is_issue --label ${{ github.event.label.name }}
|
||||
|
||||
66
.github/workflows/trigger-scylla-ci.yaml
vendored
66
.github/workflows/trigger-scylla-ci.yaml
vendored
@@ -1,66 +0,0 @@
|
||||
name: Trigger Scylla CI Route
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types:
|
||||
- unlabeled
|
||||
|
||||
jobs:
|
||||
trigger-jenkins:
|
||||
if: (github.event_name == 'issue_comment' && github.event.comment.user.login != 'scylladbbot') || github.event.label.name == 'conflicts'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Verify Org Membership
|
||||
id: verify_author
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
|
||||
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
|
||||
COMMENT_ASSOCIATION: ${{ github.event.comment.author_association }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "$EVENT_NAME" == "pull_request_target" ]]; then
|
||||
AUTHOR="$PR_AUTHOR"
|
||||
ASSOCIATION="$PR_ASSOCIATION"
|
||||
else
|
||||
AUTHOR="$COMMENT_AUTHOR"
|
||||
ASSOCIATION="$COMMENT_ASSOCIATION"
|
||||
fi
|
||||
if [[ "$ASSOCIATION" == "MEMBER" || "$ASSOCIATION" == "OWNER" ]]; then
|
||||
echo "member=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "::warning::${AUTHOR} is not a member of scylladb (association: ${ASSOCIATION}); skipping CI trigger."
|
||||
echo "member=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Validate Comment Trigger
|
||||
if: github.event_name == 'issue_comment'
|
||||
id: verify_comment
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
shell: bash
|
||||
run: |
|
||||
CLEAN_BODY=$(echo "$COMMENT_BODY" | grep -v '^[[:space:]]*>')
|
||||
|
||||
if echo "$CLEAN_BODY" | grep -qi '@scylladbbot' && echo "$CLEAN_BODY" | grep -qi 'trigger-ci'; then
|
||||
echo "trigger=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "trigger=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Trigger Scylla-CI-Route Jenkins Job
|
||||
if: steps.verify_author.outputs.member == 'true' && (github.event_name == 'pull_request_target' || steps.verify_comment.outputs.trigger == 'true')
|
||||
env:
|
||||
JENKINS_USER: ${{ secrets.JENKINS_USERNAME }}
|
||||
JENKINS_API_TOKEN: ${{ secrets.JENKINS_TOKEN }}
|
||||
JENKINS_URL: "https://jenkins.scylladb.com"
|
||||
PR_NUMBER: "${{ github.event.issue.number || github.event.pull_request.number }}"
|
||||
PR_REPO_NAME: "${{ github.event.repository.full_name }}"
|
||||
run: |
|
||||
curl -X POST "$JENKINS_URL/job/releng/job/Scylla-CI-Route/buildWithParameters?PR_NUMBER=$PR_NUMBER&PR_REPO_NAME=$PR_REPO_NAME" \
|
||||
--user "$JENKINS_USER:$JENKINS_API_TOKEN" --fail
|
||||
242
.github/workflows/trigger_ci.yaml
vendored
242
.github/workflows/trigger_ci.yaml
vendored
@@ -1,242 +0,0 @@
|
||||
name: Trigger next gating
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
trigger-ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Needed to access full history
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Fetch before commit if needed
|
||||
run: |
|
||||
if ! git cat-file -e ${{ github.event.before }} 2>/dev/null; then
|
||||
echo "Fetching before commit ${{ github.event.before }}"
|
||||
git fetch --depth=1 origin ${{ github.event.before }}
|
||||
fi
|
||||
|
||||
- name: Compare commits for file changes
|
||||
if: github.action == 'synchronize'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Base: ${{ github.event.before }}"
|
||||
echo "Head: ${{ github.event.after }}"
|
||||
|
||||
TREE_BEFORE=$(git show -s --format=%T ${{ github.event.before }})
|
||||
TREE_AFTER=$(git show -s --format=%T ${{ github.event.after }})
|
||||
|
||||
echo "TREE_BEFORE=$TREE_BEFORE" >> $GITHUB_ENV
|
||||
echo "TREE_AFTER=$TREE_AFTER" >> $GITHUB_ENV
|
||||
|
||||
- name: Check if last push has file changes
|
||||
run: |
|
||||
if [[ "${{ env.TREE_BEFORE }}" == "${{ env.TREE_AFTER }}" ]]; then
|
||||
echo "No file changes detected in the last push, only commit message edit."
|
||||
echo "has_file_changes=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "File changes detected in the last push."
|
||||
echo "has_file_changes=true" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Rule 1 - Check PR draft or conflict status
|
||||
run: |
|
||||
# Check if PR is in draft mode
|
||||
IS_DRAFT="${{ github.event.pull_request.draft }}"
|
||||
|
||||
# Check if PR has 'conflict' label
|
||||
HAS_CONFLICT_LABEL="false"
|
||||
LABELS='${{ toJson(github.event.pull_request.labels) }}'
|
||||
if echo "$LABELS" | jq -r '.[].name' | grep -q "^conflict$"; then
|
||||
HAS_CONFLICT_LABEL="true"
|
||||
fi
|
||||
|
||||
# Set draft_or_conflict variable
|
||||
if [[ "$IS_DRAFT" == "true" || "$HAS_CONFLICT_LABEL" == "true" ]]; then
|
||||
echo "draft_or_conflict=true" >> $GITHUB_ENV
|
||||
echo "✅ Rule 1: PR is in draft mode or has conflict label - setting draft_or_conflict=true"
|
||||
else
|
||||
echo "draft_or_conflict=false" >> $GITHUB_ENV
|
||||
echo "✅ Rule 1: PR is ready and has no conflict label - setting draft_or_conflict=false"
|
||||
fi
|
||||
|
||||
echo "Draft status: $IS_DRAFT"
|
||||
echo "Has conflict label: $HAS_CONFLICT_LABEL"
|
||||
echo "Result: draft_or_conflict = $draft_or_conflict"
|
||||
|
||||
- name: Rule 2 - Check labels
|
||||
run: |
|
||||
# Check if PR has P0 or P1 labels
|
||||
HAS_P0_P1_LABEL="false"
|
||||
LABELS='${{ toJson(github.event.pull_request.labels) }}'
|
||||
if echo "$LABELS" | jq -r '.[].name' | grep -E "^(P0|P1)$" > /dev/null; then
|
||||
HAS_P0_P1_LABEL="true"
|
||||
fi
|
||||
|
||||
# Check if PR already has force_on_cloud label
|
||||
echo "HAS_FORCE_ON_CLOUD_LABEL=false" >> $GITHUB_ENV
|
||||
if echo "$LABELS" | jq -r '.[].name' | grep -q "^force_on_cloud$"; then
|
||||
HAS_FORCE_ON_CLOUD_LABEL="true"
|
||||
echo "HAS_FORCE_ON_CLOUD_LABEL=true" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
echo "Has P0/P1 label: $HAS_P0_P1_LABEL"
|
||||
echo "Has force_on_cloud label: $HAS_FORCE_ON_CLOUD_LABEL"
|
||||
|
||||
# Add force_on_cloud label if PR has P0/P1 and doesn't already have force_on_cloud
|
||||
if [[ "$HAS_P0_P1_LABEL" == "true" && "$HAS_FORCE_ON_CLOUD_LABEL" == "false" ]]; then
|
||||
echo "✅ Rule 2: PR has P0 or P1 label - adding force_on_cloud label"
|
||||
curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" \
|
||||
-d '{"labels":["force_on_cloud"]}'
|
||||
elif [[ "$HAS_P0_P1_LABEL" == "true" && "$HAS_FORCE_ON_CLOUD_LABEL" == "true" ]]; then
|
||||
echo "✅ Rule 2: PR has P0 or P1 label and already has force_on_cloud label - no action needed"
|
||||
else
|
||||
echo "✅ Rule 2: PR does not have P0 or P1 label - no force_on_cloud label needed"
|
||||
fi
|
||||
|
||||
SKIP_UNIT_TEST_CUSTOM="false"
|
||||
if echo "$LABELS" | jq -r '.[].name' | grep -q "^ci/skip_unit-tests_custom$"; then
|
||||
SKIP_UNIT_TEST_CUSTOM="true"
|
||||
fi
|
||||
echo "SKIP_UNIT_TEST_CUSTOM=$SKIP_UNIT_TEST_CUSTOM" >> $GITHUB_ENV
|
||||
|
||||
- name: Rule 3 - Analyze changed files and set build requirements
|
||||
run: |
|
||||
# Get list of changed files
|
||||
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
|
||||
echo "Changed files:"
|
||||
echo "$CHANGED_FILES"
|
||||
echo ""
|
||||
|
||||
# Initialize all requirements to false
|
||||
REQUIRE_BUILD="false"
|
||||
REQUIRE_DTEST="false"
|
||||
REQUIRE_UNITTEST="false"
|
||||
REQUIRE_ARTIFACTS="false"
|
||||
REQUIRE_SCYLLA_GDB="false"
|
||||
|
||||
# Check each file against patterns
|
||||
while IFS= read -r file; do
|
||||
if [[ -n "$file" ]]; then
|
||||
echo "Checking file: $file"
|
||||
|
||||
# Build pattern: ^(?!scripts\/pull_github_pr.sh).*$
|
||||
# Everything except scripts/pull_github_pr.sh
|
||||
if [[ "$file" != "scripts/pull_github_pr.sh" ]]; then
|
||||
REQUIRE_BUILD="true"
|
||||
echo " ✓ Matches build pattern"
|
||||
fi
|
||||
|
||||
# Dtest pattern: ^(?!test(.py|\/)|dist\/docker\/|dist\/common\/scripts\/).*$
|
||||
# Everything except test files, dist/docker/, dist/common/scripts/
|
||||
if [[ ! "$file" =~ ^test\.(py|/).*$ ]] && [[ ! "$file" =~ ^dist/docker/.*$ ]] && [[ ! "$file" =~ ^dist/common/scripts/.*$ ]]; then
|
||||
REQUIRE_DTEST="true"
|
||||
echo " ✓ Matches dtest pattern"
|
||||
fi
|
||||
|
||||
# Unittest pattern: ^(?!dist\/docker\/|dist\/common\/scripts).*$
|
||||
# Everything except dist/docker/, dist/common/scripts/
|
||||
if [[ ! "$file" =~ ^dist/docker/.*$ ]] && [[ ! "$file" =~ ^dist/common/scripts.*$ ]]; then
|
||||
REQUIRE_UNITTEST="true"
|
||||
echo " ✓ Matches unittest pattern"
|
||||
fi
|
||||
|
||||
# Artifacts pattern: ^(?:dist|tools\/toolchain).*$
|
||||
# Files starting with dist or tools/toolchain
|
||||
if [[ "$file" =~ ^dist.*$ ]] || [[ "$file" =~ ^tools/toolchain.*$ ]]; then
|
||||
REQUIRE_ARTIFACTS="true"
|
||||
echo " ✓ Matches artifacts pattern"
|
||||
fi
|
||||
|
||||
# Scylla GDB pattern: ^(scylla-gdb.py).*$
|
||||
# Files starting with scylla-gdb.py
|
||||
if [[ "$file" =~ ^scylla-gdb\.py.*$ ]]; then
|
||||
REQUIRE_SCYLLA_GDB="true"
|
||||
echo " ✓ Matches scylla_gdb pattern"
|
||||
fi
|
||||
fi
|
||||
done <<< "$CHANGED_FILES"
|
||||
|
||||
# Set environment variables
|
||||
echo "requireBuild=$REQUIRE_BUILD" >> $GITHUB_ENV
|
||||
echo "requireDtest=$REQUIRE_DTEST" >> $GITHUB_ENV
|
||||
echo "requireUnittest=$REQUIRE_UNITTEST" >> $GITHUB_ENV
|
||||
echo "requireArtifacts=$REQUIRE_ARTIFACTS" >> $GITHUB_ENV
|
||||
echo "requireScyllaGdb=$REQUIRE_SCYLLA_GDB" >> $GITHUB_ENV
|
||||
|
||||
echo ""
|
||||
echo "✅ Rule 3: File analysis complete"
|
||||
echo "Build required: $REQUIRE_BUILD"
|
||||
echo "Dtest required: $REQUIRE_DTEST"
|
||||
echo "Unittest required: $REQUIRE_UNITTEST"
|
||||
echo "Artifacts required: $REQUIRE_ARTIFACTS"
|
||||
echo "Scylla GDB required: $REQUIRE_SCYLLA_GDB"
|
||||
|
||||
- name: Determine Jenkins Job Name
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "next" ]]; then
|
||||
FOLDER_NAME="scylla-master"
|
||||
elif [[ "${{ github.ref_name }}" == "next-enterprise" ]]; then
|
||||
FOLDER_NAME="scylla-enterprise"
|
||||
else
|
||||
VERSION=$(echo "${{ github.ref_name }}" | awk -F'-' '{print $2}')
|
||||
if [[ "$VERSION" =~ ^202[0-4]\.[0-9]+$ ]]; then
|
||||
FOLDER_NAME="enterprise-$VERSION"
|
||||
elif [[ "$VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
FOLDER_NAME="scylla-$VERSION"
|
||||
fi
|
||||
fi
|
||||
echo "JOB_NAME=${FOLDER_NAME}/job/scylla-ci" >> $GITHUB_ENV
|
||||
|
||||
- name: Trigger Jenkins Job
|
||||
if: env.draft_or_conflict == 'false' && env.has_file_changes == 'true' && github.action == 'opened' || github.action == 'reopened'
|
||||
env:
|
||||
JENKINS_USER: ${{ secrets.JENKINS_USERNAME }}
|
||||
JENKINS_API_TOKEN: ${{ secrets.JENKINS_TOKEN }}
|
||||
JENKINS_URL: "https://jenkins.scylladb.com"
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
run: |
|
||||
PR_NUMBER=${{ github.event.issue.number }}
|
||||
PR_REPO_NAME=${{ github.event.repository.full_name }}
|
||||
echo "Triggering Jenkins Job: $JOB_NAME"
|
||||
curl -X POST \
|
||||
"$JENKINS_URL/job/$JOB_NAME/buildWithParameters? \
|
||||
PR_NUMBER=$PR_NUMBER& \
|
||||
RUN_DTEST=$REQUIRE_DTEST& \
|
||||
RUN_ONLY_SCYLLA_GDB=$REQUIRE_SCYLLA_GDB& \
|
||||
RUN_UNIT_TEST=$REQUIRE_UNITTEST& \
|
||||
FORCE_ON_CLOUD=$HAS_FORCE_ON_CLOUD_LABEL& \
|
||||
SKIP_UNIT_TEST_CUSTOM=$SKIP_UNIT_TEST_CUSTOM& \
|
||||
RUN_ARTIFACT_TESTS=$REQUIRE_ARTIFACTS" \
|
||||
--fail \
|
||||
--user "$JENKINS_USER:$JENKINS_API_TOKEN" \
|
||||
-i -v
|
||||
trigger-ci-via-comment:
|
||||
if: github.event.comment.user.login != 'scylladbbot' && contains(github.event.comment.body, '@scylladbbot') && contains(github.event.comment.body, 'trigger-ci')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Scylla-CI Jenkins Job
|
||||
env:
|
||||
JENKINS_USER: ${{ secrets.JENKINS_USERNAME }}
|
||||
JENKINS_API_TOKEN: ${{ secrets.JENKINS_TOKEN }}
|
||||
JENKINS_URL: "https://jenkins.scylladb.com"
|
||||
run: |
|
||||
PR_NUMBER=${{ github.event.issue.number }}
|
||||
PR_REPO_NAME=${{ github.event.repository.full_name }}
|
||||
curl -X POST "$JENKINS_URL/job/$JOB_NAME/buildWithParameters?PR_NUMBER=$PR_NUMBER" \
|
||||
--user "$JENKINS_USER:$JENKINS_API_TOKEN" --fail -i -v
|
||||
53
.github/workflows/trigger_jenkins.yaml
vendored
53
.github/workflows/trigger_jenkins.yaml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Trigger next gating
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- next**
|
||||
|
||||
jobs:
|
||||
trigger-jenkins:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Determine Jenkins Job Name
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "next" ]]; then
|
||||
FOLDER_NAME="scylla-master"
|
||||
elif [[ "${{ github.ref_name }}" == "next-enterprise" ]]; then
|
||||
FOLDER_NAME="scylla-enterprise"
|
||||
else
|
||||
VERSION=$(echo "${{ github.ref_name }}" | awk -F'-' '{print $2}')
|
||||
if [[ "$VERSION" =~ ^202[0-4]\.[0-9]+$ ]]; then
|
||||
FOLDER_NAME="enterprise-$VERSION"
|
||||
elif [[ "$VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
FOLDER_NAME="scylla-$VERSION"
|
||||
fi
|
||||
fi
|
||||
echo "JOB_NAME=${FOLDER_NAME}/job/next" >> $GITHUB_ENV
|
||||
|
||||
- name: Trigger Jenkins Job
|
||||
env:
|
||||
JENKINS_USER: ${{ secrets.JENKINS_USERNAME }}
|
||||
JENKINS_API_TOKEN: ${{ secrets.JENKINS_TOKEN }}
|
||||
JENKINS_URL: "https://jenkins.scylladb.com"
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
run: |
|
||||
echo "Triggering Jenkins Job: $JOB_NAME"
|
||||
if ! curl -X POST "$JENKINS_URL/job/$JOB_NAME/buildWithParameters" --fail --user "$JENKINS_USER:$JENKINS_API_TOKEN" -i -v; then
|
||||
echo "Error: Jenkins job trigger failed"
|
||||
|
||||
# Send Slack message
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
|
||||
--data '{
|
||||
"channel": "#releng-team",
|
||||
"text": "🚨 @here '$JOB_NAME' failed to be triggered, please check https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} for more details",
|
||||
"icon_emoji": ":warning:"
|
||||
}' \
|
||||
https://slack.com/api/chat.postMessage
|
||||
|
||||
exit 1
|
||||
fi
|
||||
2
.github/workflows/urgent_issue_reminder.yml
vendored
2
.github/workflows/urgent_issue_reminder.yml
vendored
@@ -2,7 +2,7 @@ name: Urgent Issue Reminder
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '10 8 * * *' # Runs daily at 8 AM
|
||||
- cron: '10 8 * * 1' # Runs every Monday at 8 AM
|
||||
|
||||
jobs:
|
||||
reminder:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,5 +35,3 @@ compile_commands.json
|
||||
.envrc
|
||||
clang_build
|
||||
.idea/
|
||||
nuke
|
||||
rust/target
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -9,6 +9,9 @@
|
||||
[submodule "abseil"]
|
||||
path = abseil
|
||||
url = ../abseil-cpp
|
||||
[submodule "scylla-tools"]
|
||||
path = tools/java
|
||||
url = ../scylla-tools-java
|
||||
[submodule "scylla-python3"]
|
||||
path = tools/python3
|
||||
url = ../scylla-python3
|
||||
|
||||
142
CMakeLists.txt
142
CMakeLists.txt
@@ -22,8 +22,6 @@ if(DEFINED CMAKE_BUILD_TYPE)
|
||||
endif()
|
||||
endif(DEFINED CMAKE_BUILD_TYPE)
|
||||
|
||||
option(Scylla_ENABLE_LTO "Turn on link-time optimization for the 'release' mode." ON)
|
||||
|
||||
include(mode.common)
|
||||
get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
if(is_multi_config)
|
||||
@@ -44,12 +42,11 @@ else()
|
||||
endif()
|
||||
|
||||
include(limit_jobs)
|
||||
|
||||
# Configure Seastar compile options to align with Scylla
|
||||
set(CMAKE_CXX_STANDARD "23" CACHE INTERNAL "")
|
||||
set(CMAKE_CXX_EXTENSIONS ON CACHE INTERNAL "")
|
||||
set(CMAKE_CXX_SCAN_FOR_MODULES OFF CACHE INTERNAL "")
|
||||
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
|
||||
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
|
||||
|
||||
if(is_multi_config)
|
||||
find_package(Seastar)
|
||||
@@ -66,37 +63,36 @@ if(is_multi_config)
|
||||
# establishing proper dependencies between them
|
||||
include(ExternalProject)
|
||||
|
||||
# should be consistent with configure_seastar() in configure.py
|
||||
set(seastar_build_dir "${CMAKE_BINARY_DIR}/$<CONFIG>/seastar")
|
||||
ExternalProject_Add(Seastar
|
||||
SOURCE_DIR "${PROJECT_SOURCE_DIR}/seastar"
|
||||
BINARY_DIR "${CMAKE_BINARY_DIR}/$<CONFIG>/seastar"
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ${CMAKE_COMMAND} --build "${seastar_build_dir}"
|
||||
BUILD_COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR>
|
||||
--target seastar
|
||||
--target seastar_testing
|
||||
--target seastar_perf_testing
|
||||
--target app_iotune
|
||||
BUILD_ALWAYS ON
|
||||
BUILD_BYPRODUCTS
|
||||
${seastar_build_dir}/libseastar.$<IF:$<CONFIG:Debug,Dev>,so,a>
|
||||
${seastar_build_dir}/libseastar_testing.$<IF:$<CONFIG:Debug,Dev>,so,a>
|
||||
${seastar_build_dir}/libseastar_perf_testing.$<IF:$<CONFIG:Debug,Dev>,so,a>
|
||||
${seastar_build_dir}/apps/iotune/iotune
|
||||
${seastar_build_dir}/gen/include/seastar/http/chunk_parsers.hh
|
||||
${seastar_build_dir}/gen/include/seastar/http/request_parser.hh
|
||||
${seastar_build_dir}/gen/include/seastar/http/response_parser.hh
|
||||
<BINARY_DIR>/libseastar.$<IF:$<CONFIG:Debug,Dev>,so,a>
|
||||
<BINARY_DIR>/libseastar_testing.$<IF:$<CONFIG:Debug,Dev>,so,a>
|
||||
<BINARY_DIR>/libseastar_perf_testing.$<IF:$<CONFIG:Debug,Dev>,so,a>
|
||||
<BINARY_DIR>/apps/iotune/iotune
|
||||
<BINARY_DIR>/gen/include/seastar/http/chunk_parsers.hh
|
||||
<BINARY_DIR>/gen/include/seastar/http/request_parser.hh
|
||||
<BINARY_DIR>/gen/include/seastar/http/response_parser.hh
|
||||
INSTALL_COMMAND "")
|
||||
add_dependencies(Seastar::seastar Seastar)
|
||||
add_dependencies(Seastar::seastar_testing Seastar)
|
||||
else()
|
||||
set(Seastar_TESTING ON CACHE BOOL "" FORCE)
|
||||
set(Seastar_API_LEVEL 9 CACHE STRING "" FORCE)
|
||||
set(Seastar_API_LEVEL 7 CACHE STRING "" FORCE)
|
||||
set(Seastar_DEPRECATED_OSTREAM_FORMATTERS OFF CACHE BOOL "" FORCE)
|
||||
set(Seastar_APPS ON CACHE BOOL "" FORCE)
|
||||
set(Seastar_EXCLUDE_APPS_FROM_ALL ON CACHE BOOL "" FORCE)
|
||||
set(Seastar_EXCLUDE_TESTS_FROM_ALL ON CACHE BOOL "" FORCE)
|
||||
set(Seastar_IO_URING ON CACHE BOOL "" FORCE)
|
||||
set(Seastar_SCHEDULING_GROUPS_COUNT 21 CACHE STRING "" FORCE)
|
||||
set(Seastar_SCHEDULING_GROUPS_COUNT 16 CACHE STRING "" FORCE)
|
||||
set(Seastar_UNUSED_RESULT_ERROR ON CACHE BOOL "" FORCE)
|
||||
add_subdirectory(seastar)
|
||||
target_compile_definitions (seastar
|
||||
@@ -106,18 +102,13 @@ endif()
|
||||
|
||||
set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "" FORCE)
|
||||
|
||||
if(Scylla_ENABLE_LTO)
|
||||
list(APPEND absl_cxx_flags $<$<CONFIG:RelWithDebInfo>:${CMAKE_CXX_COMPILE_OPTIONS_IPO};-ffat-lto-objects>)
|
||||
endif()
|
||||
|
||||
find_package(Sanitizers QUIET)
|
||||
list(APPEND absl_cxx_flags
|
||||
set(sanitizer_cxx_flags
|
||||
$<$<CONFIG:Debug,Sanitize>:$<TARGET_PROPERTY:Sanitizers::address,INTERFACE_COMPILE_OPTIONS>;$<TARGET_PROPERTY:Sanitizers::undefined_behavior,INTERFACE_COMPILE_OPTIONS>>)
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
list(APPEND ABSL_GCC_FLAGS ${absl_cxx_flags})
|
||||
set(ABSL_GCC_FLAGS ${sanitizer_cxx_flags})
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
list(APPEND absl_cxx_flags "-Wno-deprecated-builtins")
|
||||
list(APPEND ABSL_LLVM_FLAGS ${absl_cxx_flags})
|
||||
set(ABSL_LLVM_FLAGS ${sanitizer_cxx_flags})
|
||||
endif()
|
||||
set(ABSL_DEFAULT_LINKOPTS
|
||||
$<$<CONFIG:Debug,Sanitize>:$<TARGET_PROPERTY:Sanitizers::address,INTERFACE_LINK_LIBRARIES>;$<TARGET_PROPERTY:Sanitizers::undefined_behavior,INTERFACE_LINK_LIBRARIES>>)
|
||||
@@ -150,13 +141,11 @@ find_package(ICU COMPONENTS uc i18n REQUIRED)
|
||||
find_package(fmt 10.0.0 REQUIRED)
|
||||
find_package(libdeflate REQUIRED)
|
||||
find_package(libxcrypt REQUIRED)
|
||||
find_package(p11-kit REQUIRED)
|
||||
find_package(Snappy REQUIRED)
|
||||
find_package(RapidJSON REQUIRED)
|
||||
find_package(xxHash REQUIRED)
|
||||
find_package(yaml-cpp REQUIRED)
|
||||
find_package(zstd REQUIRED)
|
||||
find_package(lz4 REQUIRED)
|
||||
|
||||
set(scylla_gen_build_dir "${CMAKE_BINARY_DIR}/gen")
|
||||
file(MAKE_DIRECTORY "${scylla_gen_build_dir}")
|
||||
@@ -164,68 +153,47 @@ file(MAKE_DIRECTORY "${scylla_gen_build_dir}")
|
||||
include(add_version_library)
|
||||
generate_scylla_version()
|
||||
|
||||
option(Scylla_USE_PRECOMPILED_HEADER "Use precompiled header for Scylla" ON)
|
||||
add_library(scylla-precompiled-header STATIC exported_templates.cc)
|
||||
target_link_libraries(scylla-precompiled-header PRIVATE
|
||||
absl::headers
|
||||
absl::btree
|
||||
absl::hash
|
||||
absl::raw_hash_set
|
||||
add_library(scylla-zstd STATIC
|
||||
zstd.cc)
|
||||
target_link_libraries(scylla-zstd
|
||||
PRIVATE
|
||||
db
|
||||
Seastar::seastar
|
||||
Snappy::snappy
|
||||
systemd
|
||||
ZLIB::ZLIB
|
||||
lz4::lz4_static
|
||||
zstd::zstd_static)
|
||||
if (Scylla_USE_PRECOMPILED_HEADER)
|
||||
set(Scylla_USE_PRECOMPILED_HEADER_USE ON)
|
||||
find_program(DISTCC_EXEC NAMES distcc OPTIONAL)
|
||||
if (DISTCC_EXEC)
|
||||
if(DEFINED ENV{DISTCC_HOSTS})
|
||||
set(Scylla_USE_PRECOMPILED_HEADER_USE OFF)
|
||||
message(STATUS "Disabling precompiled header usage because distcc exists and DISTCC_HOSTS is set, assuming you're using distributed compilation.")
|
||||
else()
|
||||
file(REAL_PATH "~/.distcc/hosts" DIST_CC_HOSTS_PATH EXPAND_TILDE)
|
||||
if (EXISTS ${DIST_CC_HOSTS_PATH})
|
||||
set(Scylla_USE_PRECOMPILED_HEADER_USE OFF)
|
||||
message(STATUS "Disabling precompiled header usage because distcc and ~/.distcc/hosts exists, assuming you're using distributed compilation.")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
if (Scylla_USE_PRECOMPILED_HEADER_USE)
|
||||
message(STATUS "Using precompiled header for Scylla - remember to add `sloppiness = pch_defines,time_macros` to ccache.conf, if you're using ccache.")
|
||||
target_precompile_headers(scylla-precompiled-header PRIVATE "stdafx.hh")
|
||||
target_compile_definitions(scylla-precompiled-header PRIVATE SCYLLA_USE_PRECOMPILED_HEADER)
|
||||
endif()
|
||||
else()
|
||||
set(Scylla_USE_PRECOMPILED_HEADER_USE OFF)
|
||||
endif()
|
||||
zstd::libzstd)
|
||||
|
||||
add_library(scylla-main STATIC)
|
||||
|
||||
target_sources(scylla-main
|
||||
PRIVATE
|
||||
absl-flat_hash_map.cc
|
||||
bytes.cc
|
||||
client_data.cc
|
||||
clocks-impl.cc
|
||||
sstable_dict_autotrainer.cc
|
||||
collection_mutation.cc
|
||||
compress.cc
|
||||
converting_mutation_partition_applier.cc
|
||||
counters.cc
|
||||
direct_failure_detector/failure_detector.cc
|
||||
duration.cc
|
||||
exceptions/exceptions.cc
|
||||
frozen_schema.cc
|
||||
generic_server.cc
|
||||
debug.cc
|
||||
init.cc
|
||||
keys/keys.cc
|
||||
keys.cc
|
||||
multishard_mutation_query.cc
|
||||
mutation_query.cc
|
||||
node_ops/task_manager_module.cc
|
||||
partition_slice_builder.cc
|
||||
query/query.cc
|
||||
querier.cc
|
||||
query.cc
|
||||
query_ranges_to_vnodes.cc
|
||||
query/query-result-set.cc
|
||||
query-result-set.cc
|
||||
tombstone_gc_options.cc
|
||||
tombstone_gc.cc
|
||||
reader_concurrency_semaphore.cc
|
||||
reader_concurrency_semaphore_group.cc
|
||||
row_cache.cc
|
||||
schema_mutations.cc
|
||||
serializer.cc
|
||||
service/direct_failure_detector/failure_detector.cc
|
||||
sstables_loader.cc
|
||||
table_helper.cc
|
||||
tasks/task_handler.cc
|
||||
@@ -236,6 +204,7 @@ target_sources(scylla-main
|
||||
vint-serialization.cc)
|
||||
target_link_libraries(scylla-main
|
||||
PRIVATE
|
||||
"$<LINK_LIBRARY:WHOLE_ARCHIVE,scylla-zstd>"
|
||||
db
|
||||
absl::headers
|
||||
absl::btree
|
||||
@@ -244,11 +213,7 @@ target_link_libraries(scylla-main
|
||||
Seastar::seastar
|
||||
Snappy::snappy
|
||||
systemd
|
||||
ZLIB::ZLIB
|
||||
lz4::lz4_static
|
||||
zstd::zstd_static
|
||||
scylla-precompiled-header
|
||||
)
|
||||
ZLIB::ZLIB)
|
||||
|
||||
option(Scylla_CHECK_HEADERS
|
||||
"Add check-headers target for checking the self-containness of headers")
|
||||
@@ -283,7 +248,6 @@ add_custom_target(compiler-training)
|
||||
|
||||
add_subdirectory(api)
|
||||
add_subdirectory(alternator)
|
||||
add_subdirectory(audit)
|
||||
add_subdirectory(db)
|
||||
add_subdirectory(auth)
|
||||
add_subdirectory(cdc)
|
||||
@@ -291,7 +255,6 @@ add_subdirectory(compaction)
|
||||
add_subdirectory(cql3)
|
||||
add_subdirectory(data_dictionary)
|
||||
add_subdirectory(dht)
|
||||
add_subdirectory(ent)
|
||||
add_subdirectory(gms)
|
||||
add_subdirectory(idl)
|
||||
add_subdirectory(index)
|
||||
@@ -300,7 +263,9 @@ add_subdirectory(locator)
|
||||
add_subdirectory(message)
|
||||
add_subdirectory(mutation)
|
||||
add_subdirectory(mutation_writer)
|
||||
add_subdirectory(node_ops)
|
||||
add_subdirectory(readers)
|
||||
add_subdirectory(redis)
|
||||
add_subdirectory(replica)
|
||||
add_subdirectory(raft)
|
||||
add_subdirectory(repair)
|
||||
@@ -315,14 +280,12 @@ add_subdirectory(tracing)
|
||||
add_subdirectory(transport)
|
||||
add_subdirectory(types)
|
||||
add_subdirectory(utils)
|
||||
add_subdirectory(vector_search)
|
||||
add_version_library(scylla_version
|
||||
release.cc)
|
||||
|
||||
add_executable(scylla
|
||||
main.cc)
|
||||
set(scylla_libs
|
||||
audit
|
||||
target_link_libraries(scylla PRIVATE
|
||||
scylla-main
|
||||
api
|
||||
auth
|
||||
@@ -333,18 +296,17 @@ set(scylla_libs
|
||||
cql3
|
||||
data_dictionary
|
||||
dht
|
||||
encryption
|
||||
gms
|
||||
idl
|
||||
index
|
||||
lang
|
||||
ldap
|
||||
locator
|
||||
message
|
||||
mutation
|
||||
mutation_writer
|
||||
raft
|
||||
readers
|
||||
redis
|
||||
repair
|
||||
replica
|
||||
schema
|
||||
@@ -357,20 +319,9 @@ set(scylla_libs
|
||||
tracing
|
||||
transport
|
||||
types
|
||||
utils
|
||||
vector_search)
|
||||
target_link_libraries(scylla PRIVATE
|
||||
${scylla_libs})
|
||||
|
||||
if(Scylla_ENABLE_LTO)
|
||||
include(enable_lto)
|
||||
foreach(target scylla ${scylla_libs})
|
||||
enable_lto(${target})
|
||||
endforeach()
|
||||
endif()
|
||||
utils)
|
||||
|
||||
target_link_libraries(scylla PRIVATE
|
||||
p11-kit::p11-kit
|
||||
Seastar::seastar
|
||||
absl::headers
|
||||
yaml-cpp::yaml-cpp
|
||||
@@ -388,10 +339,3 @@ add_dependencies(compiler-training
|
||||
if(Scylla_DIST)
|
||||
add_subdirectory(dist)
|
||||
endif()
|
||||
|
||||
if(Scylla_BUILD_INSTRUMENTED)
|
||||
add_subdirectory(pgo)
|
||||
endif()
|
||||
|
||||
add_executable(patchelf
|
||||
tools/patchelf.cc)
|
||||
|
||||
@@ -12,7 +12,7 @@ Please use the [issue tracker](https://github.com/scylladb/scylla/issues/) to re
|
||||
|
||||
## Contributing code to Scylla
|
||||
|
||||
Before you can contribute code to Scylla for the first time, you should sign the [Contributor License Agreement](https://www.scylladb.com/open-source/contributor-agreement/) and send the signed form to cla@scylladb.com. You can then submit your changes as patches to the [scylladb-dev mailing list](https://groups.google.com/forum/#!forum/scylladb-dev) or as a pull request to the [Scylla project on github](https://github.com/scylladb/scylla).
|
||||
Before you can contribute code to Scylla for the first time, you should sign the [Contributor License Agreement](https://www.scylladb.com/open-source/contributor-agreement/) and send the signed form cla@scylladb.com. You can then submit your changes as patches to the to the [scylladb-dev mailing list](https://groups.google.com/forum/#!forum/scylladb-dev) or as a pull request to the [Scylla project on github](https://github.com/scylladb/scylla).
|
||||
If you need help formatting or sending patches, [check out these instructions](https://github.com/scylladb/scylla/wiki/Formatting-and-sending-patches).
|
||||
|
||||
The Scylla C++ source code uses the [Seastar coding style](https://github.com/scylladb/seastar/blob/master/coding-style.md) so please adhere to that in your patches. Note that Scylla code is written with `using namespace seastar`, so should not explicitly add the `seastar::` prefix to Seastar symbols. You will usually not need to add `using namespace seastar` to new source files, because most Scylla header files have `#include "seastarx.hh"`, which does this.
|
||||
|
||||
69
HACKING.md
69
HACKING.md
@@ -43,7 +43,7 @@ $ ./tools/toolchain/dbuild ninja build/release/scylla
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --developer-mode 1
|
||||
```
|
||||
|
||||
Note: do not mix environments - either perform all your work with dbuild, or natively on the host.
|
||||
Note: do not mix environemtns - either perform all your work with dbuild, or natively on the host.
|
||||
Note2: you can get to an interactive shell within dbuild by running it without any parameters:
|
||||
```bash
|
||||
$ ./tools/toolchain/dbuild
|
||||
@@ -91,7 +91,7 @@ You can also specify a single mode. For example
|
||||
$ ninja-build release
|
||||
```
|
||||
|
||||
Will build everything in release mode. The valid modes are
|
||||
Will build everytihng in release mode. The valid modes are
|
||||
|
||||
* Debug: Enables [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer)
|
||||
and other sanity checks. It has no optimizations, which allows for debugging with tools like
|
||||
@@ -220,9 +220,28 @@ On a development machine, one might run Scylla as
|
||||
$ SCYLLA_HOME=$HOME/scylla build/release/scylla --overprovisioned --developer-mode=yes
|
||||
```
|
||||
|
||||
To interact with scylla it is recommended to build our version of
|
||||
cqlsh. It is available at
|
||||
https://github.com/scylladb/scylla-cqlsh and is available as a submodule.
|
||||
To interact with scylla it is recommended to build our versions of
|
||||
cqlsh and nodetool. They are available at
|
||||
https://github.com/scylladb/scylla-tools-java and can be built with
|
||||
|
||||
```bash
|
||||
$ sudo ./install-dependencies.sh
|
||||
$ ant jar
|
||||
```
|
||||
|
||||
cqlsh should work out of the box, but nodetool depends on a running
|
||||
scylla-jmx (https://github.com/scylladb/scylla-jmx). It can be build
|
||||
with
|
||||
|
||||
```bash
|
||||
$ mvn package
|
||||
```
|
||||
|
||||
and must be started with
|
||||
|
||||
```bash
|
||||
$ ./scripts/scylla-jmx
|
||||
```
|
||||
|
||||
### Branches and tags
|
||||
|
||||
@@ -261,45 +280,21 @@ Once the patch set is ready to be reviewed, push the branch to the public remote
|
||||
|
||||
### Development environment and source code navigation
|
||||
|
||||
Scylla includes a [CMake](https://cmake.org/) file, `CMakeLists.txt` that can be used with development environments so
|
||||
that they can properly analyze the source code. However, building with CMake is not yet officially supported.
|
||||
Scylla includes a [CMake](https://cmake.org/) file, `CMakeLists.txt`, for use only with development environments (not for building) so that they can properly analyze the source code.
|
||||
|
||||
Good IDEs that have support for CMake build toolchain are [CLion](https://www.jetbrains.com/clion/),
|
||||
[KDevelop](https://www.kdevelop.org/) and [QtCreator](https://wiki.qt.io/Qt_Creator).
|
||||
[CLion](https://www.jetbrains.com/clion/) is a commercial IDE offers reasonably good source code navigation and advice for code hygiene, though its C++ parser sometimes makes errors and flags false issues.
|
||||
|
||||
[Eclipse](https://eclipse.org/cdt/) is another open-source option. It doesn't natively work with CMake projects and its
|
||||
C++ parser has many issues.
|
||||
Other good options that directly parse CMake files are [KDevelop](https://www.kdevelop.org/) and [QtCreator](https://wiki.qt.io/Qt_Creator).
|
||||
|
||||
#### CLion
|
||||
To use the `CMakeLists.txt` file with these programs, define the `FOR_IDE` CMake variable or shell environmental variable.
|
||||
|
||||
[CLion](https://www.jetbrains.com/clion/) is a commercial IDE offers reasonably good source code navigation and advice
|
||||
for code hygiene, though its C++ parser sometimes makes errors and flags false issues. In order to enable proper code
|
||||
analysis in CLion, the following steps are needed:
|
||||
|
||||
1. Get the ScyllaDB source code by following the [Getting the source code](#getting-the-source-code).
|
||||
2. Follow the steps in [Dependencies](#dependencies) in order to install the required tools natively into your system.
|
||||
**Don't** follow the *frozen toolchain* part described there, since CMake checks for the build dependencies installed
|
||||
in the system, not in the container image provided by the toolchain.
|
||||
3. In CLion, select `File`→`Open` and select the main ScyllaDB directory in order to open the CMake project there. The
|
||||
project should open and fail to process the `CMakeLists.txt`. That's expected.
|
||||
4. In CLion, open `File`→`Settings`.
|
||||
5. Find and click on `Toolchains` (type *toolchains* into search box).
|
||||
6. Select the toolchain you will use, for instance the `Default` one.
|
||||
7. Type in the following system-installed tools to be used:
|
||||
- `CMake`: *cmake*
|
||||
- `Build Tool`: *ninja*
|
||||
- `C Compiler`: *clang*
|
||||
- `C++ Compiler`: *clang*
|
||||
8. On the `CMake` panel/tab, click on `Reload CMake Project`
|
||||
|
||||
After that, CLion should successfully initialize the CMake project (marked by `[Finished]` in the console) and the
|
||||
source code editor should provide code analysis support normally from now on.
|
||||
[Eclipse](https://eclipse.org/cdt/) is another open-source option. It doesn't natively work with CMake projects, and its C++ parser has many similar issues as CLion.
|
||||
|
||||
### Distributed compilation: `distcc` and `ccache`
|
||||
|
||||
Scylla's compilations times can be long. Two tools help somewhat:
|
||||
|
||||
- [ccache](https://ccache.samba.org/) caches compiled object files on disk and reuses them when possible
|
||||
- [ccache](https://ccache.samba.org/) caches compiled object files on disk and re-uses them when possible
|
||||
- [distcc](https://github.com/distcc/distcc) distributes compilation jobs to remote machines
|
||||
|
||||
A reasonably-powered laptop acts as the coordinator for compilation. A second, more powerful, machine acts as a passive compilation server.
|
||||
@@ -361,7 +356,7 @@ avoid that the gold linker can be told to create an index with
|
||||
|
||||
More info at https://gcc.gnu.org/wiki/DebugFission.
|
||||
|
||||
Both options can be enabled by passing `--split-dwarf` to configure.py.
|
||||
Both options can be enable by passing `--split-dwarf` to configure.py.
|
||||
|
||||
Note that distcc is *not* compatible with it, but icecream
|
||||
(https://github.com/icecc/icecream) is.
|
||||
@@ -370,7 +365,7 @@ Note that distcc is *not* compatible with it, but icecream
|
||||
|
||||
Sometimes Scylla development is closely tied with a feature being developed in Seastar. It can be useful to compile Scylla with a particular check-out of Seastar.
|
||||
|
||||
One way to do this is to create a local remote for the Seastar submodule in the Scylla repository:
|
||||
One way to do this it to create a local remote for the Seastar submodule in the Scylla repository:
|
||||
|
||||
```bash
|
||||
$ cd $HOME/src/scylla
|
||||
|
||||
@@ -49,7 +49,7 @@ The terms "**You**" or "**Licensee**" refer to any individual accessing or using
|
||||
|
||||
* **Ownership:** Licensor retains sole and exclusive ownership of all rights, interests and title in the Software and any scripts, processes, techniques, methodologies, inventions, know-how, concepts, formatting, arrangements, visual attributes, ideas, database rights, copyrights, patents, trade secrets, and other intellectual property related thereto, and all derivatives, enhancements, modifications and improvements thereof. Except for the limited license rights granted herein, Licensee has no rights in or to the Software and/ or Licensor’s trademarks, logo, or branding and You acknowledge that such Software, trademarks, logo, or branding is the sole property of Licensor.
|
||||
* **Feedback:** Licensee is not required to provide any suggestions, enhancement requests, recommendations or other feedback regarding the Software ("Feedback"). If, notwithstanding this policy, Licensee submits Feedback, Licensee understands and acknowledges that such Feedback is not submitted in confidence and Licensor assumes no obligation, expressed or implied, by considering it. All right in any trademark or logo of Licensor or its affiliates and You shall make no claim of right to the Software or any part thereof to be supplied by Licensor hereunder and acknowledges that as between Licensor and You, such Software is the sole proprietary, title and interest in and to Licensor.such Feedback shall be assigned to, and shall become the sole and exclusive property of, Licensor upon its creation.
|
||||
* Except for the rights expressly granted to You under this Agreement, You are not granted any other licenses or rights in the Software or otherwise. This Agreement constitutes the entire agreement between You and the Licensor with respect to the subject matter hereof and supersedes all prior or contemporaneous communications, representations, or agreements, whether oral or written.
|
||||
* Except for the rights expressly granted to You under this Agreement, You are not granted any other licenses or rights in the Software or otherwise. This Agreement constitutes the entire agreement between the You and the Licensor with respect to the subject matter hereof and supersedes all prior or contemporaneous communications, representations, or agreements, whether oral or written.
|
||||
* **Third-Party Software:** Customer acknowledges that the Software may contain open and closed source components (“OSS Components”) that are governed separately by certain licenses, in each case as further provided by Company upon request. Any applicable OSS Component license is solely between Licensee and the applicable licensor of the OSS Component and Licensee shall comply with the applicable OSS Component license.
|
||||
* If any provision of this Agreement is held to be invalid or unenforceable, such provision shall be struck and the remaining provisions shall remain in full force and effect.
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
This project includes code developed by the Apache Software Foundation (http://www.apache.org/),
|
||||
especially Apache Cassandra.
|
||||
|
||||
It includes files from https://github.com/antonblanchard/crc32-vpmsum (author Anton Blanchard <anton@au.ibm.com>, IBM).
|
||||
These files are located in utils/arch/powerpc/crc32-vpmsum. Their license may be found in licenses/LICENSE-crc32-vpmsum.TXT.
|
||||
|
||||
It includes modified code from https://gitbox.apache.org/repos/asf?p=cassandra-dtest.git (owned by The Apache Software Foundation)
|
||||
|
||||
It includes modified tests from https://github.com/etcd-io/etcd.git (owned by The etcd Authors)
|
||||
|
||||
@@ -18,7 +18,7 @@ Scylla is fairly fussy about its build environment, requiring very recent
|
||||
versions of the C++23 compiler and of many libraries to build. The document
|
||||
[HACKING.md](HACKING.md) includes detailed information on building and
|
||||
developing Scylla, but to get Scylla building quickly on (almost) any build
|
||||
machine, Scylla offers a [frozen toolchain](tools/toolchain/README.md).
|
||||
machine, Scylla offers a [frozen toolchain](tools/toolchain/README.md),
|
||||
This is a pre-configured Docker image which includes recent versions of all
|
||||
the required compilers, libraries and build tools. Using the frozen toolchain
|
||||
allows you to avoid changing anything in your build machine to meet Scylla's
|
||||
@@ -43,7 +43,7 @@ For further information, please see:
|
||||
|
||||
[developer documentation]: HACKING.md
|
||||
[build documentation]: docs/dev/building.md
|
||||
[docker image build documentation]: dist/docker/redhat/README.md
|
||||
[docker image build documentation]: dist/docker/debian/README.md
|
||||
|
||||
## Running Scylla
|
||||
|
||||
@@ -102,7 +102,7 @@ If you are a developer working on Scylla, please read the [developer guidelines]
|
||||
|
||||
## Contact
|
||||
|
||||
* The [community forum] and [Slack channel] are for users to discuss configuration, management, and operations of ScyllaDB.
|
||||
* The [community forum] and [Slack channel] are for users to discuss configuration, management, and operations of the ScyllaDB open source.
|
||||
* The [developers mailing list] is for developers and people interested in following the development of ScyllaDB to discuss technical topics.
|
||||
|
||||
[Community forum]: https://forum.scylladb.com/
|
||||
|
||||
@@ -78,7 +78,7 @@ fi
|
||||
|
||||
# Default scylla product/version tags
|
||||
PRODUCT=scylla
|
||||
VERSION=2026.2.0-dev
|
||||
VERSION=6.3.0-dev
|
||||
|
||||
if test -f version
|
||||
then
|
||||
|
||||
@@ -17,8 +17,6 @@ target_sources(alternator
|
||||
streams.cc
|
||||
consumed_capacity.cc
|
||||
ttl.cc
|
||||
parsed_expression_cache.cc
|
||||
http_compression.cc
|
||||
${cql_grammar_srcs})
|
||||
target_include_directories(alternator
|
||||
PUBLIC
|
||||
@@ -35,8 +33,5 @@ target_link_libraries(alternator
|
||||
idl
|
||||
absl::headers)
|
||||
|
||||
if (Scylla_USE_PRECOMPILED_HEADER_USE)
|
||||
target_precompile_headers(alternator REUSE_FROM scylla-precompiled-header)
|
||||
endif()
|
||||
check_headers(check-headers alternator
|
||||
GLOB_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/*.hh)
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
#include "utils/log.hh"
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "bytes.hh"
|
||||
#include "alternator/auth.hh"
|
||||
#include <fmt/format.h>
|
||||
#include "db/consistency_level_type.hh"
|
||||
#include "db/system_keyspace.hh"
|
||||
#include "auth/password_authenticator.hh"
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "alternator/executor.hh"
|
||||
#include "cql3/selection/selection.hh"
|
||||
@@ -26,8 +26,8 @@ namespace alternator {
|
||||
|
||||
static logging::logger alogger("alternator-auth");
|
||||
|
||||
future<std::string> get_key_from_roles(service::storage_proxy& proxy, std::string username) {
|
||||
schema_ptr schema = proxy.data_dictionary().find_schema(db::system_keyspace::NAME, "roles");
|
||||
future<std::string> get_key_from_roles(service::storage_proxy& proxy, auth::service& as, std::string username) {
|
||||
schema_ptr schema = proxy.data_dictionary().find_schema(auth::get_auth_ks_name(as.query_processor()), "roles");
|
||||
partition_key pk = partition_key::from_single_value(*schema, utf8_type->decompose(username));
|
||||
dht::partition_range_vector partition_ranges{dht::partition_range(dht::decorate_key(*schema, pk))};
|
||||
std::vector<query::clustering_range> bounds{query::clustering_range::make_open_ended_both_sides()};
|
||||
@@ -40,7 +40,7 @@ future<std::string> get_key_from_roles(service::storage_proxy& proxy, std::strin
|
||||
auto partition_slice = query::partition_slice(std::move(bounds), {}, query::column_id_vector{salted_hash_col->id, can_login_col->id}, selection->get_query_options());
|
||||
auto command = ::make_lw_shared<query::read_command>(schema->id(), schema->version(), partition_slice,
|
||||
proxy.get_max_result_size(partition_slice), query::tombstone_limit(proxy.get_tombstone_limit()));
|
||||
auto cl = db::consistency_level::LOCAL_ONE;
|
||||
auto cl = auth::password_authenticator::consistency_for_user(username);
|
||||
|
||||
service::client_state client_state{service::client_state::internal_tag()};
|
||||
service::storage_proxy::coordinator_query_result qr = co_await proxy.query(schema, std::move(command), std::move(partition_ranges), cl,
|
||||
|
||||
@@ -20,6 +20,6 @@ namespace alternator {
|
||||
|
||||
using key_cache = utils::loading_cache<std::string, std::string, 1>;
|
||||
|
||||
future<std::string> get_key_from_roles(service::storage_proxy& proxy, std::string username);
|
||||
future<std::string> get_key_from_roles(service::storage_proxy& proxy, auth::service& as, std::string username);
|
||||
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ comparison_operator_type get_comparison_operator(const rjson::value& comparison_
|
||||
if (!comparison_operator.IsString()) {
|
||||
throw api_error::validation(fmt::format("Invalid comparison operator definition {}", rjson::print(comparison_operator)));
|
||||
}
|
||||
std::string op = rjson::to_string(comparison_operator);
|
||||
std::string op = comparison_operator.GetString();
|
||||
auto it = ops.find(op);
|
||||
if (it == ops.end()) {
|
||||
throw api_error::validation(fmt::format("Unsupported comparison operator {}", op));
|
||||
@@ -377,8 +377,8 @@ bool check_compare(const rjson::value* v1, const rjson::value& v2, const Compara
|
||||
return cmp(unwrap_number(*v1, cmp.diagnostic), unwrap_number(v2, cmp.diagnostic));
|
||||
}
|
||||
if (kv1.name == "S") {
|
||||
return cmp(rjson::to_string_view(kv1.value),
|
||||
rjson::to_string_view(kv2.value));
|
||||
return cmp(std::string_view(kv1.value.GetString(), kv1.value.GetStringLength()),
|
||||
std::string_view(kv2.value.GetString(), kv2.value.GetStringLength()));
|
||||
}
|
||||
if (kv1.name == "B") {
|
||||
auto d_kv1 = unwrap_bytes(kv1.value, v1_from_query);
|
||||
@@ -470,9 +470,9 @@ static bool check_BETWEEN(const rjson::value* v, const rjson::value& lb, const r
|
||||
return check_BETWEEN(unwrap_number(*v, diag), unwrap_number(lb, diag), unwrap_number(ub, diag), bounds_from_query);
|
||||
}
|
||||
if (kv_v.name == "S") {
|
||||
return check_BETWEEN(rjson::to_string_view(kv_v.value),
|
||||
rjson::to_string_view(kv_lb.value),
|
||||
rjson::to_string_view(kv_ub.value),
|
||||
return check_BETWEEN(std::string_view(kv_v.value.GetString(), kv_v.value.GetStringLength()),
|
||||
std::string_view(kv_lb.value.GetString(), kv_lb.value.GetStringLength()),
|
||||
std::string_view(kv_ub.value.GetString(), kv_ub.value.GetStringLength()),
|
||||
bounds_from_query);
|
||||
}
|
||||
if (kv_v.name == "B") {
|
||||
@@ -618,7 +618,7 @@ conditional_operator_type get_conditional_operator(const rjson::value& req) {
|
||||
// Check if the existing values of the item (previous_item) match the
|
||||
// conditions given by the Expected and ConditionalOperator parameters
|
||||
// (if they exist) in the request (an UpdateItem, PutItem or DeleteItem).
|
||||
// This function can throw a ValidationException API error if there
|
||||
// This function can throw an ValidationException API error if there
|
||||
// are errors in the format of the condition itself.
|
||||
bool verify_expected(const rjson::value& req, const rjson::value* previous_item) {
|
||||
const rjson::value* expected = rjson::find(req, "Expected");
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
#include "consumed_capacity.hh"
|
||||
#include "error.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -26,7 +24,7 @@ static constexpr uint64_t KB = 1024ULL;
|
||||
static constexpr uint64_t RCU_BLOCK_SIZE_LENGTH = 4*KB;
|
||||
static constexpr uint64_t WCU_BLOCK_SIZE_LENGTH = 1*KB;
|
||||
|
||||
bool consumed_capacity_counter::should_add_capacity(const rjson::value& request) {
|
||||
static bool should_add_capacity(const rjson::value& request) {
|
||||
const rjson::value* return_consumed = rjson::find(request, "ReturnConsumedCapacity");
|
||||
if (!return_consumed) {
|
||||
return false;
|
||||
@@ -34,18 +32,18 @@ bool consumed_capacity_counter::should_add_capacity(const rjson::value& request)
|
||||
if (!return_consumed->IsString()) {
|
||||
throw api_error::validation("Non-string ReturnConsumedCapacity field in request");
|
||||
}
|
||||
std::string_view consumed = rjson::to_string_view(*return_consumed);
|
||||
std::string consumed = return_consumed->GetString();
|
||||
if (consumed == "INDEXES") {
|
||||
throw api_error::validation("INDEXES consumed capacity is not supported");
|
||||
}
|
||||
if (consumed != "TOTAL") {
|
||||
throw api_error::validation(fmt::format("Unknown consumed capacity {}", consumed));
|
||||
throw api_error::validation("Unknown consumed capacity "+ consumed);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void consumed_capacity_counter::add_consumed_capacity_to_response_if_needed(rjson::value& response) const noexcept {
|
||||
if (_should_add_to_response) {
|
||||
if (_should_add_to_reponse) {
|
||||
auto consumption = rjson::empty_object();
|
||||
rjson::add(consumption, "CapacityUnits", get_consumed_capacity_units());
|
||||
rjson::add(response, "ConsumedCapacity", std::move(consumption));
|
||||
@@ -64,22 +62,15 @@ static uint64_t calculate_half_units(uint64_t unit_block_size, uint64_t total_by
|
||||
rcu_consumed_capacity_counter::rcu_consumed_capacity_counter(const rjson::value& request, bool is_quorum) :
|
||||
consumed_capacity_counter(should_add_capacity(request)),_is_quorum(is_quorum) {
|
||||
}
|
||||
uint64_t rcu_consumed_capacity_counter::get_half_units(uint64_t total_bytes, bool is_quorum) noexcept {
|
||||
return calculate_half_units(RCU_BLOCK_SIZE_LENGTH, total_bytes, is_quorum);
|
||||
}
|
||||
|
||||
uint64_t rcu_consumed_capacity_counter::get_half_units() const noexcept {
|
||||
return get_half_units(_total_bytes, _is_quorum);
|
||||
return calculate_half_units(RCU_BLOCK_SIZE_LENGTH, _total_bytes, _is_quorum);
|
||||
}
|
||||
|
||||
uint64_t wcu_consumed_capacity_counter::get_half_units() const noexcept {
|
||||
return calculate_half_units(WCU_BLOCK_SIZE_LENGTH, _total_bytes, true);
|
||||
}
|
||||
|
||||
uint64_t wcu_consumed_capacity_counter::get_units(uint64_t total_bytes) noexcept {
|
||||
return calculate_half_units(WCU_BLOCK_SIZE_LENGTH, total_bytes, true) * HALF_UNIT_MULTIPLIER;
|
||||
}
|
||||
|
||||
wcu_consumed_capacity_counter::wcu_consumed_capacity_counter(const rjson::value& request) :
|
||||
consumed_capacity_counter(should_add_capacity(request)) {
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ namespace alternator {
|
||||
class consumed_capacity_counter {
|
||||
public:
|
||||
consumed_capacity_counter() = default;
|
||||
consumed_capacity_counter(bool should_add_to_response) : _should_add_to_response(should_add_to_response){}
|
||||
consumed_capacity_counter(bool should_add_to_reponse) : _should_add_to_reponse(should_add_to_reponse){}
|
||||
bool operator()() const noexcept {
|
||||
return _should_add_to_response;
|
||||
return _should_add_to_reponse;
|
||||
}
|
||||
|
||||
consumed_capacity_counter& operator +=(uint64_t bytes);
|
||||
@@ -42,25 +42,21 @@ public:
|
||||
*/
|
||||
virtual uint64_t get_half_units() const noexcept = 0;
|
||||
uint64_t _total_bytes = 0;
|
||||
static bool should_add_capacity(const rjson::value& request);
|
||||
protected:
|
||||
bool _should_add_to_response = false;
|
||||
bool _should_add_to_reponse = false;
|
||||
};
|
||||
|
||||
class rcu_consumed_capacity_counter : public consumed_capacity_counter {
|
||||
virtual uint64_t get_half_units() const noexcept;
|
||||
bool _is_quorum = false;
|
||||
public:
|
||||
rcu_consumed_capacity_counter(const rjson::value& request, bool is_quorum);
|
||||
rcu_consumed_capacity_counter(): consumed_capacity_counter(false), _is_quorum(false){}
|
||||
virtual uint64_t get_half_units() const noexcept;
|
||||
static uint64_t get_half_units(uint64_t total_bytes, bool is_quorum) noexcept;
|
||||
};
|
||||
|
||||
class wcu_consumed_capacity_counter : public consumed_capacity_counter {
|
||||
virtual uint64_t get_half_units() const noexcept;
|
||||
public:
|
||||
wcu_consumed_capacity_counter(const rjson::value& request);
|
||||
static uint64_t get_units(uint64_t total_bytes) noexcept;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
*/
|
||||
|
||||
#include <seastar/core/with_scheduling_group.hh>
|
||||
#include <seastar/net/dns.hh>
|
||||
|
||||
#include "controller.hh"
|
||||
#include "server.hh"
|
||||
#include "executor.hh"
|
||||
@@ -28,7 +26,6 @@ static logging::logger logger("alternator_controller");
|
||||
controller::controller(
|
||||
sharded<gms::gossiper>& gossiper,
|
||||
sharded<service::storage_proxy>& proxy,
|
||||
sharded<service::storage_service>& ss,
|
||||
sharded<service::migration_manager>& mm,
|
||||
sharded<db::system_distributed_keyspace>& sys_dist_ks,
|
||||
sharded<cdc::generation_service>& cdc_gen_svc,
|
||||
@@ -40,7 +37,6 @@ controller::controller(
|
||||
: protocol_server(sg)
|
||||
, _gossiper(gossiper)
|
||||
, _proxy(proxy)
|
||||
, _ss(ss)
|
||||
, _mm(mm)
|
||||
, _sys_dist_ks(sys_dist_ks)
|
||||
, _cdc_gen_svc(cdc_gen_svc)
|
||||
@@ -91,7 +87,7 @@ future<> controller::start_server() {
|
||||
auto get_timeout_in_ms = [] (const db::config& cfg) -> utils::updateable_value<uint32_t> {
|
||||
return cfg.alternator_timeout_in_ms;
|
||||
};
|
||||
_executor.start(std::ref(_gossiper), std::ref(_proxy), std::ref(_ss), std::ref(_mm), std::ref(_sys_dist_ks),
|
||||
_executor.start(std::ref(_gossiper), std::ref(_proxy), std::ref(_mm), std::ref(_sys_dist_ks),
|
||||
sharded_parameter(get_cdc_metadata, std::ref(_cdc_gen_svc)), _ssg.value(),
|
||||
sharded_parameter(get_timeout_in_ms, std::ref(_config))).get();
|
||||
_server.start(std::ref(_executor), std::ref(_proxy), std::ref(_gossiper), std::ref(_auth_service), std::ref(_sl_controller)).get();
|
||||
@@ -105,23 +101,11 @@ future<> controller::start_server() {
|
||||
alternator_port = _config.alternator_port();
|
||||
_listen_addresses.push_back({addr, *alternator_port});
|
||||
}
|
||||
std::optional<uint16_t> alternator_port_proxy_protocol;
|
||||
if (_config.alternator_port_proxy_protocol()) {
|
||||
alternator_port_proxy_protocol = _config.alternator_port_proxy_protocol();
|
||||
_listen_addresses.push_back({addr, *alternator_port_proxy_protocol});
|
||||
}
|
||||
std::optional<uint16_t> alternator_https_port;
|
||||
std::optional<uint16_t> alternator_https_port_proxy_protocol;
|
||||
std::optional<tls::credentials_builder> creds;
|
||||
if (_config.alternator_https_port() || _config.alternator_https_port_proxy_protocol()) {
|
||||
if (_config.alternator_https_port()) {
|
||||
alternator_https_port = _config.alternator_https_port();
|
||||
_listen_addresses.push_back({addr, *alternator_https_port});
|
||||
}
|
||||
if (_config.alternator_https_port_proxy_protocol()) {
|
||||
alternator_https_port_proxy_protocol = _config.alternator_https_port_proxy_protocol();
|
||||
_listen_addresses.push_back({addr, *alternator_https_port_proxy_protocol});
|
||||
}
|
||||
if (_config.alternator_https_port()) {
|
||||
alternator_https_port = _config.alternator_https_port();
|
||||
_listen_addresses.push_back({addr, *alternator_https_port});
|
||||
creds.emplace();
|
||||
auto opts = _config.alternator_encryption_options();
|
||||
if (opts.empty()) {
|
||||
@@ -147,29 +131,18 @@ future<> controller::start_server() {
|
||||
}
|
||||
}
|
||||
_server.invoke_on_all(
|
||||
[this, addr, alternator_port, alternator_https_port, alternator_port_proxy_protocol, alternator_https_port_proxy_protocol, creds = std::move(creds)] (server& server) mutable {
|
||||
return server.init(addr, alternator_port, alternator_https_port, alternator_port_proxy_protocol, alternator_https_port_proxy_protocol, creds,
|
||||
[this, addr, alternator_port, alternator_https_port, creds = std::move(creds)] (server& server) mutable {
|
||||
return server.init(addr, alternator_port, alternator_https_port, creds,
|
||||
_config.alternator_enforce_authorization,
|
||||
_config.alternator_warn_authorization,
|
||||
_config.alternator_max_users_query_size_in_trace_output,
|
||||
&_memory_limiter.local().get_semaphore(),
|
||||
_config.max_concurrent_requests_per_shard);
|
||||
}).handle_exception([this, addr, alternator_port, alternator_https_port, alternator_port_proxy_protocol, alternator_https_port_proxy_protocol] (std::exception_ptr ep) {
|
||||
logger.error("Failed to set up Alternator HTTP server on {} port {}, TLS port {}, proxy-protocol port {}, TLS proxy-protocol port {}: {}",
|
||||
addr,
|
||||
alternator_port ? std::to_string(*alternator_port) : "OFF",
|
||||
alternator_https_port ? std::to_string(*alternator_https_port) : "OFF",
|
||||
alternator_port_proxy_protocol ? std::to_string(*alternator_port_proxy_protocol) : "OFF",
|
||||
alternator_https_port_proxy_protocol ? std::to_string(*alternator_https_port_proxy_protocol) : "OFF",
|
||||
ep);
|
||||
}).handle_exception([this, addr, alternator_port, alternator_https_port] (std::exception_ptr ep) {
|
||||
logger.error("Failed to set up Alternator HTTP server on {} port {}, TLS port {}: {}",
|
||||
addr, alternator_port ? std::to_string(*alternator_port) : "OFF", alternator_https_port ? std::to_string(*alternator_https_port) : "OFF", ep);
|
||||
return stop_server().then([ep = std::move(ep)] { return make_exception_future<>(ep); });
|
||||
}).then([addr, alternator_port, alternator_https_port, alternator_port_proxy_protocol, alternator_https_port_proxy_protocol] {
|
||||
logger.info("Alternator server listening on {}, HTTP port {}, HTTPS port {}, proxy-protocol port {}, TLS proxy-protocol port {}",
|
||||
addr,
|
||||
alternator_port ? std::to_string(*alternator_port) : "OFF",
|
||||
alternator_https_port ? std::to_string(*alternator_https_port) : "OFF",
|
||||
alternator_port_proxy_protocol ? std::to_string(*alternator_port_proxy_protocol) : "OFF",
|
||||
alternator_https_port_proxy_protocol ? std::to_string(*alternator_https_port_proxy_protocol) : "OFF");
|
||||
}).then([addr, alternator_port, alternator_https_port] {
|
||||
logger.info("Alternator server listening on {}, HTTP port {}, HTTPS port {}",
|
||||
addr, alternator_port ? std::to_string(*alternator_port) : "OFF", alternator_https_port ? std::to_string(*alternator_https_port) : "OFF");
|
||||
}).get();
|
||||
});
|
||||
}
|
||||
@@ -192,8 +165,4 @@ future<> controller::request_stop_server() {
|
||||
});
|
||||
}
|
||||
|
||||
future<utils::chunked_vector<foreign_ptr<std::unique_ptr<client_data>>>> controller::get_client_data() {
|
||||
return _server.local().get_client_data();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
#include <seastar/core/sharded.hh>
|
||||
#include <seastar/core/smp.hh>
|
||||
|
||||
#include "transport/protocol_server.hh"
|
||||
#include "protocol_server.hh"
|
||||
|
||||
namespace service {
|
||||
class storage_proxy;
|
||||
class storage_service;
|
||||
class migration_manager;
|
||||
class memory_limiter;
|
||||
}
|
||||
@@ -58,7 +57,6 @@ class server;
|
||||
class controller : public protocol_server {
|
||||
sharded<gms::gossiper>& _gossiper;
|
||||
sharded<service::storage_proxy>& _proxy;
|
||||
sharded<service::storage_service>& _ss;
|
||||
sharded<service::migration_manager>& _mm;
|
||||
sharded<db::system_distributed_keyspace>& _sys_dist_ks;
|
||||
sharded<cdc::generation_service>& _cdc_gen_svc;
|
||||
@@ -76,7 +74,6 @@ public:
|
||||
controller(
|
||||
sharded<gms::gossiper>& gossiper,
|
||||
sharded<service::storage_proxy>& proxy,
|
||||
sharded<service::storage_service>& ss,
|
||||
sharded<service::migration_manager>& mm,
|
||||
sharded<db::system_distributed_keyspace>& sys_dist_ks,
|
||||
sharded<cdc::generation_service>& cdc_gen_svc,
|
||||
@@ -93,10 +90,6 @@ public:
|
||||
virtual future<> start_server() override;
|
||||
virtual future<> stop_server() override;
|
||||
virtual future<> request_stop_server() override;
|
||||
// This virtual function is called (on each shard separately) when the
|
||||
// virtual table "system.clients" is read. It is expected to generate a
|
||||
// list of clients connected to this server (on this shard).
|
||||
virtual future<utils::chunked_vector<foreign_ptr<std::unique_ptr<client_data>>>> get_client_data() override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -88,15 +88,9 @@ public:
|
||||
static api_error table_not_found(std::string msg) {
|
||||
return api_error("TableNotFoundException", std::move(msg));
|
||||
}
|
||||
static api_error limit_exceeded(std::string msg) {
|
||||
return api_error("LimitExceededException", std::move(msg));
|
||||
}
|
||||
static api_error internal(std::string msg) {
|
||||
return api_error("InternalServerError", std::move(msg), http::reply::status_type::internal_server_error);
|
||||
}
|
||||
static api_error payload_too_large(std::string msg) {
|
||||
return api_error("PayloadTooLarge", std::move(msg), status_type::payload_too_large);
|
||||
}
|
||||
|
||||
// Provide the "std::exception" interface, to make it easier to print this
|
||||
// exception in log messages. Note that this function is *not* used to
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,20 +10,18 @@
|
||||
|
||||
#include <seastar/core/future.hh>
|
||||
#include "seastarx.hh"
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include <seastar/core/sharded.hh>
|
||||
#include <seastar/util/noncopyable_function.hh>
|
||||
|
||||
#include "service/migration_manager.hh"
|
||||
#include "service/client_state.hh"
|
||||
#include "service_permit.hh"
|
||||
#include "db/timeout_clock.hh"
|
||||
#include "db/config.hh"
|
||||
|
||||
#include "alternator/error.hh"
|
||||
#include "stats.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "utils/updateable_value.hh"
|
||||
#include "utils/simple_value_with_expiry.hh"
|
||||
|
||||
#include "tracing/trace_state.hh"
|
||||
|
||||
@@ -42,8 +40,6 @@ namespace cql3::selection {
|
||||
|
||||
namespace service {
|
||||
class storage_proxy;
|
||||
class cas_shard;
|
||||
class storage_service;
|
||||
}
|
||||
|
||||
namespace cdc {
|
||||
@@ -60,9 +56,34 @@ class schema_builder;
|
||||
|
||||
namespace alternator {
|
||||
|
||||
enum class table_status;
|
||||
class rmw_operation;
|
||||
class put_or_delete_item;
|
||||
|
||||
struct make_jsonable : public json::jsonable {
|
||||
rjson::value _value;
|
||||
public:
|
||||
explicit make_jsonable(rjson::value&& value);
|
||||
std::string to_json() const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Make return type for serializing the object "streamed",
|
||||
* i.e. direct to HTTP output stream. Note: only useful for
|
||||
* (very) large objects as there are overhead issues with this
|
||||
* as well, but for massive lists of return objects this can
|
||||
* help avoid large allocations/many re-allocs
|
||||
*/
|
||||
json::json_return_type make_streamed(rjson::value&&);
|
||||
|
||||
struct json_string : public json::jsonable {
|
||||
std::string _value;
|
||||
public:
|
||||
explicit json_string(std::string&& value);
|
||||
std::string to_json() const override;
|
||||
};
|
||||
|
||||
namespace parsed {
|
||||
class path;
|
||||
};
|
||||
|
||||
schema_ptr get_table(service::storage_proxy& proxy, const rjson::value& request);
|
||||
bool is_alternator_keyspace(const sstring& ks_name);
|
||||
@@ -134,62 +155,33 @@ using attrs_to_get_node = attribute_path_map_node<std::monostate>;
|
||||
// optional means we should get all attributes, not specific ones.
|
||||
using attrs_to_get = attribute_path_map<std::monostate>;
|
||||
|
||||
namespace parsed {
|
||||
class expression_cache;
|
||||
}
|
||||
|
||||
class executor : public peering_sharded_service<executor> {
|
||||
gms::gossiper& _gossiper;
|
||||
service::storage_service& _ss;
|
||||
service::storage_proxy& _proxy;
|
||||
service::migration_manager& _mm;
|
||||
db::system_distributed_keyspace& _sdks;
|
||||
cdc::metadata& _cdc_metadata;
|
||||
utils::updateable_value<bool> _enforce_authorization;
|
||||
utils::updateable_value<bool> _warn_authorization;
|
||||
// An smp_service_group to be used for limiting the concurrency when
|
||||
// forwarding Alternator request between shards - if necessary for LWT.
|
||||
smp_service_group _ssg;
|
||||
|
||||
std::unique_ptr<parsed::expression_cache> _parsed_expression_cache;
|
||||
|
||||
struct describe_table_info_manager;
|
||||
std::unique_ptr<describe_table_info_manager> _describe_table_info_manager;
|
||||
|
||||
future<> cache_newly_calculated_size_on_all_shards(schema_ptr schema, std::uint64_t size_in_bytes, std::chrono::nanoseconds ttl);
|
||||
future<> fill_table_size(rjson::value &table_description, schema_ptr schema, bool deleting);
|
||||
public:
|
||||
using client_state = service::client_state;
|
||||
// request_return_type is the return type of the executor methods, which
|
||||
// can be one of:
|
||||
// 1. A string, which is the response body for the request.
|
||||
// 2. A body_writer, an asynchronous function (returning future<>) that
|
||||
// takes an output_stream and writes the response body into it.
|
||||
// 3. An api_error, which is an error response that should be returned to
|
||||
// the client.
|
||||
// The body_writer is used for streaming responses, where the response body
|
||||
// is written in chunks to the output_stream. This allows for efficient
|
||||
// handling of large responses without needing to allocate a large buffer
|
||||
// in memory.
|
||||
using body_writer = noncopyable_function<future<>(output_stream<char>&&)>;
|
||||
using request_return_type = std::variant<std::string, body_writer, api_error>;
|
||||
using request_return_type = std::variant<json::json_return_type, api_error>;
|
||||
stats _stats;
|
||||
// The metric_groups object holds this stat object's metrics registered
|
||||
// as long as the stats object is alive.
|
||||
seastar::metrics::metric_groups _metrics;
|
||||
static constexpr auto ATTRS_COLUMN_NAME = ":attrs";
|
||||
static constexpr auto KEYSPACE_NAME_PREFIX = "alternator_";
|
||||
static constexpr std::string_view INTERNAL_TABLE_PREFIX = ".scylla.alternator.";
|
||||
|
||||
executor(gms::gossiper& gossiper,
|
||||
service::storage_proxy& proxy,
|
||||
service::storage_service& ss,
|
||||
service::migration_manager& mm,
|
||||
db::system_distributed_keyspace& sdks,
|
||||
cdc::metadata& cdc_metadata,
|
||||
smp_service_group ssg,
|
||||
utils::updateable_value<uint32_t> default_timeout_in_ms);
|
||||
~executor();
|
||||
|
||||
future<request_return_type> create_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> describe_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
@@ -217,35 +209,26 @@ public:
|
||||
future<request_return_type> describe_continuous_backups(client_state& client_state, service_permit permit, rjson::value request);
|
||||
|
||||
future<> start();
|
||||
future<> stop();
|
||||
future<> stop() {
|
||||
// disconnect from the value source, but keep the value unchanged.
|
||||
s_default_timeout_in_ms = utils::updateable_value<uint32_t>{s_default_timeout_in_ms()};
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
static sstring table_name(const schema&);
|
||||
static db::timeout_clock::time_point default_timeout();
|
||||
private:
|
||||
static thread_local utils::updateable_value<uint32_t> s_default_timeout_in_ms;
|
||||
public:
|
||||
static schema_ptr find_table(service::storage_proxy&, std::string_view table_name);
|
||||
static schema_ptr find_table(service::storage_proxy&, const rjson::value& request);
|
||||
|
||||
private:
|
||||
friend class rmw_operation;
|
||||
|
||||
static void describe_key_schema(rjson::value& parent, const schema&, std::unordered_map<std::string,std::string> * = nullptr, const std::map<sstring, sstring> *tags = nullptr);
|
||||
future<rjson::value> fill_table_description(schema_ptr schema, table_status tbl_status, service::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit);
|
||||
future<executor::request_return_type> create_table_on_shard0(service::client_state&& client_state, tracing::trace_state_ptr trace_state, rjson::value request, bool enforce_authorization, bool warn_authorization, const db::tablets_mode_t::mode tablets_mode);
|
||||
|
||||
future<> do_batch_write(
|
||||
std::vector<std::pair<schema_ptr, put_or_delete_item>> mutation_builders,
|
||||
service::client_state& client_state,
|
||||
tracing::trace_state_ptr trace_state,
|
||||
service_permit permit);
|
||||
|
||||
future<> cas_write(schema_ptr schema, service::cas_shard cas_shard, const dht::decorated_key& dk,
|
||||
const std::vector<put_or_delete_item>& mutation_builders, service::client_state& client_state,
|
||||
tracing::trace_state_ptr trace_state, service_permit permit);
|
||||
static void describe_key_schema(rjson::value& parent, const schema&, std::unordered_map<std::string,std::string> * = nullptr);
|
||||
|
||||
public:
|
||||
static void describe_key_schema(rjson::value& parent, const schema& schema, std::unordered_map<std::string,std::string>&, const std::map<sstring, sstring> *tags = nullptr);
|
||||
static void describe_key_schema(rjson::value& parent, const schema& schema, std::unordered_map<std::string,std::string>&);
|
||||
|
||||
static std::optional<rjson::value> describe_single_item(schema_ptr,
|
||||
const query::partition_slice&,
|
||||
@@ -254,15 +237,11 @@ public:
|
||||
const std::optional<attrs_to_get>&,
|
||||
uint64_t* = nullptr);
|
||||
|
||||
// Converts a multi-row selection result to JSON compatible with DynamoDB.
|
||||
// For each row, this method calls item_callback, which takes the size of
|
||||
// the item as the parameter.
|
||||
static future<std::vector<rjson::value>> describe_multi_item(schema_ptr schema,
|
||||
const query::partition_slice&& slice,
|
||||
shared_ptr<cql3::selection::selection> selection,
|
||||
foreign_ptr<lw_shared_ptr<query::result>> query_result,
|
||||
shared_ptr<const std::optional<attrs_to_get>> attrs_to_get,
|
||||
noncopyable_function<void(uint64_t)> item_callback = {});
|
||||
shared_ptr<const std::optional<attrs_to_get>> attrs_to_get);
|
||||
|
||||
static void describe_single_item(const cql3::selection::selection&,
|
||||
const std::vector<managed_bytes_opt>&,
|
||||
@@ -271,7 +250,7 @@ public:
|
||||
uint64_t* item_length_in_bytes = nullptr,
|
||||
bool = false);
|
||||
|
||||
static bool add_stream_options(const rjson::value& stream_spec, schema_builder&, service::storage_proxy& sp);
|
||||
static void add_stream_options(const rjson::value& stream_spec, schema_builder&, service::storage_proxy& sp);
|
||||
static void supplement_table_info(rjson::value& descr, const schema& schema, service::storage_proxy& sp);
|
||||
static void supplement_table_stream_info(rjson::value& descr, const schema& schema, const service::storage_proxy& sp);
|
||||
};
|
||||
@@ -290,15 +269,6 @@ bool is_big(const rjson::value& val, int big_size = 100'000);
|
||||
// Check CQL's Role-Based Access Control (RBAC) permission (MODIFY,
|
||||
// SELECT, DROP, etc.) on the given table. When permission is denied an
|
||||
// appropriate user-readable api_error::access_denied is thrown.
|
||||
future<> verify_permission(bool enforce_authorization, bool warn_authorization, const service::client_state&, const schema_ptr&, auth::permission, alternator::stats& stats);
|
||||
|
||||
/**
|
||||
* Make return type for serializing the object "streamed",
|
||||
* i.e. direct to HTTP output stream. Note: only useful for
|
||||
* (very) large objects as there are overhead issues with this
|
||||
* as well, but for massive lists of return objects this can
|
||||
* help avoid large allocations/many re-allocs
|
||||
*/
|
||||
executor::body_writer make_streamed(rjson::value&&);
|
||||
future<> verify_permission(bool enforce_authorization, const service::client_state&, const schema_ptr&, auth::permission);
|
||||
|
||||
}
|
||||
|
||||
@@ -165,9 +165,7 @@ static std::optional<std::string> resolve_path_component(const std::string& colu
|
||||
fmt::format("ExpressionAttributeNames missing entry '{}' required by expression", column_name));
|
||||
}
|
||||
used_attribute_names.emplace(column_name);
|
||||
auto result = std::string(rjson::to_string_view(*value));
|
||||
validate_attr_name_length("", result.size(), false, "ExpressionAttributeNames contains invalid value: ");
|
||||
return result;
|
||||
return std::string(rjson::to_string_view(*value));
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -739,26 +737,6 @@ rjson::value calculate_value(const parsed::set_rhs& rhs,
|
||||
return rjson::null_value();
|
||||
}
|
||||
|
||||
void validate_attr_name_length(std::string_view supplementary_context, size_t attr_name_length, bool is_key, std::string_view error_msg_prefix) {
|
||||
constexpr const size_t DYNAMODB_KEY_ATTR_NAME_SIZE_MAX = 255;
|
||||
constexpr const size_t DYNAMODB_NONKEY_ATTR_NAME_SIZE_MAX = 65535;
|
||||
|
||||
const size_t max_length = is_key ? DYNAMODB_KEY_ATTR_NAME_SIZE_MAX : DYNAMODB_NONKEY_ATTR_NAME_SIZE_MAX;
|
||||
if (attr_name_length > max_length) {
|
||||
std::string error_msg;
|
||||
if (!error_msg_prefix.empty()) {
|
||||
error_msg += error_msg_prefix;
|
||||
}
|
||||
if (!supplementary_context.empty()) {
|
||||
error_msg += "in ";
|
||||
error_msg += supplementary_context;
|
||||
error_msg += " - ";
|
||||
}
|
||||
error_msg += fmt::format("Attribute name is too large, must be less than {} bytes", std::to_string(max_length + 1));
|
||||
throw api_error::validation(error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace alternator
|
||||
|
||||
auto fmt::formatter<alternator::parsed::path>::format(const alternator::parsed::path& p, fmt::format_context& ctx) const
|
||||
|
||||
@@ -91,18 +91,6 @@ options {
|
||||
throw expressions_syntax_error(format("{} at char {}", err,
|
||||
ex->get_charPositionInLine()));
|
||||
}
|
||||
|
||||
// ANTLR3 tries to recover missing tokens - it tries to finish parsing
|
||||
// and create valid objects, as if the missing token was there.
|
||||
// But it has a bug and leaks these tokens.
|
||||
// We override offending method and handle abandoned pointers.
|
||||
std::vector<std::unique_ptr<TokenType>> _missing_tokens;
|
||||
TokenType* getMissingSymbol(IntStreamType* istream, ExceptionBaseType* e,
|
||||
ANTLR_UINT32 expectedTokenType, BitsetListType* follow) {
|
||||
auto token = BaseType::getMissingSymbol(istream, e, expectedTokenType, follow);
|
||||
_missing_tokens.emplace_back(token);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@lexer::context {
|
||||
void displayRecognitionError(ANTLR_UINT8** token_names, ExceptionBaseType* ex) {
|
||||
@@ -196,13 +184,7 @@ path_component: NAME | NAMEREF;
|
||||
path returns [parsed::path p]:
|
||||
root=path_component { $p.set_root($root.text); }
|
||||
( '.' name=path_component { $p.add_dot($name.text); }
|
||||
| '[' INTEGER ']' {
|
||||
try {
|
||||
$p.add_index(std::stoi($INTEGER.text));
|
||||
} catch(std::out_of_range&) {
|
||||
throw expressions_syntax_error("list index out of integer range");
|
||||
}
|
||||
}
|
||||
| '[' INTEGER ']' { $p.add_index(std::stoi($INTEGER.text)); }
|
||||
)*;
|
||||
|
||||
/* See comment above why the "depth" counter was needed here */
|
||||
@@ -248,7 +230,7 @@ update_expression_clause returns [parsed::update_expression e]:
|
||||
// Note the "EOF" token at the end of the update expression. We want to the
|
||||
// parser to match the entire string given to it - not just its beginning!
|
||||
update_expression returns [parsed::update_expression e]:
|
||||
(update_expression_clause { e.append($update_expression_clause.e); })+ EOF;
|
||||
(update_expression_clause { e.append($update_expression_clause.e); })* EOF;
|
||||
|
||||
projection_expression returns [std::vector<parsed::path> v]:
|
||||
p=path { $v.push_back(std::move($p.p)); }
|
||||
@@ -275,13 +257,6 @@ primitive_condition returns [parsed::primitive_condition c]:
|
||||
(',' v=value[0] { $c.add_value(std::move($v.v)); })*
|
||||
')'
|
||||
)?
|
||||
{
|
||||
// Post-parse check to reject non-function single values
|
||||
if ($c._op == parsed::primitive_condition::type::VALUE &&
|
||||
!$c._values.front().is_func()) {
|
||||
throw expressions_syntax_error("Single value must be a function");
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
// The following rules for parsing boolean expressions are verbose and
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
#include "expressions_types.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "utils/updateable_value.hh"
|
||||
#include "stats.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -28,26 +26,6 @@ public:
|
||||
using runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
namespace parsed {
|
||||
class expression_cache_impl;
|
||||
class expression_cache {
|
||||
std::unique_ptr<expression_cache_impl> _impl;
|
||||
public:
|
||||
struct config {
|
||||
utils::updateable_value<uint32_t> max_cache_entries;
|
||||
};
|
||||
expression_cache(config cfg, stats& stats);
|
||||
~expression_cache();
|
||||
// stop background tasks, if any
|
||||
future<> stop();
|
||||
|
||||
update_expression parse_update_expression(std::string_view query);
|
||||
std::vector<path> parse_projection_expression(std::string_view query);
|
||||
condition_expression parse_condition_expression(std::string_view query, const char* caller);
|
||||
};
|
||||
} // namespace parsed
|
||||
|
||||
// Preferably use parsed::expression_cache instance instead of this free functions.
|
||||
parsed::update_expression parse_update_expression(std::string_view query);
|
||||
std::vector<parsed::path> parse_projection_expression(std::string_view query);
|
||||
parsed::condition_expression parse_condition_expression(std::string_view query, const char* caller);
|
||||
@@ -113,7 +91,5 @@ rjson::value calculate_value(const parsed::value& v,
|
||||
rjson::value calculate_value(const parsed::set_rhs& rhs,
|
||||
const rjson::value* previous_item);
|
||||
|
||||
void validate_attr_name_length(std::string_view supplementary_context, size_t attr_name_length, bool is_key, std::string_view error_msg_prefix = {});
|
||||
|
||||
|
||||
} /* namespace alternator */
|
||||
|
||||
@@ -50,7 +50,7 @@ public:
|
||||
_operators.emplace_back(i);
|
||||
check_depth_limit();
|
||||
}
|
||||
void add_dot(std::string name) {
|
||||
void add_dot(std::string(name)) {
|
||||
_operators.emplace_back(std::move(name));
|
||||
check_depth_limit();
|
||||
}
|
||||
@@ -85,7 +85,7 @@ struct constant {
|
||||
}
|
||||
};
|
||||
|
||||
// "value" is a value used in the right hand side of an assignment
|
||||
// "value" is is a value used in the right hand side of an assignment
|
||||
// expression, "SET a = ...". It can be a constant (a reference to a value
|
||||
// included in the request, e.g., ":val"), a path to an attribute from the
|
||||
// existing item (e.g., "a.b[3].c"), or a function of other such values.
|
||||
@@ -205,11 +205,13 @@ public:
|
||||
// The supported primitive conditions are:
|
||||
// 1. Binary operators - v1 OP v2, where OP is =, <>, <, <=, >, or >= and
|
||||
// v1 and v2 are values - from the item (an attribute path), the query
|
||||
// (a ":val" reference), or a function of the above (only the size()
|
||||
// (a ":val" reference), or a function of the the above (only the size()
|
||||
// function is supported).
|
||||
// 2. Ternary operator - v1 BETWEEN v2 and v3 (means v1 >= v2 AND v1 <= v3).
|
||||
// 3. N-ary operator - v1 IN ( v2, v3, ... )
|
||||
// 4. A single function call (attribute_exists etc.).
|
||||
// 4. A single function call (attribute_exists etc.). The parser actually
|
||||
// accepts a more general "value" here but later stages reject a value
|
||||
// which is not a function call (because DynamoDB does it too).
|
||||
class primitive_condition {
|
||||
public:
|
||||
enum class type {
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024-present ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "utils/rjson.hh"
|
||||
#include "serialization.hh"
|
||||
#include "schema/column_computation.hh"
|
||||
#include "db/view/regular_column_transformation.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// An implementation of a "column_computation" which extracts a specific
|
||||
// non-key attribute from the big map (":attrs") of all non-key attributes,
|
||||
// and deserializes it if it has the desired type. GSI will use this computed
|
||||
// column as a materialized-view key when the view key attribute isn't a
|
||||
// full-fledged CQL column but rather stored in ":attrs".
|
||||
class extract_from_attrs_column_computation : public regular_column_transformation {
|
||||
// The name of the CQL column name holding the attribute map. It is a
|
||||
// constant defined in executor.cc (as ":attrs"), so doesn't need
|
||||
// to be specified when constructing the column computation.
|
||||
static const bytes MAP_NAME;
|
||||
// The top-level attribute name to extract from the ":attrs" map.
|
||||
bytes _attr_name;
|
||||
// The type we expect for the value stored in the attribute. If the type
|
||||
// matches the expected type, it is decoded from the serialized format
|
||||
// we store in the map's values) into the raw CQL type value that we use
|
||||
// for keys, and returned by compute_value(). Only the types "S" (string),
|
||||
// "B" (bytes) and "N" (number) are allowed as keys in DynamoDB, and
|
||||
// therefore in desired_type.
|
||||
alternator_type _desired_type;
|
||||
public:
|
||||
virtual column_computation_ptr clone() const override;
|
||||
// TYPE_NAME is a unique string that distinguishes this class from other
|
||||
// column_computation subclasses. column_computation::deserialize() will
|
||||
// construct an object of this subclass if it sees a "type" TYPE_NAME.
|
||||
static inline const std::string TYPE_NAME = "alternator_extract_from_attrs";
|
||||
// Serialize the *definition* of this column computation into a JSON
|
||||
// string with a unique "type" string - TYPE_NAME - which then causes
|
||||
// column_computation::deserialize() to create an object from this class.
|
||||
virtual bytes serialize() const override;
|
||||
// Construct this object based on the previous output of serialize().
|
||||
// Calls on_internal_error() if the string doesn't match the output format
|
||||
// of serialize(). "type" is not checked column_computation::deserialize()
|
||||
// won't call this constructor if "type" doesn't match.
|
||||
extract_from_attrs_column_computation(const rjson::value &v);
|
||||
extract_from_attrs_column_computation(bytes_view attr_name, alternator_type desired_type)
|
||||
: _attr_name(attr_name), _desired_type(desired_type)
|
||||
{}
|
||||
// Implement regular_column_transformation's compute_value() that
|
||||
// accepts the full row:
|
||||
result compute_value(const schema& schema, const partition_key& key,
|
||||
const db::view::clustering_or_static_row& row) const override;
|
||||
// But do not implement column_computation's compute_value() that
|
||||
// accepts only a partition key - that's not enough so our implementation
|
||||
// of this function does on_internal_error().
|
||||
bytes compute_value(const schema& schema, const partition_key& key) const override;
|
||||
// This computed column does depend on a non-primary key column, so
|
||||
// its result may change in the update and we need to compute it
|
||||
// before and after the update.
|
||||
virtual bool depends_on_non_primary_key_column() const override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} // namespace alternator
|
||||
@@ -1,301 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025-present ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
*/
|
||||
|
||||
#include "alternator/http_compression.hh"
|
||||
#include "alternator/server.hh"
|
||||
#include <seastar/coroutine/maybe_yield.hh>
|
||||
#include <zlib.h>
|
||||
|
||||
static logging::logger slogger("alternator-http-compression");
|
||||
|
||||
namespace alternator {
|
||||
|
||||
|
||||
static constexpr size_t compressed_buffer_size = 1024;
|
||||
class zlib_compressor {
|
||||
z_stream _zs;
|
||||
temporary_buffer<char> _output_buf;
|
||||
noncopyable_function<future<>(temporary_buffer<char>&&)> _write_func;
|
||||
public:
|
||||
zlib_compressor(bool gzip, int compression_level, noncopyable_function<future<>(temporary_buffer<char>&&)> write_func)
|
||||
: _write_func(std::move(write_func)) {
|
||||
memset(&_zs, 0, sizeof(_zs));
|
||||
if (deflateInit2(&_zs, std::clamp(compression_level, Z_NO_COMPRESSION, Z_BEST_COMPRESSION), Z_DEFLATED,
|
||||
(gzip ? 16 : 0) + MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
|
||||
// Should only happen if memory allocation fails
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
}
|
||||
~zlib_compressor() {
|
||||
deflateEnd(&_zs);
|
||||
}
|
||||
future<> close() {
|
||||
return compress(nullptr, 0, true);
|
||||
}
|
||||
|
||||
future<> compress(const char* buf, size_t len, bool is_last_chunk = false) {
|
||||
_zs.next_in = reinterpret_cast<unsigned char*>(const_cast<char*>(buf));
|
||||
_zs.avail_in = (uInt) len;
|
||||
int mode = is_last_chunk ? Z_FINISH : Z_NO_FLUSH;
|
||||
while(_zs.avail_in > 0 || is_last_chunk) {
|
||||
co_await coroutine::maybe_yield();
|
||||
if (_output_buf.empty()) {
|
||||
if (is_last_chunk) {
|
||||
uint32_t max_buffer_size = 0;
|
||||
deflatePending(&_zs, &max_buffer_size, nullptr);
|
||||
max_buffer_size += deflateBound(&_zs, _zs.avail_in) + 1;
|
||||
_output_buf = temporary_buffer<char>(std::min(compressed_buffer_size, (size_t) max_buffer_size));
|
||||
} else {
|
||||
_output_buf = temporary_buffer<char>(compressed_buffer_size);
|
||||
}
|
||||
_zs.next_out = reinterpret_cast<unsigned char*>(_output_buf.get_write());
|
||||
_zs.avail_out = compressed_buffer_size;
|
||||
}
|
||||
int e = deflate(&_zs, mode);
|
||||
if (e < Z_OK) {
|
||||
throw api_error::internal("Error during compression of response body");
|
||||
}
|
||||
if (e == Z_STREAM_END || _zs.avail_out < compressed_buffer_size / 4) {
|
||||
_output_buf.trim(compressed_buffer_size - _zs.avail_out);
|
||||
co_await _write_func(std::move(_output_buf));
|
||||
if (e == Z_STREAM_END) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Helper string_view functions for parsing Accept-Encoding header
|
||||
struct case_insensitive_cmp_sv {
|
||||
bool operator()(std::string_view s1, std::string_view s2) const {
|
||||
return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(),
|
||||
[](char a, char b) { return ::tolower(a) == ::tolower(b); });
|
||||
}
|
||||
};
|
||||
static inline std::string_view trim_left(std::string_view sv) {
|
||||
while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.front())))
|
||||
sv.remove_prefix(1);
|
||||
return sv;
|
||||
}
|
||||
static inline std::string_view trim_right(std::string_view sv) {
|
||||
while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.back())))
|
||||
sv.remove_suffix(1);
|
||||
return sv;
|
||||
}
|
||||
static inline std::string_view trim(std::string_view sv) {
|
||||
return trim_left(trim_right(sv));
|
||||
}
|
||||
|
||||
inline std::vector<std::string_view> split(std::string_view text, char separator) {
|
||||
std::vector<std::string_view> tokens;
|
||||
if (text == "") {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
auto pos = text.find_first_of(separator);
|
||||
if (pos != std::string_view::npos) {
|
||||
tokens.emplace_back(text.data(), pos);
|
||||
text.remove_prefix(pos + 1);
|
||||
} else {
|
||||
tokens.emplace_back(text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
constexpr response_compressor::compression_type response_compressor::get_compression_type(std::string_view encoding) {
|
||||
for (size_t i = 0; i < static_cast<size_t>(compression_type::count); ++i) {
|
||||
if (case_insensitive_cmp_sv{}(encoding, compression_names[i])) {
|
||||
return static_cast<compression_type>(i);
|
||||
}
|
||||
}
|
||||
return compression_type::unknown;
|
||||
}
|
||||
|
||||
response_compressor::compression_type response_compressor::find_compression(std::string_view accept_encoding, size_t response_size) {
|
||||
std::optional<float> ct_q[static_cast<size_t>(compression_type::count)];
|
||||
ct_q[static_cast<size_t>(compression_type::none)] = std::numeric_limits<float>::min(); // enabled, but lowest priority
|
||||
compression_type selected_ct = compression_type::none;
|
||||
|
||||
std::vector<std::string_view> entries = split(accept_encoding, ',');
|
||||
for (auto& e : entries) {
|
||||
std::vector<std::string_view> params = split(e, ';');
|
||||
if (params.size() == 0) {
|
||||
continue;
|
||||
}
|
||||
compression_type ct = get_compression_type(trim(params[0]));
|
||||
if (ct == compression_type::unknown) {
|
||||
continue; // ignore unknown encoding types
|
||||
}
|
||||
if (ct_q[static_cast<size_t>(ct)].has_value() && ct_q[static_cast<size_t>(ct)] != 0.0f) {
|
||||
continue; // already processed this encoding
|
||||
}
|
||||
if (response_size < _threshold[static_cast<size_t>(ct)]) {
|
||||
continue; // below threshold treat as unknown
|
||||
}
|
||||
for (size_t i = 1; i < params.size(); ++i) { // find "q=" parameter
|
||||
auto pos = params[i].find("q=");
|
||||
if (pos == std::string_view::npos) {
|
||||
continue;
|
||||
}
|
||||
std::string_view param = params[i].substr(pos + 2);
|
||||
param = trim(param);
|
||||
// parse quality value
|
||||
float q_value = 1.0f;
|
||||
auto [ptr, ec] = std::from_chars(param.data(), param.data() + param.size(), q_value);
|
||||
if (ec != std::errc() || ptr != param.data() + param.size()) {
|
||||
continue;
|
||||
}
|
||||
if (q_value < 0.0) {
|
||||
q_value = 0.0;
|
||||
} else if (q_value > 1.0) {
|
||||
q_value = 1.0;
|
||||
}
|
||||
ct_q[static_cast<size_t>(ct)] = q_value;
|
||||
break; // we parsed quality value
|
||||
}
|
||||
if (!ct_q[static_cast<size_t>(ct)].has_value()) {
|
||||
ct_q[static_cast<size_t>(ct)] = 1.0f; // default quality value
|
||||
}
|
||||
// keep the highest encoding (in the order, unless 'any')
|
||||
if (selected_ct == compression_type::any) {
|
||||
if (ct_q[static_cast<size_t>(ct)] >= ct_q[static_cast<size_t>(selected_ct)]) {
|
||||
selected_ct = ct;
|
||||
}
|
||||
} else {
|
||||
if (ct_q[static_cast<size_t>(ct)] > ct_q[static_cast<size_t>(selected_ct)]) {
|
||||
selected_ct = ct;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selected_ct == compression_type::any) {
|
||||
// select any not mentioned or highest quality
|
||||
selected_ct = compression_type::none;
|
||||
for (size_t i = 0; i < static_cast<size_t>(compression_type::compressions_count); ++i) {
|
||||
if (!ct_q[i].has_value()) {
|
||||
return static_cast<compression_type>(i);
|
||||
}
|
||||
if (ct_q[i] > ct_q[static_cast<size_t>(selected_ct)]) {
|
||||
selected_ct = static_cast<compression_type>(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected_ct;
|
||||
}
|
||||
|
||||
static future<chunked_content> compress(response_compressor::compression_type ct, const db::config& cfg, std::string str) {
|
||||
chunked_content compressed;
|
||||
auto write = [&compressed](temporary_buffer<char>&& buf) -> future<> {
|
||||
compressed.push_back(std::move(buf));
|
||||
return make_ready_future<>();
|
||||
};
|
||||
zlib_compressor compressor(ct != response_compressor::compression_type::deflate,
|
||||
cfg.alternator_response_gzip_compression_level(), std::move(write));
|
||||
co_await compressor.compress(str.data(), str.size(), true);
|
||||
co_return compressed;
|
||||
}
|
||||
|
||||
static sstring flatten(chunked_content&& cc) {
|
||||
size_t total_size = 0;
|
||||
for (const auto& chunk : cc) {
|
||||
total_size += chunk.size();
|
||||
}
|
||||
sstring result = sstring{ sstring::initialized_later{}, total_size };
|
||||
size_t offset = 0;
|
||||
for (const auto& chunk : cc) {
|
||||
std::copy(chunk.begin(), chunk.end(), result.begin() + offset);
|
||||
offset += chunk.size();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
future<std::unique_ptr<http::reply>> response_compressor::generate_reply(std::unique_ptr<http::reply> rep, sstring accept_encoding, const char* content_type, std::string&& response_body) {
|
||||
response_compressor::compression_type ct = find_compression(accept_encoding, response_body.size());
|
||||
if (ct != response_compressor::compression_type::none) {
|
||||
rep->add_header("Content-Encoding", get_encoding_name(ct));
|
||||
rep->set_content_type(content_type);
|
||||
return compress(ct, cfg, std::move(response_body)).then([rep = std::move(rep)] (chunked_content compressed) mutable {
|
||||
rep->_content = flatten(std::move(compressed));
|
||||
return make_ready_future<std::unique_ptr<http::reply>>(std::move(rep));
|
||||
});
|
||||
} else {
|
||||
// Note that despite the move, there is a copy here -
|
||||
// as str is std::string and rep->_content is sstring.
|
||||
rep->_content = std::move(response_body);
|
||||
rep->set_content_type(content_type);
|
||||
}
|
||||
return make_ready_future<std::unique_ptr<http::reply>>(std::move(rep));
|
||||
}
|
||||
|
||||
template<typename Compressor>
|
||||
class compressed_data_sink_impl : public data_sink_impl {
|
||||
output_stream<char> _out;
|
||||
Compressor _compressor;
|
||||
public:
|
||||
template<typename... Args>
|
||||
compressed_data_sink_impl(output_stream<char>&& out, Args&&... args)
|
||||
: _out(std::move(out)), _compressor(std::forward<Args>(args)..., [this](temporary_buffer<char>&& buf) {
|
||||
return _out.write(std::move(buf));
|
||||
}) { }
|
||||
|
||||
future<> put(std::span<temporary_buffer<char>> data) override {
|
||||
return data_sink_impl::fallback_put(data, [this] (temporary_buffer<char>&& buf) {
|
||||
return do_put(std::move(buf));
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
future<> do_put(temporary_buffer<char> buf) {
|
||||
co_return co_await _compressor.compress(buf.get(), buf.size());
|
||||
|
||||
}
|
||||
future<> close() override {
|
||||
return _compressor.close().then([this] {
|
||||
return _out.close();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
executor::body_writer compress(response_compressor::compression_type ct, const db::config& cfg, executor::body_writer&& bw) {
|
||||
return [bw = std::move(bw), ct, level = cfg.alternator_response_gzip_compression_level()](output_stream<char>&& out) mutable -> future<> {
|
||||
output_stream_options opts;
|
||||
opts.trim_to_size = true;
|
||||
std::unique_ptr<data_sink_impl> data_sink_impl;
|
||||
switch (ct) {
|
||||
case response_compressor::compression_type::gzip:
|
||||
data_sink_impl = std::make_unique<compressed_data_sink_impl<zlib_compressor>>(std::move(out), true, level);
|
||||
break;
|
||||
case response_compressor::compression_type::deflate:
|
||||
data_sink_impl = std::make_unique<compressed_data_sink_impl<zlib_compressor>>(std::move(out), false, level);
|
||||
break;
|
||||
case response_compressor::compression_type::none:
|
||||
case response_compressor::compression_type::any:
|
||||
case response_compressor::compression_type::unknown:
|
||||
on_internal_error(slogger,"Compression not selected");
|
||||
default:
|
||||
on_internal_error(slogger, "Unsupported compression type for data sink");
|
||||
}
|
||||
return bw(output_stream<char>(data_sink(std::move(data_sink_impl)), compressed_buffer_size, opts));
|
||||
};
|
||||
}
|
||||
|
||||
future<std::unique_ptr<http::reply>> response_compressor::generate_reply(std::unique_ptr<http::reply> rep, sstring accept_encoding, const char* content_type, executor::body_writer&& body_writer) {
|
||||
response_compressor::compression_type ct = find_compression(accept_encoding, std::numeric_limits<size_t>::max());
|
||||
if (ct != response_compressor::compression_type::none) {
|
||||
rep->add_header("Content-Encoding", get_encoding_name(ct));
|
||||
rep->write_body(content_type, compress(ct, cfg, std::move(body_writer)));
|
||||
} else {
|
||||
rep->write_body(content_type, std::move(body_writer));
|
||||
}
|
||||
return make_ready_future<std::unique_ptr<http::reply>>(std::move(rep));
|
||||
}
|
||||
|
||||
} // namespace alternator
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025-present ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "alternator/executor.hh"
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include "db/config.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class response_compressor {
|
||||
public:
|
||||
enum class compression_type {
|
||||
gzip,
|
||||
deflate,
|
||||
compressions_count,
|
||||
any = compressions_count,
|
||||
none,
|
||||
count,
|
||||
unknown = count
|
||||
};
|
||||
static constexpr std::string_view compression_names[] = {
|
||||
"gzip",
|
||||
"deflate",
|
||||
"*",
|
||||
"identity"
|
||||
};
|
||||
|
||||
static sstring get_encoding_name(compression_type ct) {
|
||||
return sstring(compression_names[static_cast<size_t>(ct)]);
|
||||
}
|
||||
static constexpr compression_type get_compression_type(std::string_view encoding);
|
||||
|
||||
sstring get_accepted_encoding(const http::request& req) {
|
||||
if (get_threshold() == 0) {
|
||||
return "";
|
||||
}
|
||||
return req.get_header("Accept-Encoding");
|
||||
}
|
||||
compression_type find_compression(std::string_view accept_encoding, size_t response_size);
|
||||
|
||||
response_compressor(const db::config& cfg)
|
||||
: cfg(cfg)
|
||||
,_gzip_level_observer(
|
||||
cfg.alternator_response_gzip_compression_level.observe([this](int v) {
|
||||
update_threshold();
|
||||
}))
|
||||
,_gzip_threshold_observer(
|
||||
cfg.alternator_response_compression_threshold_in_bytes.observe([this](uint32_t v) {
|
||||
update_threshold();
|
||||
}))
|
||||
{
|
||||
update_threshold();
|
||||
}
|
||||
response_compressor(const response_compressor& rhs) : response_compressor(rhs.cfg) {}
|
||||
|
||||
private:
|
||||
const db::config& cfg;
|
||||
utils::observable<int>::observer _gzip_level_observer;
|
||||
utils::observable<uint32_t>::observer _gzip_threshold_observer;
|
||||
uint32_t _threshold[static_cast<size_t>(compression_type::count)];
|
||||
|
||||
size_t get_threshold() { return _threshold[static_cast<size_t>(compression_type::any)]; }
|
||||
void update_threshold() {
|
||||
_threshold[static_cast<size_t>(compression_type::none)] = std::numeric_limits<uint32_t>::max();
|
||||
_threshold[static_cast<size_t>(compression_type::any)] = std::numeric_limits<uint32_t>::max();
|
||||
uint32_t gzip = cfg.alternator_response_gzip_compression_level() <= 0 ? std::numeric_limits<uint32_t>::max()
|
||||
: cfg.alternator_response_compression_threshold_in_bytes();
|
||||
_threshold[static_cast<size_t>(compression_type::gzip)] = gzip;
|
||||
_threshold[static_cast<size_t>(compression_type::deflate)] = gzip;
|
||||
for (size_t i = 0; i < static_cast<size_t>(compression_type::compressions_count); ++i) {
|
||||
if (_threshold[i] < _threshold[static_cast<size_t>(compression_type::any)]) {
|
||||
_threshold[static_cast<size_t>(compression_type::any)] = _threshold[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
future<std::unique_ptr<http::reply>> generate_reply(std::unique_ptr<http::reply> rep,
|
||||
sstring accept_encoding, const char* content_type, std::string&& response_body);
|
||||
future<std::unique_ptr<http::reply>> generate_reply(std::unique_ptr<http::reply> rep,
|
||||
sstring accept_encoding, const char* content_type, executor::body_writer&& body_writer);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025-present ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
*/
|
||||
|
||||
#include "expressions.hh"
|
||||
#include "utils/log.hh"
|
||||
|
||||
#include "utils/lru_string_map.hh"
|
||||
#include <variant>
|
||||
|
||||
static logging::logger logger_("parsed-expression-cache");
|
||||
|
||||
namespace alternator::parsed {
|
||||
|
||||
struct expression_cache_impl {
|
||||
stats& _stats;
|
||||
|
||||
using cached_expressions_types = std::variant<
|
||||
update_expression,
|
||||
condition_expression,
|
||||
std::vector<path>
|
||||
>;
|
||||
sized_lru_string_map<cached_expressions_types> _cached_entries;
|
||||
utils::observable<uint32_t>::observer _max_cache_entries_observer;
|
||||
|
||||
expression_cache_impl(expression_cache::config cfg, stats& stats);
|
||||
|
||||
// to define the specialized return type of `get_or_create()`
|
||||
template <typename Func, typename... Args>
|
||||
using ParseResult = std::invoke_result_t<Func, std::string_view, Args...>;
|
||||
|
||||
// Caching layer for parsed expressions
|
||||
// The expression type is determined by the type of the parsing function passed as a parameter,
|
||||
// and the return type is exactly the same as the return type of this parsing function.
|
||||
// StatsType is used only to update appropriate statistics - currently it is aligned with the expression type,
|
||||
// but it could be extended in the future if needed, e.g. split per operation.
|
||||
template <stats::expression_types StatsType, typename Func, typename... Args>
|
||||
ParseResult<Func, Args...> get_or_create(std::string_view query, Func&& parse_func, Args&&... other_args) {
|
||||
if (_cached_entries.disabled()) {
|
||||
return parse_func(query, std::forward<Args>(other_args)...);
|
||||
}
|
||||
if (!_cached_entries.sanity_check()) {
|
||||
_stats.expression_cache.requests[StatsType].misses++;
|
||||
return parse_func(query, std::forward<Args>(other_args)...);
|
||||
}
|
||||
auto value = _cached_entries.find(query);
|
||||
if (value) {
|
||||
logger_.trace("Cache hit for query: {}", query);
|
||||
_stats.expression_cache.requests[StatsType].hits++;
|
||||
try {
|
||||
return std::get<ParseResult<Func, Args...>>(value->get());
|
||||
} catch (const std::bad_variant_access&) {
|
||||
// User can reach this code, by sending the same query string as a different expression type.
|
||||
// In practice valid queries are different enough to not collide.
|
||||
// Entries in cache are only valid queries.
|
||||
// This request will fail at parsing below.
|
||||
// If, by any chance this is a valid query, it will be updated below with the new value.
|
||||
logger_.trace("Cache hit for '{}', but type mismatch.", query);
|
||||
_stats.expression_cache.requests[StatsType].hits--;
|
||||
}
|
||||
} else {
|
||||
logger_.trace("Cache miss for query: {}", query);
|
||||
}
|
||||
ParseResult<Func, Args...> expr = parse_func(query, std::forward<Args>(other_args)...);
|
||||
// Invalid query will throw here ^
|
||||
|
||||
_stats.expression_cache.requests[StatsType].misses++;
|
||||
if (value) [[unlikely]] {
|
||||
value->get() = cached_expressions_types{expr};
|
||||
} else {
|
||||
_cached_entries.insert(query, cached_expressions_types{expr});
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
};
|
||||
|
||||
expression_cache_impl::expression_cache_impl(expression_cache::config cfg, stats& stats) :
|
||||
_stats(stats), _cached_entries(logger_, _stats.expression_cache.evictions),
|
||||
_max_cache_entries_observer(cfg.max_cache_entries.observe([this] (uint32_t max_value) {
|
||||
_cached_entries.set_max_size(max_value);
|
||||
})) {
|
||||
_cached_entries.set_max_size(cfg.max_cache_entries());
|
||||
}
|
||||
|
||||
expression_cache::expression_cache(expression_cache::config cfg, stats& stats) :
|
||||
_impl(std::make_unique<expression_cache_impl>(std::move(cfg), stats)) {
|
||||
}
|
||||
expression_cache::~expression_cache() = default;
|
||||
future<> expression_cache::stop() {
|
||||
return _impl->_cached_entries.stop();
|
||||
}
|
||||
|
||||
update_expression expression_cache::parse_update_expression(std::string_view query) {
|
||||
return _impl->get_or_create<stats::expression_types::UPDATE_EXPRESSION>(query, alternator::parse_update_expression);
|
||||
}
|
||||
|
||||
std::vector<path> expression_cache::parse_projection_expression(std::string_view query) {
|
||||
return _impl->get_or_create<stats::expression_types::PROJECTION_EXPRESSION>(query, alternator::parse_projection_expression);
|
||||
}
|
||||
|
||||
condition_expression expression_cache::parse_condition_expression(std::string_view query, const char* caller) {
|
||||
return _impl->get_or_create<stats::expression_types::CONDITION_EXPRESSION>(query, alternator::parse_condition_expression, caller);
|
||||
}
|
||||
|
||||
} // namespace alternator::parsed
|
||||
@@ -8,16 +8,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cdc/cdc_options.hh"
|
||||
#include "cdc/log.hh"
|
||||
#include "seastarx.hh"
|
||||
#include "service/paxos/cas_request.hh"
|
||||
#include "service/cas_shard.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "consumed_capacity.hh"
|
||||
#include "executor.hh"
|
||||
#include "tracing/trace_state.hh"
|
||||
#include "keys/keys.hh"
|
||||
#include "keys.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -58,7 +55,7 @@ public:
|
||||
static write_isolation get_write_isolation_for_schema(schema_ptr schema);
|
||||
|
||||
static write_isolation default_write_isolation;
|
||||
|
||||
public:
|
||||
static void set_default_write_isolation(std::string_view mode);
|
||||
|
||||
protected:
|
||||
@@ -109,27 +106,21 @@ public:
|
||||
// violating this). We mark apply() "const" to let the compiler validate
|
||||
// this for us. The output-only field _return_attributes is marked
|
||||
// "mutable" above so that apply() can still write to it.
|
||||
virtual std::optional<mutation> apply(std::unique_ptr<rjson::value> previous_item, api::timestamp_type ts, cdc::per_request_options& cdc_opts) const = 0;
|
||||
virtual std::optional<mutation> apply(std::unique_ptr<rjson::value> previous_item, api::timestamp_type ts) const = 0;
|
||||
// Convert the above apply() into the signature needed by cas_request:
|
||||
virtual std::optional<mutation> apply(foreign_ptr<lw_shared_ptr<query::result>> qr, const query::partition_slice& slice, api::timestamp_type ts, cdc::per_request_options& cdc_opts) override;
|
||||
virtual std::optional<mutation> apply(foreign_ptr<lw_shared_ptr<query::result>> qr, const query::partition_slice& slice, api::timestamp_type ts) override;
|
||||
virtual ~rmw_operation() = default;
|
||||
const wcu_consumed_capacity_counter& consumed_capacity() const noexcept { return _consumed_capacity; }
|
||||
schema_ptr schema() const { return _schema; }
|
||||
const rjson::value& request() const { return _request; }
|
||||
rjson::value&& move_request() && { return std::move(_request); }
|
||||
future<executor::request_return_type> execute(service::storage_proxy& proxy,
|
||||
std::optional<service::cas_shard> cas_shard,
|
||||
service::client_state& client_state,
|
||||
tracing::trace_state_ptr trace_state,
|
||||
service_permit permit,
|
||||
bool needs_read_before_write,
|
||||
stats& global_stats,
|
||||
stats& per_table_stats,
|
||||
stats& stats,
|
||||
uint64_t& wcu_total);
|
||||
std::optional<service::cas_shard> shard_for_execute(bool needs_read_before_write);
|
||||
|
||||
private:
|
||||
inline bool should_fill_preimage() const { return _schema->cdc_options().enabled(); }
|
||||
std::optional<shard_id> shard_for_execute(bool needs_read_before_write);
|
||||
};
|
||||
|
||||
} // namespace alternator
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
#include "utils/log.hh"
|
||||
#include "serialization.hh"
|
||||
#include "error.hh"
|
||||
#include "types/concrete_types.hh"
|
||||
#include "types/json_utils.hh"
|
||||
#include "concrete_types.hh"
|
||||
#include "cql3/type_json.hh"
|
||||
#include "mutation/position_in_partition.hh"
|
||||
|
||||
static logging::logger slogger("alternator-serialization");
|
||||
@@ -245,27 +245,6 @@ rjson::value deserialize_item(bytes_view bv) {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
// This function takes a bytes_view created earlier by serialize_item(), and
|
||||
// if has the type "expected_type", the function returns the value as a
|
||||
// raw Scylla type. If the type doesn't match, returns an unset optional.
|
||||
// This function only supports the key types S (string), B (bytes) and N
|
||||
// (number) - serialize_item() serializes those types as a single-byte type
|
||||
// followed by the serialized raw Scylla type, so all this function needs to
|
||||
// do is to remove the first byte. This makes this function much more
|
||||
// efficient than deserialize_item() above because it avoids transformation
|
||||
// to/from JSON.
|
||||
std::optional<bytes> serialized_value_if_type(bytes_view bv, alternator_type expected_type) {
|
||||
if (bv.empty() || alternator_type(bv[0]) != expected_type) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// Currently, serialize_item() for types in alternator_type (notably S, B
|
||||
// and N) are nothing more than Scylla's raw format for these types
|
||||
// preceded by a type byte. So we just need to skip that byte and we are
|
||||
// left by exactly what we need to return.
|
||||
bv.remove_prefix(1);
|
||||
return bytes(bv);
|
||||
}
|
||||
|
||||
std::string type_to_string(data_type type) {
|
||||
static thread_local std::unordered_map<data_type, std::string> types = {
|
||||
{utf8_type, "S"},
|
||||
@@ -282,23 +261,15 @@ std::string type_to_string(data_type type) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::optional<bytes> try_get_key_column_value(const rjson::value& item, const column_definition& column) {
|
||||
bytes get_key_column_value(const rjson::value& item, const column_definition& column) {
|
||||
std::string column_name = column.name_as_text();
|
||||
const rjson::value* key_typed_value = rjson::find(item, column_name);
|
||||
if (!key_typed_value) {
|
||||
return std::nullopt;
|
||||
throw api_error::validation(fmt::format("Key column {} not found", column_name));
|
||||
}
|
||||
return get_key_from_typed_value(*key_typed_value, column);
|
||||
}
|
||||
|
||||
bytes get_key_column_value(const rjson::value& item, const column_definition& column) {
|
||||
auto value = try_get_key_column_value(item, column);
|
||||
if (!value) {
|
||||
throw api_error::validation(fmt::format("Key column {} not found", column.name_as_text()));
|
||||
}
|
||||
return std::move(*value);
|
||||
}
|
||||
|
||||
// Parses the JSON encoding for a key value, which is a map with a single
|
||||
// entry whose key is the type and the value is the encoded value.
|
||||
// If this type does not match the desired "type_str", an api_error::validation
|
||||
@@ -388,38 +359,20 @@ clustering_key ck_from_json(const rjson::value& item, schema_ptr schema) {
|
||||
return clustering_key::make_empty();
|
||||
}
|
||||
std::vector<bytes> raw_ck;
|
||||
// Note: it's possible to get more than one clustering column here, as
|
||||
// Alternator can be used to read scylla internal tables.
|
||||
// FIXME: this is a loop, but we really allow only one clustering key column.
|
||||
for (const column_definition& cdef : schema->clustering_key_columns()) {
|
||||
auto raw_value = get_key_column_value(item, cdef);
|
||||
bytes raw_value = get_key_column_value(item, cdef);
|
||||
raw_ck.push_back(std::move(raw_value));
|
||||
}
|
||||
|
||||
return clustering_key::from_exploded(raw_ck);
|
||||
}
|
||||
|
||||
clustering_key_prefix ck_prefix_from_json(const rjson::value& item, schema_ptr schema) {
|
||||
if (schema->clustering_key_size() == 0) {
|
||||
return clustering_key_prefix::make_empty();
|
||||
}
|
||||
std::vector<bytes> raw_ck;
|
||||
for (const column_definition& cdef : schema->clustering_key_columns()) {
|
||||
auto raw_value = try_get_key_column_value(item, cdef);
|
||||
if (!raw_value) {
|
||||
break;
|
||||
}
|
||||
raw_ck.push_back(std::move(*raw_value));
|
||||
}
|
||||
|
||||
return clustering_key_prefix::from_exploded(raw_ck);
|
||||
}
|
||||
|
||||
position_in_partition pos_from_json(const rjson::value& item, schema_ptr schema) {
|
||||
const bool is_alternator_ks = is_alternator_keyspace(schema->ks_name());
|
||||
if (is_alternator_ks) {
|
||||
return position_in_partition::for_key(ck_from_json(item, schema));
|
||||
auto ck = ck_from_json(item, schema);
|
||||
if (is_alternator_keyspace(schema->ks_name())) {
|
||||
return position_in_partition::for_key(std::move(ck));
|
||||
}
|
||||
|
||||
const auto region_item = rjson::find(item, scylla_paging_region);
|
||||
const auto weight_item = rjson::find(item, scylla_paging_weight);
|
||||
if (bool(region_item) != bool(weight_item)) {
|
||||
@@ -439,9 +392,8 @@ position_in_partition pos_from_json(const rjson::value& item, schema_ptr schema)
|
||||
} else {
|
||||
throw std::runtime_error(fmt::format("Invalid value for weight: {}", weight_view));
|
||||
}
|
||||
return position_in_partition(region, weight, region == partition_region::clustered ? std::optional(ck_prefix_from_json(item, schema)) : std::nullopt);
|
||||
return position_in_partition(region, weight, region == partition_region::clustered ? std::optional(std::move(ck)) : std::nullopt);
|
||||
}
|
||||
auto ck = ck_from_json(item, schema);
|
||||
if (ck.is_empty()) {
|
||||
return position_in_partition::for_partition_start();
|
||||
}
|
||||
@@ -496,7 +448,7 @@ const std::pair<std::string, const rjson::value*> unwrap_set(const rjson::value&
|
||||
return {"", nullptr};
|
||||
}
|
||||
auto it = v.MemberBegin();
|
||||
const std::string it_key = rjson::to_string(it->name);
|
||||
const std::string it_key = it->name.GetString();
|
||||
if (it_key != "SS" && it_key != "BS" && it_key != "NS") {
|
||||
return {std::move(it_key), nullptr};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include <optional>
|
||||
#include "types/types.hh"
|
||||
#include "schema/schema_fwd.hh"
|
||||
#include "keys/keys.hh"
|
||||
#include "keys.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "utils/big_decimal.hh"
|
||||
|
||||
@@ -43,7 +43,6 @@ type_representation represent_type(alternator_type atype);
|
||||
|
||||
bytes serialize_item(const rjson::value& item);
|
||||
rjson::value deserialize_item(bytes_view bv);
|
||||
std::optional<bytes> serialized_value_if_type(bytes_view bv, alternator_type expected_type);
|
||||
|
||||
std::string type_to_string(data_type type);
|
||||
|
||||
@@ -55,7 +54,7 @@ partition_key pk_from_json(const rjson::value& item, schema_ptr schema);
|
||||
clustering_key ck_from_json(const rjson::value& item, schema_ptr schema);
|
||||
position_in_partition pos_from_json(const rjson::value& item, schema_ptr schema);
|
||||
|
||||
// If v encodes a number (i.e., it is a {"N": [...]}), returns an object representing it. Otherwise,
|
||||
// If v encodes a number (i.e., it is a {"N": [...]}, returns an object representing it. Otherwise,
|
||||
// raises ValidationException with diagnostic.
|
||||
big_decimal unwrap_number(const rjson::value& v, std::string_view diagnostic);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include <seastar/http/function_handlers.hh>
|
||||
#include <seastar/http/short_streams.hh>
|
||||
#include <seastar/core/coroutine.hh>
|
||||
#include <seastar/coroutine/maybe_yield.hh>
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include <seastar/util/defer.hh>
|
||||
#include <seastar/util/short_streams.hh>
|
||||
#include "seastarx.hh"
|
||||
@@ -31,10 +31,6 @@
|
||||
#include "gms/gossiper.hh"
|
||||
#include "utils/overloaded_functor.hh"
|
||||
#include "utils/aws_sigv4.hh"
|
||||
#include "client_data.hh"
|
||||
#include "utils/updateable_value.hh"
|
||||
#include <zlib.h>
|
||||
#include "alternator/http_compression.hh"
|
||||
|
||||
static logging::logger slogger("alternator-server");
|
||||
|
||||
@@ -104,20 +100,10 @@ static void handle_CORS(const request& req, reply& rep, bool preflight) {
|
||||
// the user directly. Other exceptions are unexpected, and reported as
|
||||
// Internal Server Error.
|
||||
class api_handler : public handler_base {
|
||||
// Although the the DynamoDB API responses are JSON, additional
|
||||
// conventions apply to these responses. For this reason, DynamoDB uses
|
||||
// the content type "application/x-amz-json-1.0" instead of the standard
|
||||
// "application/json". Some other AWS services use later versions instead
|
||||
// of "1.0", but DynamoDB currently uses "1.0". Note that this content
|
||||
// type applies to all replies, both success and error.
|
||||
static constexpr const char* REPLY_CONTENT_TYPE = "application/x-amz-json-1.0";
|
||||
public:
|
||||
api_handler(const std::function<future<executor::request_return_type>(std::unique_ptr<request> req)>& _handle,
|
||||
const db::config& config) : _response_compressor(config), _f_handle(
|
||||
api_handler(const std::function<future<executor::request_return_type>(std::unique_ptr<request> req)>& _handle) : _f_handle(
|
||||
[this, _handle](std::unique_ptr<request> req, std::unique_ptr<reply> rep) {
|
||||
sstring accept_encoding = _response_compressor.get_accepted_encoding(*req);
|
||||
return seastar::futurize_invoke(_handle, std::move(req)).then_wrapped(
|
||||
[this, rep = std::move(rep), accept_encoding=std::move(accept_encoding)](future<executor::request_return_type> resf) mutable {
|
||||
return seastar::futurize_invoke(_handle, std::move(req)).then_wrapped([this, rep = std::move(rep)](future<executor::request_return_type> resf) mutable {
|
||||
if (resf.failed()) {
|
||||
// Exceptions of type api_error are wrapped as JSON and
|
||||
// returned to the client as expected. Other types of
|
||||
@@ -137,20 +123,25 @@ public:
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
auto res = resf.get();
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (std::string&& str) {
|
||||
return _response_compressor.generate_reply(std::move(rep), std::move(accept_encoding),
|
||||
REPLY_CONTENT_TYPE, std::move(str));
|
||||
},
|
||||
[&] (executor::body_writer&& body_writer) {
|
||||
return _response_compressor.generate_reply(std::move(rep), std::move(accept_encoding),
|
||||
REPLY_CONTENT_TYPE, std::move(body_writer));
|
||||
},
|
||||
[&] (const api_error& err) {
|
||||
generate_error_reply(*rep, err);
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
}, std::move(res));
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const json::json_return_type& json_return_value) {
|
||||
slogger.trace("api_handler success case");
|
||||
if (json_return_value._body_writer) {
|
||||
// Unfortunately, write_body() forces us to choose
|
||||
// from a fixed and irrelevant list of "mime-types"
|
||||
// at this point. But we'll override it with the
|
||||
// one (application/x-amz-json-1.0) below.
|
||||
rep->write_body("json", std::move(json_return_value._body_writer));
|
||||
} else {
|
||||
rep->_content += json_return_value._res;
|
||||
}
|
||||
},
|
||||
[&] (const api_error& err) {
|
||||
generate_error_reply(*rep, err);
|
||||
}
|
||||
}, res);
|
||||
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
});
|
||||
}) { }
|
||||
|
||||
@@ -160,6 +151,7 @@ public:
|
||||
handle_CORS(*req, *rep, false);
|
||||
return _f_handle(std::move(req), std::move(rep)).then(
|
||||
[](std::unique_ptr<reply> rep) {
|
||||
rep->set_mime_type("application/x-amz-json-1.0");
|
||||
rep->done();
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
});
|
||||
@@ -175,11 +167,9 @@ protected:
|
||||
rjson::add(results, "message", err._msg);
|
||||
rep._content = rjson::print(std::move(results));
|
||||
rep._status = err._http_code;
|
||||
rep.set_content_type(REPLY_CONTENT_TYPE);
|
||||
slogger.trace("api_handler error case: {}", rep._content);
|
||||
}
|
||||
|
||||
response_compressor _response_compressor;
|
||||
future_handler_function _f_handle;
|
||||
};
|
||||
|
||||
@@ -227,7 +217,7 @@ protected:
|
||||
// If the DC does not exist, we return an empty list - not an error.
|
||||
sstring query_dc = req->get_query_param("dc");
|
||||
sstring local_dc = query_dc.empty() ? topology.get_datacenter() : query_dc;
|
||||
std::unordered_set<locator::host_id> local_dc_nodes;
|
||||
std::unordered_set<gms::inet_address> local_dc_nodes;
|
||||
const auto& endpoints = topology.get_datacenter_endpoints();
|
||||
auto dc_it = endpoints.find(local_dc);
|
||||
if (dc_it != endpoints.end()) {
|
||||
@@ -237,9 +227,9 @@ protected:
|
||||
// DC, unless a single rack is selected by the "rack" query option.
|
||||
// If the rack does not exist, we return an empty list - not an error.
|
||||
sstring query_rack = req->get_query_param("rack");
|
||||
for (auto& id : local_dc_nodes) {
|
||||
for (auto& ip : local_dc_nodes) {
|
||||
if (!query_rack.empty()) {
|
||||
auto rack = _gossiper.get_application_state_value(id, gms::application_state::RACK);
|
||||
auto rack = _gossiper.get_application_state_value(ip, gms::application_state::RACK);
|
||||
if (rack != query_rack) {
|
||||
continue;
|
||||
}
|
||||
@@ -247,10 +237,10 @@ protected:
|
||||
// Note that it's not enough for the node to be is_alive() - a
|
||||
// node joining the cluster is also "alive" but not responsive to
|
||||
// requests. We alive *and* normal. See #19694, #21538.
|
||||
if (_gossiper.is_alive(id) && _gossiper.is_normal(id)) {
|
||||
if (_gossiper.is_alive(ip) && _gossiper.is_normal(ip)) {
|
||||
// Use the gossiped broadcast_rpc_address if available instead
|
||||
// of the internal IP address "ip". See discussion in #18711.
|
||||
rjson::push_back(results, rjson::from_string(_gossiper.get_rpc_address(id)));
|
||||
rjson::push_back(results, rjson::from_string(_gossiper.get_rpc_address(ip)));
|
||||
}
|
||||
}
|
||||
rep->set_status(reply::status_type::ok);
|
||||
@@ -276,57 +266,24 @@ protected:
|
||||
}
|
||||
};
|
||||
|
||||
// This function increments the authentication_failures counter, and may also
|
||||
// log a warn-level message and/or throw an exception, depending on what
|
||||
// enforce_authorization and warn_authorization are set to.
|
||||
// The username and client address are only used for logging purposes -
|
||||
// they are not included in the error message returned to the client, since
|
||||
// the client knows who it is.
|
||||
// Note that if enforce_authorization is false, this function will return
|
||||
// without throwing. So a caller that doesn't want to continue after an
|
||||
// authentication_error must explicitly return after calling this function.
|
||||
template<typename Exception>
|
||||
static void authentication_error(alternator::stats& stats, bool enforce_authorization, bool warn_authorization, Exception&& e, std::string_view user, gms::inet_address client_address) {
|
||||
stats.authentication_failures++;
|
||||
if (enforce_authorization) {
|
||||
if (warn_authorization) {
|
||||
slogger.warn("alternator_warn_authorization=true: {} for user {}, client address {}", e.what(), user, client_address);
|
||||
}
|
||||
throw std::move(e);
|
||||
} else {
|
||||
if (warn_authorization) {
|
||||
slogger.warn("If you set alternator_enforce_authorization=true the following will be enforced: {} for user {}, client address {}", e.what(), user, client_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
future<std::string> server::verify_signature(const request& req, const chunked_content& content) {
|
||||
if (!_enforce_authorization.get() && !_warn_authorization.get()) {
|
||||
if (!_enforce_authorization) {
|
||||
slogger.debug("Skipping authorization");
|
||||
return make_ready_future<std::string>();
|
||||
}
|
||||
auto host_it = req._headers.find("Host");
|
||||
if (host_it == req._headers.end()) {
|
||||
authentication_error(_executor._stats, _enforce_authorization.get(), _warn_authorization.get(),
|
||||
api_error::invalid_signature("Host header is mandatory for signature verification"),
|
||||
"", req.get_client_address());
|
||||
return make_ready_future<std::string>();
|
||||
throw api_error::invalid_signature("Host header is mandatory for signature verification");
|
||||
}
|
||||
auto authorization_it = req._headers.find("Authorization");
|
||||
if (authorization_it == req._headers.end()) {
|
||||
authentication_error(_executor._stats, _enforce_authorization.get(), _warn_authorization.get(),
|
||||
api_error::missing_authentication_token("Authorization header is mandatory for signature verification"),
|
||||
"", req.get_client_address());
|
||||
return make_ready_future<std::string>();
|
||||
throw api_error::missing_authentication_token("Authorization header is mandatory for signature verification");
|
||||
}
|
||||
std::string host = host_it->second;
|
||||
std::string_view authorization_header = authorization_it->second;
|
||||
auto pos = authorization_header.find_first_of(' ');
|
||||
if (pos == std::string_view::npos || authorization_header.substr(0, pos) != "AWS4-HMAC-SHA256") {
|
||||
authentication_error(_executor._stats, _enforce_authorization.get(), _warn_authorization.get(),
|
||||
api_error::invalid_signature(fmt::format("Authorization header must use AWS4-HMAC-SHA256 algorithm: {}", authorization_header)),
|
||||
"", req.get_client_address());
|
||||
return make_ready_future<std::string>();
|
||||
throw api_error::invalid_signature(fmt::format("Authorization header must use AWS4-HMAC-SHA256 algorithm: {}", authorization_header));
|
||||
}
|
||||
authorization_header.remove_prefix(pos+1);
|
||||
std::string credential;
|
||||
@@ -361,9 +318,7 @@ future<std::string> server::verify_signature(const request& req, const chunked_c
|
||||
|
||||
std::vector<std::string_view> credential_split = split(credential, '/');
|
||||
if (credential_split.size() != 5) {
|
||||
authentication_error(_executor._stats, _enforce_authorization.get(), _warn_authorization.get(),
|
||||
api_error::validation(fmt::format("Incorrect credential information format: {}", credential)), "", req.get_client_address());
|
||||
return make_ready_future<std::string>();
|
||||
throw api_error::validation(fmt::format("Incorrect credential information format: {}", credential));
|
||||
}
|
||||
std::string user(credential_split[0]);
|
||||
std::string datestamp(credential_split[1]);
|
||||
@@ -374,81 +329,39 @@ future<std::string> server::verify_signature(const request& req, const chunked_c
|
||||
for (const auto& header : signed_headers) {
|
||||
signed_headers_map.emplace(header, std::string_view());
|
||||
}
|
||||
std::vector<std::string> modified_values;
|
||||
for (auto& header : req._headers) {
|
||||
std::string header_str;
|
||||
header_str.resize(header.first.size());
|
||||
std::transform(header.first.begin(), header.first.end(), header_str.begin(), ::tolower);
|
||||
auto it = signed_headers_map.find(header_str);
|
||||
if (it != signed_headers_map.end()) {
|
||||
// replace multiple spaces in the header value header.second with
|
||||
// a single space, as required by AWS SigV4 header canonization.
|
||||
// If we modify the value, we need to save it in modified_values
|
||||
// to keep it alive.
|
||||
std::string value;
|
||||
value.reserve(header.second.size());
|
||||
bool prev_space = false;
|
||||
bool modified = false;
|
||||
for (char ch : header.second) {
|
||||
if (ch == ' ') {
|
||||
if (!prev_space) {
|
||||
value += ch;
|
||||
prev_space = true;
|
||||
} else {
|
||||
modified = true; // skip a space
|
||||
}
|
||||
} else {
|
||||
value += ch;
|
||||
prev_space = false;
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
modified_values.emplace_back(std::move(value));
|
||||
it->second = std::string_view(modified_values.back());
|
||||
} else {
|
||||
it->second = std::string_view(header.second);
|
||||
}
|
||||
it->second = std::string_view(header.second);
|
||||
}
|
||||
}
|
||||
|
||||
auto cache_getter = [&proxy = _proxy] (std::string username) {
|
||||
return get_key_from_roles(proxy, std::move(username));
|
||||
auto cache_getter = [&proxy = _proxy, &as = _auth_service] (std::string username) {
|
||||
return get_key_from_roles(proxy, as, std::move(username));
|
||||
};
|
||||
return _key_cache.get_ptr(user, cache_getter).then_wrapped([this, &req, &content,
|
||||
return _key_cache.get_ptr(user, cache_getter).then([this, &req, &content,
|
||||
user = std::move(user),
|
||||
host = std::move(host),
|
||||
datestamp = std::move(datestamp),
|
||||
signed_headers_str = std::move(signed_headers_str),
|
||||
signed_headers_map = std::move(signed_headers_map),
|
||||
modified_values = std::move(modified_values),
|
||||
region = std::move(region),
|
||||
service = std::move(service),
|
||||
user_signature = std::move(user_signature)] (future<key_cache::value_ptr> key_ptr_fut) {
|
||||
key_cache::value_ptr key_ptr(nullptr);
|
||||
try {
|
||||
key_ptr = key_ptr_fut.get();
|
||||
} catch (const api_error& e) {
|
||||
authentication_error(_executor._stats, _enforce_authorization.get(), _warn_authorization.get(),
|
||||
e, user, req.get_client_address());
|
||||
return std::string();
|
||||
}
|
||||
user_signature = std::move(user_signature)] (key_cache::value_ptr key_ptr) {
|
||||
std::string signature;
|
||||
try {
|
||||
signature = utils::aws::get_signature(user, *key_ptr, std::string_view(host), "/", req._method,
|
||||
datestamp, signed_headers_str, signed_headers_map, &content, region, service, "");
|
||||
} catch (const std::exception& e) {
|
||||
authentication_error(_executor._stats, _enforce_authorization.get(), _warn_authorization.get(),
|
||||
api_error::invalid_signature(fmt::format("invalid signature: {}", e.what())),
|
||||
user, req.get_client_address());
|
||||
return std::string();
|
||||
throw api_error::invalid_signature(e.what());
|
||||
}
|
||||
|
||||
if (signature != std::string_view(user_signature)) {
|
||||
_key_cache.remove(user);
|
||||
authentication_error(_executor._stats, _enforce_authorization.get(), _warn_authorization.get(),
|
||||
api_error::unrecognized_client("wrong signature"),
|
||||
user, req.get_client_address());
|
||||
return std::string();
|
||||
throw api_error::unrecognized_client("The security token included in the request is invalid.");
|
||||
}
|
||||
return user;
|
||||
});
|
||||
@@ -461,82 +374,35 @@ static tracing::trace_state_ptr create_tracing_session(tracing::tracing& tracing
|
||||
return tracing_instance.create_session(tracing::trace_type::QUERY, props);
|
||||
}
|
||||
|
||||
// A helper class to represent a potentially truncated view of a chunked_content.
|
||||
// If the content is short enough and single chunked, it just holds a view into the content.
|
||||
// Otherwise it will be copied into an internal buffer, possibly truncated (depending on maximum allowed size passed in),
|
||||
// and the view will point into that buffer.
|
||||
// `as_view()` method will return the view.
|
||||
// `take_as_sstring()` will either move out the internal buffer (if any), or create a new sstring from the view.
|
||||
// You should consider `as_view()` valid as long both the original chunked_content and the truncated_content object are alive.
|
||||
class truncated_content {
|
||||
std::string_view _view;
|
||||
sstring _content_maybe;
|
||||
|
||||
void copy_from_content(const chunked_content& content) {
|
||||
size_t offset = 0;
|
||||
for(auto &tmp : content) {
|
||||
size_t to_copy = std::min(tmp.size(), _content_maybe.size() - offset);
|
||||
std::copy(tmp.get(), tmp.get() + to_copy, _content_maybe.data() + offset);
|
||||
offset += to_copy;
|
||||
if (offset >= _content_maybe.size()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// truncated_content_view() prints a potentially long chunked_content for
|
||||
// debugging purposes. In the common case when the content is not excessively
|
||||
// long, it just returns a view into the given content, without any copying.
|
||||
// But when the content is very long, it is truncated after some arbitrary
|
||||
// max_len (or one chunk, whichever comes first), with "<truncated>" added at
|
||||
// the end. To do this modification to the string, we need to create a new
|
||||
// std::string, so the caller must pass us a reference to one, "buf", where
|
||||
// we can store the content. The returned view is only alive for as long this
|
||||
// buf is kept alive.
|
||||
static std::string_view truncated_content_view(const chunked_content& content, std::string& buf) {
|
||||
constexpr size_t max_len = 1024;
|
||||
if (content.empty()) {
|
||||
return std::string_view();
|
||||
} else if (content.size() == 1 && content.begin()->size() <= max_len) {
|
||||
return std::string_view(content.begin()->get(), content.begin()->size());
|
||||
} else {
|
||||
buf = std::string(content.begin()->get(), std::min(content.begin()->size(), max_len)) + "<truncated>";
|
||||
return std::string_view(buf);
|
||||
}
|
||||
public:
|
||||
truncated_content(const chunked_content& content, size_t max_len = std::numeric_limits<size_t>::max()) {
|
||||
if (content.empty()) return;
|
||||
if (content.size() == 1 && content.begin()->size() <= max_len) {
|
||||
_view = std::string_view(content.begin()->get(), content.begin()->size());
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr std::string_view truncated_text = "<truncated>";
|
||||
size_t content_size = 0;
|
||||
for(auto &tmp : content) {
|
||||
content_size += tmp.size();
|
||||
}
|
||||
if (content_size <= max_len) {
|
||||
_content_maybe = sstring{ sstring::initialized_later{}, content_size };
|
||||
copy_from_content(content);
|
||||
}
|
||||
else {
|
||||
_content_maybe = sstring{ sstring::initialized_later{}, max_len + truncated_text.size() };
|
||||
copy_from_content(content);
|
||||
std::copy(truncated_text.begin(), truncated_text.end(), _content_maybe.data() + _content_maybe.size() - truncated_text.size());
|
||||
}
|
||||
_view = std::string_view(_content_maybe);
|
||||
}
|
||||
|
||||
std::string_view as_view() const { return _view; }
|
||||
sstring take_as_sstring() && {
|
||||
if (_content_maybe.empty() && !_view.empty()) {
|
||||
return sstring{_view};
|
||||
}
|
||||
return std::move(_content_maybe);
|
||||
}
|
||||
};
|
||||
|
||||
// `truncated_content_view` will produce an object representing a view to a passed content
|
||||
// possibly truncated at some length. The value returned is used in two ways:
|
||||
// - to print it in logs (use `as_view()` method for this)
|
||||
// - to pass it to tracing object, where it will be stored and used later
|
||||
// (use `take_as_sstring()` method as this produces a copy in form of a sstring)
|
||||
// `truncated_content` delays constructing `sstring` object until it's actually needed.
|
||||
// `truncated_content` is valid as long as passed `content` is alive.
|
||||
// if the content is truncated, `<truncated>` will be appended at the maximum size limit
|
||||
// and total size will be `max_users_query_size_in_trace_output() + strlen("<truncated>")`.
|
||||
static truncated_content truncated_content_view(const chunked_content& content, size_t max_size) {
|
||||
return truncated_content{content, max_size};
|
||||
}
|
||||
|
||||
static tracing::trace_state_ptr maybe_trace_query(service::client_state& client_state, std::string_view username, std::string_view op, const chunked_content& query, size_t max_users_query_size_in_trace_output) {
|
||||
static tracing::trace_state_ptr maybe_trace_query(service::client_state& client_state, std::string_view username, std::string_view op, const chunked_content& query) {
|
||||
tracing::trace_state_ptr trace_state;
|
||||
tracing::tracing& tracing_instance = tracing::tracing::get_local_tracing_instance();
|
||||
if (tracing_instance.trace_next_query() || tracing_instance.slow_query_tracing_enabled()) {
|
||||
trace_state = create_tracing_session(tracing_instance);
|
||||
std::string buf;
|
||||
tracing::add_session_param(trace_state, "alternator_op", op);
|
||||
tracing::add_query(trace_state, truncated_content_view(query, max_users_query_size_in_trace_output).take_as_sstring());
|
||||
tracing::add_query(trace_state, truncated_content_view(query, buf));
|
||||
tracing::begin(trace_state, seastar::format("Alternator {}", op), client_state.get_client_address());
|
||||
if (!username.empty()) {
|
||||
tracing::set_username(trace_state, auth::authenticated_user(username));
|
||||
@@ -545,215 +411,30 @@ static tracing::trace_state_ptr maybe_trace_query(service::client_state& client_
|
||||
return trace_state;
|
||||
}
|
||||
|
||||
// This read_entire_stream() is similar to Seastar's read_entire_stream()
|
||||
// which reads the given content_stream until its end into non-contiguous
|
||||
// memory. The difference is that this implementation takes an extra length
|
||||
// limit, and throws an error if we read more than this limit.
|
||||
// This length-limited variant would not have been needed if Seastar's HTTP
|
||||
// server's set_content_length_limit() worked in every case, but unfortunately
|
||||
// it does not - it only works if the request has a Content-Length header (see
|
||||
// issue #8196). In contrast this function can limit the request's length no
|
||||
// matter how it's encoded. We need this limit to protect Alternator from
|
||||
// oversized requests that can deplete memory.
|
||||
static future<chunked_content>
|
||||
read_entire_stream(input_stream<char>& inp, size_t length_limit) {
|
||||
chunked_content ret;
|
||||
// We try to read length_limit + 1 bytes, so that we can throw an
|
||||
// exception if we managed to read more than length_limit.
|
||||
ssize_t remain = length_limit + 1;
|
||||
do {
|
||||
temporary_buffer<char> buf = co_await inp.read_up_to(remain);
|
||||
if (buf.empty()) {
|
||||
break;
|
||||
}
|
||||
remain -= buf.size();
|
||||
ret.push_back(std::move(buf));
|
||||
} while (remain > 0);
|
||||
// If we read the full length_limit + 1 bytes, we went over the limit:
|
||||
if (remain <= 0) {
|
||||
// By throwing here an error, we may send a reply (the error message)
|
||||
// without having read the full request body. Seastar's httpd will
|
||||
// realize that we have not read the entire content stream, and
|
||||
// correctly mark the connection unreusable, i.e., close it.
|
||||
// This means we are currently exposed to issue #12166 caused by
|
||||
// Seastar issue 1325), where the client may get an RST instead of
|
||||
// a FIN, and may rarely get a "Connection reset by peer" before
|
||||
// reading the error we send.
|
||||
throw api_error::payload_too_large(fmt::format("Request content length limit of {} bytes exceeded", length_limit));
|
||||
}
|
||||
co_return ret;
|
||||
}
|
||||
|
||||
// safe_gzip_stream is an exception-safe wrapper for zlib's z_stream.
|
||||
// The "z_stream" struct is used by zlib to hold state while decompressing a
|
||||
// stream of data. It allocates memory which must be freed with inflateEnd(),
|
||||
// which the destructor of this class does.
|
||||
class safe_gzip_zstream {
|
||||
z_stream _zs;
|
||||
public:
|
||||
// If gzip is true, decode a gzip header (for "Content-Encoding: gzip").
|
||||
// Otherwise, a zlib header (for "Content-Encoding: deflate").
|
||||
safe_gzip_zstream(bool gzip = true) {
|
||||
memset(&_zs, 0, sizeof(_zs));
|
||||
if (inflateInit2(&_zs, gzip ? 16 + MAX_WBITS : MAX_WBITS) != Z_OK) {
|
||||
// Should only happen if memory allocation fails
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
}
|
||||
~safe_gzip_zstream() {
|
||||
inflateEnd(&_zs);
|
||||
}
|
||||
z_stream* operator->() {
|
||||
return &_zs;
|
||||
}
|
||||
z_stream* get() {
|
||||
return &_zs;
|
||||
}
|
||||
void reset() {
|
||||
inflateReset(&_zs);
|
||||
}
|
||||
};
|
||||
|
||||
// ungzip() takes a chunked_content of a compressed request body, and returns
|
||||
// the uncompressed content as a chunked_content. If gzip is true, we expect
|
||||
// gzip header (for "Content-Encoding: gzip"), if gzip is false, we expect a
|
||||
// zlib header (for "Content-Encoding: deflate").
|
||||
// If the uncompressed content exceeds length_limit, an error is thrown.
|
||||
static future<chunked_content>
|
||||
ungzip(chunked_content&& compressed_body, size_t length_limit, bool gzip = true) {
|
||||
chunked_content ret;
|
||||
// output_buf can be any size - when uncompressing input_buf, it doesn't
|
||||
// need to fit in a single output_buf, we'll use multiple output_buf for
|
||||
// a single input_buf if needed.
|
||||
constexpr size_t OUTPUT_BUF_SIZE = 4096;
|
||||
temporary_buffer<char> output_buf;
|
||||
safe_gzip_zstream strm(gzip);
|
||||
bool complete_stream = false; // empty input is not a valid gzip/deflate
|
||||
size_t total_out_bytes = 0;
|
||||
for (const temporary_buffer<char>& input_buf : compressed_body) {
|
||||
if (input_buf.empty()) {
|
||||
continue;
|
||||
}
|
||||
complete_stream = false;
|
||||
strm->next_in = (Bytef*) input_buf.get();
|
||||
strm->avail_in = (uInt) input_buf.size();
|
||||
do {
|
||||
co_await coroutine::maybe_yield();
|
||||
if (output_buf.empty()) {
|
||||
output_buf = temporary_buffer<char>(OUTPUT_BUF_SIZE);
|
||||
}
|
||||
strm->next_out = (Bytef*) output_buf.get();
|
||||
strm->avail_out = OUTPUT_BUF_SIZE;
|
||||
int e = inflate(strm.get(), Z_NO_FLUSH);
|
||||
size_t out_bytes = OUTPUT_BUF_SIZE - strm->avail_out;
|
||||
if (out_bytes > 0) {
|
||||
// If output_buf is nearly full, we save it as-is in ret. But
|
||||
// if it only has little data, better copy to a small buffer.
|
||||
if (out_bytes > OUTPUT_BUF_SIZE/2) {
|
||||
ret.push_back(std::move(output_buf).prefix(out_bytes));
|
||||
// output_buf is now empty. if this loop finds more input,
|
||||
// we'll allocate a new output buffer.
|
||||
} else {
|
||||
ret.push_back(temporary_buffer<char>(output_buf.get(), out_bytes));
|
||||
}
|
||||
total_out_bytes += out_bytes;
|
||||
if (total_out_bytes > length_limit) {
|
||||
throw api_error::payload_too_large(fmt::format("Request content length limit of {} bytes exceeded", length_limit));
|
||||
}
|
||||
}
|
||||
if (e == Z_STREAM_END) {
|
||||
// There may be more input after the first gzip stream - in
|
||||
// either this input_buf or the next one. The additional input
|
||||
// should be a second concatenated gzip. We need to allow that
|
||||
// by resetting the gzip stream and continuing the input loop
|
||||
// until there's no more input.
|
||||
strm.reset();
|
||||
if (strm->avail_in == 0) {
|
||||
complete_stream = true;
|
||||
break;
|
||||
}
|
||||
} else if (e != Z_OK && e != Z_BUF_ERROR) {
|
||||
// DynamoDB returns an InternalServerError when given a bad
|
||||
// gzip request body. See test test_broken_gzip_content
|
||||
throw api_error::internal("Error during gzip decompression of request body");
|
||||
}
|
||||
} while (strm->avail_in > 0 || strm->avail_out == 0);
|
||||
}
|
||||
if (!complete_stream) {
|
||||
// The gzip stream was not properly finished with Z_STREAM_END
|
||||
throw api_error::internal("Truncated gzip in request body");
|
||||
}
|
||||
co_return ret;
|
||||
}
|
||||
|
||||
future<executor::request_return_type> server::handle_api_request(std::unique_ptr<request> req) {
|
||||
_executor._stats.total_operations++;
|
||||
sstring target = req->get_header("X-Amz-Target");
|
||||
// target is DynamoDB API version followed by a dot '.' and operation type (e.g. CreateTable)
|
||||
auto dot = target.find('.');
|
||||
std::string_view op = (dot == sstring::npos) ? std::string_view() : std::string_view(target).substr(dot+1);
|
||||
if (req->content_length > request_content_length_limit) {
|
||||
// If we have a Content-Length header and know the request will be too
|
||||
// long, we don't need to wait for read_entire_stream() below to
|
||||
// discover it. And we definitely mustn't try to get_units() below for
|
||||
// for such a size.
|
||||
co_return api_error::payload_too_large(fmt::format("Request content length limit of {} bytes exceeded", request_content_length_limit));
|
||||
}
|
||||
// JSON parsing can allocate up to roughly 2x the size of the raw
|
||||
// document, + a couple of bytes for maintenance.
|
||||
// If the Content-Length of the request is not available, we assume
|
||||
// the largest possible request (request_content_length_limit, i.e., 16 MB)
|
||||
// and after reading the request we return_units() the excess.
|
||||
size_t mem_estimate = (req->content_length ? req->content_length : request_content_length_limit) * 2 + 8000;
|
||||
// TODO: consider the case where req->content_length is missing. Maybe
|
||||
// we need to take the content_length_limit and return some of the units
|
||||
// when we finish read_content_and_verify_signature?
|
||||
size_t mem_estimate = req->content_length * 2 + 8000;
|
||||
auto units_fut = get_units(*_memory_limiter, mem_estimate);
|
||||
if (_memory_limiter->waiters()) {
|
||||
++_executor._stats.requests_blocked_memory;
|
||||
}
|
||||
auto units = co_await std::move(units_fut);
|
||||
throwing_assert(req->content_stream);
|
||||
chunked_content content = co_await read_entire_stream(*req->content_stream, request_content_length_limit);
|
||||
// If the request had no Content-Length, we reserved too many units
|
||||
// so need to return some
|
||||
if (req->content_length == 0) {
|
||||
size_t content_length = 0;
|
||||
for (const auto& chunk : content) {
|
||||
content_length += chunk.size();
|
||||
}
|
||||
size_t new_mem_estimate = content_length * 2 + 8000;
|
||||
units.return_units(mem_estimate - new_mem_estimate);
|
||||
}
|
||||
SCYLLA_ASSERT(req->content_stream);
|
||||
chunked_content content = co_await util::read_entire_stream(*req->content_stream);
|
||||
auto username = co_await verify_signature(*req, content);
|
||||
// If the request is compressed, uncompress it now, after we checked
|
||||
// the signature (the signature is computed on the compressed content).
|
||||
// We apply the request_content_length_limit again to the uncompressed
|
||||
// content - we don't want to allow a tiny compressed request to
|
||||
// expand to a huge uncompressed request.
|
||||
sstring content_encoding = req->get_header("Content-Encoding");
|
||||
if (content_encoding == "gzip") {
|
||||
content = co_await ungzip(std::move(content), request_content_length_limit);
|
||||
} else if (content_encoding == "deflate") {
|
||||
content = co_await ungzip(std::move(content), request_content_length_limit, false);
|
||||
} else if (!content_encoding.empty()) {
|
||||
// DynamoDB returns a 500 error for unsupported Content-Encoding.
|
||||
// I'm not sure if this is the best error code, but let's do it too.
|
||||
// See the test test_garbage_content_encoding confirming this case.
|
||||
co_return api_error::internal("Unsupported Content-Encoding");
|
||||
}
|
||||
|
||||
// As long as the system_clients_entry object is alive, this request will
|
||||
// be visible in the "system.clients" virtual table. When requested, this
|
||||
// entry will be formatted by server::ongoing_request::make_client_data().
|
||||
auto user_agent_header = co_await _connection_options_keys_and_values.get_or_load(req->get_header("User-Agent"), [] (const client_options_cache_key_type&) {
|
||||
return make_ready_future<options_cache_value_type>(options_cache_value_type{});
|
||||
});
|
||||
|
||||
auto system_clients_entry = _ongoing_requests.emplace(
|
||||
req->get_client_address(), std::move(user_agent_header),
|
||||
username, current_scheduling_group(),
|
||||
req->get_protocol_name() == "https");
|
||||
|
||||
if (slogger.is_enabled(log_level::trace)) {
|
||||
slogger.trace("Request: {} {} {}", op, truncated_content_view(content, _max_users_query_size_in_trace_output).as_view(), req->_headers);
|
||||
std::string buf;
|
||||
slogger.trace("Request: {} {} {}", op, truncated_content_view(content, buf), req->_headers);
|
||||
}
|
||||
auto callback_it = _callbacks.find(op);
|
||||
if (callback_it == _callbacks.end()) {
|
||||
@@ -771,29 +452,19 @@ future<executor::request_return_type> server::handle_api_request(std::unique_ptr
|
||||
if (!username.empty()) {
|
||||
client_state.set_login(auth::authenticated_user(username));
|
||||
}
|
||||
client_state.maybe_update_per_service_level_params();
|
||||
co_await client_state.maybe_update_per_service_level_params();
|
||||
|
||||
tracing::trace_state_ptr trace_state = maybe_trace_query(client_state, username, op, content, _max_users_query_size_in_trace_output.get());
|
||||
tracing::trace_state_ptr trace_state = maybe_trace_query(client_state, username, op, content);
|
||||
tracing::trace(trace_state, "{}", op);
|
||||
|
||||
auto user = client_state.user();
|
||||
auto f = [this, content = std::move(content), &callback = callback_it->second,
|
||||
client_state = std::move(client_state), trace_state = std::move(trace_state),
|
||||
units = std::move(units), req = std::move(req)] () mutable -> future<executor::request_return_type> {
|
||||
rjson::value json_request = co_await _json_parser.parse(std::move(content));
|
||||
if (!json_request.IsObject()) {
|
||||
co_return api_error::validation("Request content must be an object");
|
||||
}
|
||||
co_return co_await callback(_executor, client_state, trace_state,
|
||||
make_service_permit(std::move(units)), std::move(json_request), std::move(req));
|
||||
};
|
||||
co_return co_await _sl_controller.with_user_service_level(user, std::ref(f));
|
||||
rjson::value json_request = co_await _json_parser.parse(std::move(content));
|
||||
co_return co_await callback_it->second(_executor, client_state, trace_state,
|
||||
make_service_permit(std::move(units)), std::move(json_request), std::move(req));
|
||||
}
|
||||
|
||||
void server::set_routes(routes& r) {
|
||||
api_handler* req_handler = new api_handler([this] (std::unique_ptr<request> req) mutable {
|
||||
return handle_api_request(std::move(req));
|
||||
}, _proxy.data_dictionary().get_config());
|
||||
});
|
||||
|
||||
r.put(operation_type::POST, "/", req_handler);
|
||||
r.put(operation_type::GET, "/", new health_handler(_pending_requests));
|
||||
@@ -824,9 +495,9 @@ server::server(executor& exec, service::storage_proxy& proxy, gms::gossiper& gos
|
||||
, _auth_service(auth_service)
|
||||
, _sl_controller(sl_controller)
|
||||
, _key_cache(1024, 1min, slogger)
|
||||
, _max_users_query_size_in_trace_output(1024)
|
||||
, _enforce_authorization(false)
|
||||
, _enabled_servers{}
|
||||
, _pending_requests("alternator::server::pending_requests")
|
||||
, _pending_requests{}
|
||||
, _timeout_config(_proxy.data_dictionary().get_config())
|
||||
, _callbacks{
|
||||
{"CreateTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
@@ -904,66 +575,37 @@ server::server(executor& exec, service::storage_proxy& proxy, gms::gossiper& gos
|
||||
} {
|
||||
}
|
||||
|
||||
future<> server::init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port,
|
||||
std::optional<uint16_t> port_proxy_protocol, std::optional<uint16_t> https_port_proxy_protocol,
|
||||
std::optional<tls::credentials_builder> creds,
|
||||
utils::updateable_value<bool> enforce_authorization, utils::updateable_value<bool> warn_authorization, utils::updateable_value<uint64_t> max_users_query_size_in_trace_output,
|
||||
semaphore* memory_limiter, utils::updateable_value<uint32_t> max_concurrent_requests) {
|
||||
future<> server::init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port, std::optional<tls::credentials_builder> creds,
|
||||
utils::updateable_value<bool> enforce_authorization, semaphore* memory_limiter, utils::updateable_value<uint32_t> max_concurrent_requests) {
|
||||
_memory_limiter = memory_limiter;
|
||||
_enforce_authorization = std::move(enforce_authorization);
|
||||
_warn_authorization = std::move(warn_authorization);
|
||||
_max_concurrent_requests = std::move(max_concurrent_requests);
|
||||
_max_users_query_size_in_trace_output = std::move(max_users_query_size_in_trace_output);
|
||||
if (!port && !https_port && !port_proxy_protocol && !https_port_proxy_protocol) {
|
||||
if (!port && !https_port) {
|
||||
return make_exception_future<>(std::runtime_error("Either regular port or TLS port"
|
||||
" must be specified in order to init an alternator HTTP server instance"));
|
||||
}
|
||||
return seastar::async([this, addr, port, https_port, port_proxy_protocol, https_port_proxy_protocol, creds] {
|
||||
return seastar::async([this, addr, port, https_port, creds] {
|
||||
_executor.start().get();
|
||||
|
||||
if (port || port_proxy_protocol) {
|
||||
if (port) {
|
||||
set_routes(_http_server._routes);
|
||||
_http_server.set_content_length_limit(server::content_length_limit);
|
||||
_http_server.set_content_streaming(true);
|
||||
if (port) {
|
||||
_http_server.listen(socket_address{addr, *port}).get();
|
||||
}
|
||||
if (port_proxy_protocol) {
|
||||
listen_options lo;
|
||||
lo.reuse_address = true;
|
||||
lo.proxy_protocol = true;
|
||||
_http_server.listen(socket_address{addr, *port_proxy_protocol}, lo).get();
|
||||
}
|
||||
_http_server.listen(socket_address{addr, *port}).get();
|
||||
_enabled_servers.push_back(std::ref(_http_server));
|
||||
}
|
||||
if (https_port || https_port_proxy_protocol) {
|
||||
if (https_port) {
|
||||
set_routes(_https_server._routes);
|
||||
_https_server.set_content_length_limit(server::content_length_limit);
|
||||
_https_server.set_content_streaming(true);
|
||||
|
||||
if (this_shard_id() == 0) {
|
||||
_credentials = creds->build_reloadable_server_credentials([this](const tls::credentials_builder& b, const std::unordered_set<sstring>& files, std::exception_ptr ep) -> future<> {
|
||||
if (ep) {
|
||||
slogger.warn("Exception loading {}: {}", files, ep);
|
||||
} else {
|
||||
co_await container().invoke_on_others([&b](server& s) {
|
||||
if (s._credentials) {
|
||||
b.rebuild(*s._credentials);
|
||||
}
|
||||
});
|
||||
slogger.info("Reloaded {}", files);
|
||||
}
|
||||
}).get();
|
||||
} else {
|
||||
_credentials = creds->build_server_credentials();
|
||||
}
|
||||
if (https_port) {
|
||||
_https_server.listen(socket_address{addr, *https_port}, _credentials).get();
|
||||
}
|
||||
if (https_port_proxy_protocol) {
|
||||
listen_options lo;
|
||||
lo.reuse_address = true;
|
||||
lo.proxy_protocol = true;
|
||||
_https_server.listen(socket_address{addr, *https_port_proxy_protocol}, lo, _credentials).get();
|
||||
}
|
||||
auto server_creds = creds->build_reloadable_server_credentials([](const std::unordered_set<sstring>& files, std::exception_ptr ep) {
|
||||
if (ep) {
|
||||
slogger.warn("Exception loading {}: {}", files, ep);
|
||||
} else {
|
||||
slogger.info("Reloaded {}", files);
|
||||
}
|
||||
}).get();
|
||||
_https_server.listen(socket_address{addr, *https_port}, std::move(server_creds)).get();
|
||||
_enabled_servers.push_back(std::ref(_https_server));
|
||||
}
|
||||
});
|
||||
@@ -1019,36 +661,6 @@ future<> server::json_parser::stop() {
|
||||
return std::move(_run_parse_json_thread);
|
||||
}
|
||||
|
||||
// Convert an entry in the server's list of ongoing Alternator requests
|
||||
// (_ongoing_requests) into a client_data object. This client_data object
|
||||
// will then be used to produce a row for the "system.clients" virtual table.
|
||||
client_data server::ongoing_request::make_client_data() const {
|
||||
client_data cd;
|
||||
cd.ct = client_type::alternator;
|
||||
cd.ip = _client_address.addr();
|
||||
cd.port = _client_address.port();
|
||||
cd.shard_id = this_shard_id();
|
||||
cd.connection_stage = client_connection_stage::established;
|
||||
cd.username = _username;
|
||||
cd.scheduling_group_name = _scheduling_group.name();
|
||||
cd.ssl_enabled = _is_https;
|
||||
// For now, we save the full User-Agent header as the "driver name"
|
||||
// and keep "driver_version" unset.
|
||||
cd.driver_name = _user_agent;
|
||||
// Leave "protocol_version" unset, it has no meaning in Alternator.
|
||||
// Leave "hostname", "ssl_protocol" and "ssl_cipher_suite" unset for Alternator.
|
||||
// Note: CQL sets ssl_protocol and ssl_cipher_suite via generic_server::connection base class.
|
||||
return cd;
|
||||
}
|
||||
|
||||
future<utils::chunked_vector<foreign_ptr<std::unique_ptr<client_data>>>> server::get_client_data() {
|
||||
utils::chunked_vector<foreign_ptr<std::unique_ptr<client_data>>> ret;
|
||||
co_await _ongoing_requests.for_each_gently([&ret] (const ongoing_request& r) {
|
||||
ret.emplace_back(make_foreign(std::make_unique<client_data>(r.make_client_data())));
|
||||
});
|
||||
co_return ret;
|
||||
}
|
||||
|
||||
const char* api_error::what() const noexcept {
|
||||
if (_what_string.empty()) {
|
||||
_what_string = fmt::format("{} {}: {}", std::to_underlying(_http_code), _type, _msg);
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "alternator/executor.hh"
|
||||
#include "utils/scoped_item_list.hh"
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/core/condition-variable.hh>
|
||||
#include <seastar/http/httpd.hh>
|
||||
@@ -21,18 +20,12 @@
|
||||
#include "utils/updateable_value.hh"
|
||||
#include <seastar/core/units.hh>
|
||||
|
||||
struct client_data;
|
||||
|
||||
namespace alternator {
|
||||
|
||||
using chunked_content = rjson::chunked_content;
|
||||
|
||||
class server : public peering_sharded_service<server> {
|
||||
// The maximum size of a request body that Alternator will accept,
|
||||
// in bytes. This is a safety measure to prevent Alternator from
|
||||
// running out of memory when a client sends a very large request.
|
||||
// DynamoDB also has the same limit set to 16 MB.
|
||||
static constexpr size_t request_content_length_limit = 16*MB;
|
||||
class server {
|
||||
static constexpr size_t content_length_limit = 16*MB;
|
||||
using alternator_callback = std::function<future<executor::request_return_type>(executor&, executor::client_state&,
|
||||
tracing::trace_state_ptr, service_permit, rjson::value, std::unique_ptr<http::request>)>;
|
||||
using alternator_callbacks_map = std::unordered_map<std::string_view, alternator_callback>;
|
||||
@@ -47,23 +40,18 @@ class server : public peering_sharded_service<server> {
|
||||
|
||||
key_cache _key_cache;
|
||||
utils::updateable_value<bool> _enforce_authorization;
|
||||
utils::updateable_value<bool> _warn_authorization;
|
||||
utils::updateable_value<uint64_t> _max_users_query_size_in_trace_output;
|
||||
utils::small_vector<std::reference_wrapper<seastar::httpd::http_server>, 2> _enabled_servers;
|
||||
named_gate _pending_requests;
|
||||
gate _pending_requests;
|
||||
// In some places we will need a CQL updateable_timeout_config object even
|
||||
// though it isn't really relevant for Alternator which defines its own
|
||||
// timeouts separately. We can create this object only once.
|
||||
updateable_timeout_config _timeout_config;
|
||||
client_options_cache_type _connection_options_keys_and_values;
|
||||
|
||||
alternator_callbacks_map _callbacks;
|
||||
|
||||
semaphore* _memory_limiter;
|
||||
utils::updateable_value<uint32_t> _max_concurrent_requests;
|
||||
|
||||
::shared_ptr<seastar::tls::server_credentials> _credentials;
|
||||
|
||||
class json_parser {
|
||||
static constexpr size_t yieldable_parsing_threshold = 16*KB;
|
||||
chunked_content _raw_document;
|
||||
@@ -84,33 +72,12 @@ class server : public peering_sharded_service<server> {
|
||||
};
|
||||
json_parser _json_parser;
|
||||
|
||||
// The server maintains a list of ongoing requests, that are being handled
|
||||
// by handle_api_request(). It uses this list in get_client_data(), which
|
||||
// is called when reading the "system.clients" virtual table.
|
||||
struct ongoing_request {
|
||||
socket_address _client_address;
|
||||
client_options_cache_entry_type _user_agent;
|
||||
sstring _username;
|
||||
scheduling_group _scheduling_group;
|
||||
bool _is_https;
|
||||
client_data make_client_data() const;
|
||||
};
|
||||
utils::scoped_item_list<ongoing_request> _ongoing_requests;
|
||||
|
||||
public:
|
||||
server(executor& executor, service::storage_proxy& proxy, gms::gossiper& gossiper, auth::service& service, qos::service_level_controller& sl_controller);
|
||||
|
||||
future<> init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port,
|
||||
std::optional<uint16_t> port_proxy_protocol, std::optional<uint16_t> https_port_proxy_protocol,
|
||||
std::optional<tls::credentials_builder> creds,
|
||||
utils::updateable_value<bool> enforce_authorization, utils::updateable_value<bool> warn_authorization, utils::updateable_value<uint64_t> max_users_query_size_in_trace_output,
|
||||
semaphore* memory_limiter, utils::updateable_value<uint32_t> max_concurrent_requests);
|
||||
future<> init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port, std::optional<tls::credentials_builder> creds,
|
||||
utils::updateable_value<bool> enforce_authorization, semaphore* memory_limiter, utils::updateable_value<uint32_t> max_concurrent_requests);
|
||||
future<> stop();
|
||||
// get_client_data() is called (on each shard separately) when the virtual
|
||||
// table "system.clients" is read. It is expected to generate a list of
|
||||
// clients connected to this server (on this shard). This function is
|
||||
// called by alternator::controller::get_client_data().
|
||||
future<utils::chunked_vector<foreign_ptr<std::unique_ptr<client_data>>>> get_client_data();
|
||||
private:
|
||||
void set_routes(seastar::httpd::routes& r);
|
||||
// If verification succeeds, returns the authenticated user's username
|
||||
|
||||
@@ -9,49 +9,32 @@
|
||||
#include "stats.hh"
|
||||
#include "utils/histogram_metrics_helper.hh"
|
||||
#include <seastar/core/metrics.hh>
|
||||
#include "utils/labels.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
const char* ALTERNATOR_METRICS = "alternator";
|
||||
|
||||
static seastar::metrics::label column_family_label("cf");
|
||||
static seastar::metrics::label keyspace_label("ks");
|
||||
|
||||
|
||||
static void register_metrics_with_optional_table(seastar::metrics::metric_groups& metrics, const stats& stats, const sstring& ks, const sstring& table) {
|
||||
|
||||
stats::stats() : api_operations{} {
|
||||
// Register the
|
||||
seastar::metrics::label op("op");
|
||||
bool has_table = table.length();
|
||||
std::vector<seastar::metrics::label> aggregate_labels;
|
||||
std::vector<seastar::metrics::label_instance> labels = {alternator_label};
|
||||
sstring group_name = (has_table)? "alternator_table" : "alternator";
|
||||
if (has_table) {
|
||||
labels.push_back(column_family_label(table));
|
||||
labels.push_back(keyspace_label(ks));
|
||||
aggregate_labels.push_back(seastar::metrics::shard_label);
|
||||
}
|
||||
metrics.add_group(group_name, {
|
||||
#define OPERATION(name, CamelCaseName) \
|
||||
seastar::metrics::make_total_operations("operation", stats.api_operations.name, \
|
||||
seastar::metrics::description("number of operations via Alternator API"), labels)(basic_level)(op(CamelCaseName)).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
#define OPERATION_LATENCY(name, CamelCaseName) \
|
||||
metrics.add_group(group_name, { \
|
||||
seastar::metrics::make_histogram("op_latency", \
|
||||
seastar::metrics::description("Latency histogram of an operation via Alternator API"), labels, [&stats]{return to_metrics_histogram(stats.api_operations.name.histogram());})(op(CamelCaseName))(basic_level).aggregate({seastar::metrics::shard_label}).set_skip_when_empty()}); \
|
||||
if (!has_table) {\
|
||||
metrics.add_group("alternator", { \
|
||||
seastar::metrics::make_summary("op_latency_summary", \
|
||||
seastar::metrics::description("Latency summary of an operation via Alternator API"), [&stats]{return to_metrics_summary(stats.api_operations.name.summary());})(op(CamelCaseName))(basic_level)(alternator_label).set_skip_when_empty()}); \
|
||||
}
|
||||
|
||||
_metrics.add_group("alternator", {
|
||||
#define OPERATION(name, CamelCaseName) \
|
||||
seastar::metrics::make_total_operations("operation", api_operations.name, \
|
||||
seastar::metrics::description("number of operations via Alternator API"), {op(CamelCaseName)}).set_skip_when_empty(),
|
||||
#define OPERATION_LATENCY(name, CamelCaseName) \
|
||||
seastar::metrics::make_histogram("op_latency", \
|
||||
seastar::metrics::description("Latency histogram of an operation via Alternator API"), {op(CamelCaseName)}, [this]{return to_metrics_histogram(api_operations.name.histogram());}).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(), \
|
||||
seastar::metrics::make_summary("op_latency_summary", \
|
||||
seastar::metrics::description("Latency summary of an operation via Alternator API"), [this]{return to_metrics_summary(api_operations.name.summary());})(op(CamelCaseName)).set_skip_when_empty(),
|
||||
OPERATION(batch_get_item, "BatchGetItem")
|
||||
OPERATION(batch_write_item, "BatchWriteItem")
|
||||
OPERATION(create_backup, "CreateBackup")
|
||||
OPERATION(create_global_table, "CreateGlobalTable")
|
||||
OPERATION(create_table, "CreateTable")
|
||||
OPERATION(delete_backup, "DeleteBackup")
|
||||
OPERATION(delete_item, "DeleteItem")
|
||||
OPERATION(delete_table, "DeleteTable")
|
||||
OPERATION(describe_backup, "DescribeBackup")
|
||||
OPERATION(describe_continuous_backups, "DescribeContinuousBackups")
|
||||
OPERATION(describe_endpoints, "DescribeEndpoints")
|
||||
@@ -80,117 +63,55 @@ static void register_metrics_with_optional_table(seastar::metrics::metric_groups
|
||||
OPERATION(update_item, "UpdateItem")
|
||||
OPERATION(update_table, "UpdateTable")
|
||||
OPERATION(update_time_to_live, "UpdateTimeToLive")
|
||||
OPERATION_LATENCY(put_item_latency, "PutItem")
|
||||
OPERATION_LATENCY(get_item_latency, "GetItem")
|
||||
OPERATION_LATENCY(delete_item_latency, "DeleteItem")
|
||||
OPERATION_LATENCY(update_item_latency, "UpdateItem")
|
||||
OPERATION_LATENCY(batch_write_item_latency, "BatchWriteItem")
|
||||
OPERATION_LATENCY(batch_get_item_latency, "BatchGetItem")
|
||||
OPERATION(list_streams, "ListStreams")
|
||||
OPERATION(describe_stream, "DescribeStream")
|
||||
OPERATION(get_shard_iterator, "GetShardIterator")
|
||||
OPERATION(get_records, "GetRecords")
|
||||
OPERATION_LATENCY(get_records_latency, "GetRecords")
|
||||
});
|
||||
OPERATION_LATENCY(put_item_latency, "PutItem")
|
||||
OPERATION_LATENCY(get_item_latency, "GetItem")
|
||||
OPERATION_LATENCY(delete_item_latency, "DeleteItem")
|
||||
OPERATION_LATENCY(update_item_latency, "UpdateItem")
|
||||
OPERATION_LATENCY(batch_write_item_latency, "BatchWriteItem")
|
||||
OPERATION_LATENCY(batch_get_item_latency, "BatchGetItem")
|
||||
OPERATION_LATENCY(get_records_latency, "GetRecords")
|
||||
if (!has_table) {
|
||||
// Create and delete operations are not applicable to a per-table metrics
|
||||
// only register it for the global metrics
|
||||
metrics.add_group("alternator", {
|
||||
OPERATION(create_table, "CreateTable")
|
||||
OPERATION(delete_table, "DeleteTable")
|
||||
|
||||
});
|
||||
}
|
||||
metrics.add_group(group_name, {
|
||||
seastar::metrics::make_total_operations("unsupported_operations", stats.unsupported_operations,
|
||||
seastar::metrics::description("number of unsupported operations via Alternator API"), labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("total_operations", stats.total_operations,
|
||||
seastar::metrics::description("number of total operations via Alternator API"), labels)(basic_level).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("reads_before_write", stats.reads_before_write,
|
||||
seastar::metrics::description("number of performed read-before-write operations"), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("write_using_lwt", stats.write_using_lwt,
|
||||
seastar::metrics::description("number of writes that used LWT"), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("shard_bounce_for_lwt", stats.shard_bounce_for_lwt,
|
||||
seastar::metrics::description("number writes that had to be bounced from this shard because of LWT requirements"), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("requests_blocked_memory", stats.requests_blocked_memory,
|
||||
seastar::metrics::description("Counts a number of requests blocked due to memory pressure."), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("requests_shed", stats.requests_shed,
|
||||
seastar::metrics::description("Counts a number of requests shed due to overload."), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("filtered_rows_read_total", stats.cql_stats.filtered_rows_read_total,
|
||||
seastar::metrics::description("number of rows read during filtering operations"), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("filtered_rows_matched_total", stats.cql_stats.filtered_rows_matched_total,
|
||||
seastar::metrics::description("number of rows read and matched during filtering operations"), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("rcu_total", [&stats]{return 0.5 * stats.rcu_half_units_total;},
|
||||
seastar::metrics::description("total number of consumed read units"), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("wcu_total", stats.wcu_total[stats::wcu_types::PUT_ITEM],
|
||||
seastar::metrics::description("total number of consumed write units"), labels)(op("PutItem")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("wcu_total", stats.wcu_total[stats::wcu_types::DELETE_ITEM],
|
||||
seastar::metrics::description("total number of consumed write units"), labels)(op("DeleteItem")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("wcu_total", stats.wcu_total[stats::wcu_types::UPDATE_ITEM],
|
||||
seastar::metrics::description("total number of consumed write units"), labels)(op("UpdateItem")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("wcu_total", stats.wcu_total[stats::wcu_types::INDEX],
|
||||
seastar::metrics::description("total number of consumed write units"), labels)(op("Index")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("filtered_rows_dropped_total", [&stats] { return stats.cql_stats.filtered_rows_read_total - stats.cql_stats.filtered_rows_matched_total; },
|
||||
seastar::metrics::description("number of rows read and dropped during filtering operations"), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("batch_item_count", seastar::metrics::description("The total number of items processed across all batches"), labels,
|
||||
stats.api_operations.batch_write_item_batch_total)(op("BatchWriteItem")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("batch_item_count", seastar::metrics::description("The total number of items processed across all batches"), labels,
|
||||
stats.api_operations.batch_get_item_batch_total)(op("BatchGetItem")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_histogram("batch_item_count_histogram", seastar::metrics::description("Histogram of the number of items in a batch request"), labels,
|
||||
[&stats]{ return to_metrics_histogram(stats.api_operations.batch_get_item_histogram);})(op("BatchGetItem")).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
seastar::metrics::make_histogram("batch_item_count_histogram", seastar::metrics::description("Histogram of the number of items in a batch request"), labels,
|
||||
[&stats]{ return to_metrics_histogram(stats.api_operations.batch_write_item_histogram);})(op("BatchWriteItem")).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
seastar::metrics::make_histogram("operation_size_kb", seastar::metrics::description("Histogram of item sizes involved in a request"), labels,
|
||||
[&stats]{ return to_metrics_histogram(stats.operation_sizes.get_item_op_size_kb);})(op("GetItem")).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
seastar::metrics::make_histogram("operation_size_kb", seastar::metrics::description("Histogram of item sizes involved in a request"), labels,
|
||||
[&stats]{ return to_metrics_histogram(stats.operation_sizes.put_item_op_size_kb);})(op("PutItem")).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
seastar::metrics::make_histogram("operation_size_kb", seastar::metrics::description("Histogram of item sizes involved in a request"), labels,
|
||||
[&stats]{ return to_metrics_histogram(stats.operation_sizes.delete_item_op_size_kb);})(op("DeleteItem")).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
seastar::metrics::make_histogram("operation_size_kb", seastar::metrics::description("Histogram of item sizes involved in a request"), labels,
|
||||
[&stats]{ return to_metrics_histogram(stats.operation_sizes.update_item_op_size_kb);})(op("UpdateItem")).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
seastar::metrics::make_histogram("operation_size_kb", seastar::metrics::description("Histogram of item sizes involved in a request"), labels,
|
||||
[&stats]{ return to_metrics_histogram(stats.operation_sizes.batch_get_item_op_size_kb);})(op("BatchGetItem")).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
seastar::metrics::make_histogram("operation_size_kb", seastar::metrics::description("Histogram of item sizes involved in a request"), labels,
|
||||
[&stats]{ return to_metrics_histogram(stats.operation_sizes.batch_write_item_op_size_kb);})(op("BatchWriteItem")).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
_metrics.add_group("alternator", {
|
||||
seastar::metrics::make_total_operations("unsupported_operations", unsupported_operations,
|
||||
seastar::metrics::description("number of unsupported operations via Alternator API")),
|
||||
seastar::metrics::make_total_operations("total_operations", total_operations,
|
||||
seastar::metrics::description("number of total operations via Alternator API")),
|
||||
seastar::metrics::make_total_operations("reads_before_write", reads_before_write,
|
||||
seastar::metrics::description("number of performed read-before-write operations")),
|
||||
seastar::metrics::make_total_operations("write_using_lwt", write_using_lwt,
|
||||
seastar::metrics::description("number of writes that used LWT")),
|
||||
seastar::metrics::make_total_operations("shard_bounce_for_lwt", shard_bounce_for_lwt,
|
||||
seastar::metrics::description("number writes that had to be bounced from this shard because of LWT requirements")),
|
||||
seastar::metrics::make_total_operations("requests_blocked_memory", requests_blocked_memory,
|
||||
seastar::metrics::description("Counts a number of requests blocked due to memory pressure.")),
|
||||
seastar::metrics::make_total_operations("requests_shed", requests_shed,
|
||||
seastar::metrics::description("Counts a number of requests shed due to overload.")),
|
||||
seastar::metrics::make_total_operations("filtered_rows_read_total", cql_stats.filtered_rows_read_total,
|
||||
seastar::metrics::description("number of rows read during filtering operations")),
|
||||
seastar::metrics::make_total_operations("filtered_rows_matched_total", cql_stats.filtered_rows_matched_total,
|
||||
seastar::metrics::description("number of rows read and matched during filtering operations")),
|
||||
seastar::metrics::make_counter("rcu_total", rcu_total,
|
||||
seastar::metrics::description("total number of consumed read units, counted as half units")).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("wcu_total", wcu_total[wcu_types::PUT_ITEM],
|
||||
seastar::metrics::description("total number of consumed write units, counted as half units"),{op("PutItem")}).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("wcu_total", wcu_total[wcu_types::DELETE_ITEM],
|
||||
seastar::metrics::description("total number of consumed write units, counted as half units"),{op("DeleteItem")}).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("wcu_total", wcu_total[wcu_types::UPDATE_ITEM],
|
||||
seastar::metrics::description("total number of consumed write units, counted as half units"),{op("UpdateItem")}).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("wcu_total", wcu_total[wcu_types::INDEX],
|
||||
seastar::metrics::description("total number of consumed write units, counted as half units"),{op("Index")}).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("filtered_rows_dropped_total", [this] { return cql_stats.filtered_rows_read_total - cql_stats.filtered_rows_matched_total; },
|
||||
seastar::metrics::description("number of rows read and dropped during filtering operations")),
|
||||
seastar::metrics::make_counter("batch_item_count", seastar::metrics::description("The total number of items processed across all batches"),{op("BatchWriteItem")},
|
||||
api_operations.batch_write_item_batch_total).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("batch_item_count", seastar::metrics::description("The total number of items processed across all batches"),{op("BatchGetItem")},
|
||||
api_operations.batch_get_item_batch_total).set_skip_when_empty(),
|
||||
});
|
||||
|
||||
seastar::metrics::label expression_label("expression");
|
||||
metrics.add_group(group_name, {
|
||||
seastar::metrics::make_total_operations("expression_cache_evictions", stats.expression_cache.evictions,
|
||||
seastar::metrics::description("Counts number of entries evicted from expressions cache"), labels).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
|
||||
seastar::metrics::make_total_operations("expression_cache_hits", stats.expression_cache.requests[stats::expression_types::UPDATE_EXPRESSION].hits,
|
||||
seastar::metrics::description("Counts number of hits of cached expressions"), labels)(expression_label("UpdateExpression")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("expression_cache_misses", stats.expression_cache.requests[stats::expression_types::UPDATE_EXPRESSION].misses,
|
||||
seastar::metrics::description("Counts number of misses of cached expressions"), labels)(expression_label("UpdateExpression")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
|
||||
seastar::metrics::make_total_operations("expression_cache_hits", stats.expression_cache.requests[stats::expression_types::CONDITION_EXPRESSION].hits,
|
||||
seastar::metrics::description("Counts number of hits of cached expressions"), labels)(expression_label("ConditionExpression")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("expression_cache_misses", stats.expression_cache.requests[stats::expression_types::CONDITION_EXPRESSION].misses,
|
||||
seastar::metrics::description("Counts number of misses of cached expressions"), labels)(expression_label("ConditionExpression")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
|
||||
seastar::metrics::make_total_operations("expression_cache_hits", stats.expression_cache.requests[stats::expression_types::PROJECTION_EXPRESSION].hits,
|
||||
seastar::metrics::description("Counts number of hits of cached expressions"), labels)(expression_label("ProjectionExpression")).aggregate(aggregate_labels).set_skip_when_empty(),
|
||||
seastar::metrics::make_total_operations("expression_cache_misses", stats.expression_cache.requests[stats::expression_types::PROJECTION_EXPRESSION].misses,
|
||||
seastar::metrics::description("Counts number of misses of cached expressions"), labels)(expression_label("ProjectionExpression")).aggregate(aggregate_labels).set_skip_when_empty()
|
||||
});
|
||||
|
||||
// Only register the following metrics for the global metrics, not per-table
|
||||
if (!has_table) {
|
||||
metrics.add_group("alternator", {
|
||||
seastar::metrics::make_counter("authentication_failures", stats.authentication_failures,
|
||||
seastar::metrics::description("total number of authentication failures"), labels).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
seastar::metrics::make_counter("authorization_failures", stats.authorization_failures,
|
||||
seastar::metrics::description("total number of authorization failures"), labels).aggregate({seastar::metrics::shard_label}).set_skip_when_empty(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void register_metrics(seastar::metrics::metric_groups& metrics, const stats& stats) {
|
||||
register_metrics_with_optional_table(metrics, stats, "", "");
|
||||
}
|
||||
table_stats::table_stats(const sstring& ks, const sstring& table) {
|
||||
_stats = make_lw_shared<stats>();
|
||||
register_metrics_with_optional_table(_metrics, *_stats, ks, table);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,18 +12,16 @@
|
||||
|
||||
#include <seastar/core/metrics_registration.hh>
|
||||
#include "utils/histogram.hh"
|
||||
#include "utils/estimated_histogram.hh"
|
||||
#include "cql3/stats.hh"
|
||||
|
||||
namespace alternator {
|
||||
using batch_histogram = utils::estimated_histogram_with_max<128>;
|
||||
using op_size_histogram = utils::estimated_histogram_with_max<512>;
|
||||
|
||||
// Object holding per-shard statistics related to Alternator.
|
||||
// While this object is alive, these metrics are also registered to be
|
||||
// visible by the metrics REST API, with the "alternator" prefix.
|
||||
class stats {
|
||||
public:
|
||||
stats();
|
||||
// Count of DynamoDB API operations by types
|
||||
struct {
|
||||
uint64_t batch_get_item = 0;
|
||||
@@ -77,47 +75,7 @@ public:
|
||||
utils::timed_rate_moving_average_summary_and_histogram batch_write_item_latency;
|
||||
utils::timed_rate_moving_average_summary_and_histogram batch_get_item_latency;
|
||||
utils::timed_rate_moving_average_summary_and_histogram get_records_latency;
|
||||
|
||||
batch_histogram batch_get_item_histogram;
|
||||
batch_histogram batch_write_item_histogram;
|
||||
} api_operations;
|
||||
// Operation size metrics
|
||||
struct {
|
||||
// Item size statistics collected per table and aggregated per node.
|
||||
// Each histogram covers the range 0 - 512. Resolves #25143.
|
||||
// A size is the retrieved item's size.
|
||||
op_size_histogram get_item_op_size_kb;
|
||||
// A size is the maximum of the new item's size and the old item's size.
|
||||
op_size_histogram put_item_op_size_kb;
|
||||
// A size is the deleted item's size. If the deleted item's size is
|
||||
// unknown (i.e. read-before-write wasn't necessary and it wasn't
|
||||
// forced by a configuration option), it won't be recorded on the
|
||||
// histogram.
|
||||
op_size_histogram delete_item_op_size_kb;
|
||||
// A size is the maximum of existing item's size and the estimated size
|
||||
// of the update. This will be changed to the maximum of the existing item's
|
||||
// size and the new item's size in a subsequent PR.
|
||||
op_size_histogram update_item_op_size_kb;
|
||||
|
||||
// A size is the sum of the sizes of all items per table. This means
|
||||
// that a single BatchGetItem / BatchWriteItem updates the histogram
|
||||
// for each table that it has items in.
|
||||
// The sizes are the retrieved items' sizes grouped per table.
|
||||
op_size_histogram batch_get_item_op_size_kb;
|
||||
// The sizes are the the written items' sizes grouped per table.
|
||||
op_size_histogram batch_write_item_op_size_kb;
|
||||
} operation_sizes;
|
||||
// Count of authentication and authorization failures, counted if either
|
||||
// alternator_enforce_authorization or alternator_warn_authorization are
|
||||
// set to true. If both are false, no authentication or authorization
|
||||
// checks are performed, so failures are not recognized or counted.
|
||||
// "authentication" failure means the request was not signed with a valid
|
||||
// user and key combination. "authorization" failure means the request was
|
||||
// authenticated to a valid user - but this user did not have permissions
|
||||
// to perform the operation (considering RBAC settings and the user's
|
||||
// superuser status).
|
||||
uint64_t authentication_failures = 0;
|
||||
uint64_t authorization_failures = 0;
|
||||
// Miscellaneous event counters
|
||||
uint64_t total_operations = 0;
|
||||
uint64_t unsupported_operations = 0;
|
||||
@@ -126,7 +84,7 @@ public:
|
||||
uint64_t shard_bounce_for_lwt = 0;
|
||||
uint64_t requests_blocked_memory = 0;
|
||||
uint64_t requests_shed = 0;
|
||||
uint64_t rcu_half_units_total = 0;
|
||||
uint64_t rcu_total = 0;
|
||||
// wcu can results from put, update, delete and index
|
||||
// Index related will be done on top of the operation it comes with
|
||||
enum wcu_types {
|
||||
@@ -140,33 +98,10 @@ public:
|
||||
uint64_t wcu_total[NUM_TYPES] = {0};
|
||||
// CQL-derived stats
|
||||
cql3::cql_stats cql_stats;
|
||||
|
||||
// Enumeration of expression types only for stats
|
||||
// if needed it can be extended e.g. per operation
|
||||
enum expression_types {
|
||||
UPDATE_EXPRESSION,
|
||||
CONDITION_EXPRESSION,
|
||||
PROJECTION_EXPRESSION,
|
||||
NUM_EXPRESSION_TYPES
|
||||
};
|
||||
struct {
|
||||
struct {
|
||||
uint64_t hits = 0;
|
||||
uint64_t misses = 0;
|
||||
} requests[NUM_EXPRESSION_TYPES];
|
||||
uint64_t evictions = 0;
|
||||
} expression_cache;
|
||||
};
|
||||
|
||||
struct table_stats {
|
||||
table_stats(const sstring& ks, const sstring& table);
|
||||
private:
|
||||
// The metric_groups object holds this stat object's metrics registered
|
||||
// as long as the stats object is alive.
|
||||
seastar::metrics::metric_groups _metrics;
|
||||
lw_shared_ptr<stats> _stats;
|
||||
};
|
||||
void register_metrics(seastar::metrics::metric_groups& metrics, const stats& stats);
|
||||
|
||||
inline uint64_t bytes_to_kb_ceil(uint64_t bytes) {
|
||||
return (bytes) / 1024;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <seastar/json/formatter.hh>
|
||||
|
||||
#include "auth/permission.hh"
|
||||
#include "db/config.hh"
|
||||
|
||||
#include "cdc/log.hh"
|
||||
@@ -31,9 +32,6 @@
|
||||
|
||||
#include "executor.hh"
|
||||
#include "data_dictionary/data_dictionary.hh"
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
static logging::logger elogger("alternator-streams");
|
||||
|
||||
/**
|
||||
* Base template type to implement rapidjson::internal::TypeHelper<...>:s
|
||||
@@ -128,7 +126,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace alternator
|
||||
}
|
||||
|
||||
template<typename ValueType>
|
||||
struct rapidjson::internal::TypeHelper<ValueType, alternator::stream_arn>
|
||||
@@ -219,7 +217,7 @@ future<alternator::executor::request_return_type> alternator::executor::list_str
|
||||
rjson::add(ret, "LastEvaluatedStreamArn", *last);
|
||||
}
|
||||
|
||||
return make_ready_future<executor::request_return_type>(rjson::print(std::move(ret)));
|
||||
return make_ready_future<executor::request_return_type>(make_jsonable(std::move(ret)));
|
||||
}
|
||||
|
||||
struct shard_id {
|
||||
@@ -298,7 +296,7 @@ sequence_number::sequence_number(std::string_view v)
|
||||
}())
|
||||
{}
|
||||
|
||||
} // namespace alternator
|
||||
}
|
||||
|
||||
template<typename ValueType>
|
||||
struct rapidjson::internal::TypeHelper<ValueType, alternator::shard_id>
|
||||
@@ -358,7 +356,7 @@ static stream_view_type cdc_options_to_steam_view_type(const cdc::options& opts)
|
||||
return type;
|
||||
}
|
||||
|
||||
} // namespace alternator
|
||||
}
|
||||
|
||||
template<typename ValueType>
|
||||
struct rapidjson::internal::TypeHelper<ValueType, alternator::stream_view_type>
|
||||
@@ -430,25 +428,6 @@ using namespace std::chrono_literals;
|
||||
// Dynamo docs says no data shall live longer than 24h.
|
||||
static constexpr auto dynamodb_streams_max_window = 24h;
|
||||
|
||||
// find the parent shard in previous generation for the given child shard
|
||||
// takes care of wrap-around case in vnodes
|
||||
// prev_streams must be sorted by token
|
||||
const cdc::stream_id& find_parent_shard_in_previous_generation(db_clock::time_point prev_timestamp, const utils::chunked_vector<cdc::stream_id> &prev_streams, const cdc::stream_id &child) {
|
||||
if (prev_streams.empty()) {
|
||||
// something is really wrong - streams are empty
|
||||
// let's try internal_error in hope it will be notified and fixed
|
||||
on_internal_error(elogger, fmt::format("streams are empty for cdc generation at {} ({})", prev_timestamp, prev_timestamp.time_since_epoch().count()));
|
||||
}
|
||||
auto it = std::lower_bound(prev_streams.begin(), prev_streams.end(), child.token(), [](const cdc::stream_id& id, const dht::token& t) {
|
||||
return id.token() < t;
|
||||
});
|
||||
if (it == prev_streams.end()) {
|
||||
// wrap around case - take first
|
||||
it = prev_streams.begin();
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
|
||||
future<executor::request_return_type> executor::describe_stream(client_state& client_state, service_permit permit, rjson::value request) {
|
||||
_stats.api_operations.describe_stream++;
|
||||
|
||||
@@ -496,10 +475,10 @@ future<executor::request_return_type> executor::describe_stream(client_state& cl
|
||||
} else {
|
||||
status = "ENABLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto ttl = std::chrono::seconds(opts.ttl());
|
||||
|
||||
|
||||
rjson::add(stream_desc, "StreamStatus", rjson::from_string(status));
|
||||
|
||||
stream_view_type type = cdc_options_to_steam_view_type(opts);
|
||||
@@ -512,7 +491,7 @@ future<executor::request_return_type> executor::describe_stream(client_state& cl
|
||||
|
||||
if (!opts.enabled()) {
|
||||
rjson::add(ret, "StreamDescription", std::move(stream_desc));
|
||||
co_return rjson::print(std::move(ret));
|
||||
return make_ready_future<executor::request_return_type>(make_jsonable(std::move(ret)));
|
||||
}
|
||||
|
||||
// TODO: label
|
||||
@@ -523,113 +502,123 @@ future<executor::request_return_type> executor::describe_stream(client_state& cl
|
||||
// filter out cdc generations older than the table or now() - cdc::ttl (typically dynamodb_streams_max_window - 24h)
|
||||
auto low_ts = std::max(as_timepoint(schema->id()), db_clock::now() - ttl);
|
||||
|
||||
std::map<db_clock::time_point, cdc::streams_version> topologies = co_await _sdks.cdc_get_versioned_streams(low_ts, { normal_token_owners });
|
||||
auto e = topologies.end();
|
||||
auto prev = e;
|
||||
auto shards = rjson::empty_array();
|
||||
return _sdks.cdc_get_versioned_streams(low_ts, { normal_token_owners }).then([db, shard_start, limit, ret = std::move(ret), stream_desc = std::move(stream_desc)] (std::map<db_clock::time_point, cdc::streams_version> topologies) mutable {
|
||||
|
||||
std::optional<shard_id> last;
|
||||
auto e = topologies.end();
|
||||
auto prev = e;
|
||||
auto shards = rjson::empty_array();
|
||||
|
||||
auto i = topologies.begin();
|
||||
// if we're a paged query, skip to the generation where we left of.
|
||||
if (shard_start) {
|
||||
i = topologies.find(shard_start->time);
|
||||
}
|
||||
std::optional<shard_id> last;
|
||||
|
||||
// for parent-child stuff we need id:s to be sorted by token
|
||||
// (see explanation above) since we want to find closest
|
||||
// token boundary when determining parent.
|
||||
// #7346 - we processed and searched children/parents in
|
||||
// stored order, which is not necessarily token order,
|
||||
// so the finding of "closest" token boundary (using upper bound)
|
||||
// could give somewhat weird results.
|
||||
static auto token_cmp = [](const cdc::stream_id& id1, const cdc::stream_id& id2) {
|
||||
return id1.token() < id2.token();
|
||||
};
|
||||
auto i = topologies.begin();
|
||||
// if we're a paged query, skip to the generation where we left of.
|
||||
if (shard_start) {
|
||||
i = topologies.find(shard_start->time);
|
||||
}
|
||||
|
||||
// #7409 - shards must be returned in lexicographical order,
|
||||
// normal bytes compare is string_traits<int8_t>::compare.
|
||||
// thus bytes 0x8000 is less than 0x0000. By doing unsigned
|
||||
// compare instead we inadvertently will sort in string lexical.
|
||||
static auto id_cmp = [](const cdc::stream_id& id1, const cdc::stream_id& id2) {
|
||||
return compare_unsigned(id1.to_bytes(), id2.to_bytes()) < 0;
|
||||
};
|
||||
|
||||
// need a prev even if we are skipping stuff
|
||||
if (i != topologies.begin()) {
|
||||
prev = std::prev(i);
|
||||
}
|
||||
|
||||
for (; limit > 0 && i != e; prev = i, ++i) {
|
||||
auto& [ts, sv] = *i;
|
||||
|
||||
last = std::nullopt;
|
||||
|
||||
auto lo = sv.streams.begin();
|
||||
auto end = sv.streams.end();
|
||||
// for parent-child stuff we need id:s to be sorted by token
|
||||
// (see explanation above) since we want to find closest
|
||||
// token boundary when determining parent.
|
||||
// #7346 - we processed and searched children/parents in
|
||||
// stored order, which is not necessarily token order,
|
||||
// so the finding of "closest" token boundary (using upper bound)
|
||||
// could give somewhat weird results.
|
||||
static auto token_cmp = [](const cdc::stream_id& id1, const cdc::stream_id& id2) {
|
||||
return id1.token() < id2.token();
|
||||
};
|
||||
|
||||
// #7409 - shards must be returned in lexicographical order,
|
||||
std::sort(lo, end, id_cmp);
|
||||
// normal bytes compare is string_traits<int8_t>::compare.
|
||||
// thus bytes 0x8000 is less than 0x0000. By doing unsigned
|
||||
// compare instead we inadvertently will sort in string lexical.
|
||||
static auto id_cmp = [](const cdc::stream_id& id1, const cdc::stream_id& id2) {
|
||||
return compare_unsigned(id1.to_bytes(), id2.to_bytes()) < 0;
|
||||
};
|
||||
|
||||
if (shard_start) {
|
||||
// find next shard position
|
||||
lo = std::upper_bound(lo, end, shard_start->id, id_cmp);
|
||||
shard_start = std::nullopt;
|
||||
// need a prev even if we are skipping stuff
|
||||
if (i != topologies.begin()) {
|
||||
prev = std::prev(i);
|
||||
}
|
||||
|
||||
if (lo != end && prev != e) {
|
||||
// We want older stuff sorted in token order so we can find matching
|
||||
// token range when determining parent shard.
|
||||
std::stable_sort(prev->second.streams.begin(), prev->second.streams.end(), token_cmp);
|
||||
}
|
||||
|
||||
auto expired = [&]() -> std::optional<db_clock::time_point> {
|
||||
auto j = std::next(i);
|
||||
if (j == e) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// add this so we sort of match potential
|
||||
// sequence numbers in get_records result.
|
||||
return j->first + confidence_interval(db);
|
||||
}();
|
||||
|
||||
while (lo != end) {
|
||||
auto& id = *lo++;
|
||||
|
||||
auto shard = rjson::empty_object();
|
||||
|
||||
if (prev != e) {
|
||||
auto &pid = find_parent_shard_in_previous_generation(prev->first, prev->second.streams, id);
|
||||
rjson::add(shard, "ParentShardId", shard_id(prev->first, pid));
|
||||
}
|
||||
|
||||
last.emplace(ts, id);
|
||||
rjson::add(shard, "ShardId", *last);
|
||||
auto range = rjson::empty_object();
|
||||
rjson::add(range, "StartingSequenceNumber", sequence_number(utils::UUID_gen::min_time_UUID(ts.time_since_epoch())));
|
||||
if (expired) {
|
||||
rjson::add(range, "EndingSequenceNumber", sequence_number(utils::UUID_gen::min_time_UUID(expired->time_since_epoch())));
|
||||
}
|
||||
|
||||
rjson::add(shard, "SequenceNumberRange", std::move(range));
|
||||
rjson::push_back(shards, std::move(shard));
|
||||
|
||||
if (--limit == 0) {
|
||||
break;
|
||||
}
|
||||
for (; limit > 0 && i != e; prev = i, ++i) {
|
||||
auto& [ts, sv] = *i;
|
||||
|
||||
last = std::nullopt;
|
||||
|
||||
auto lo = sv.streams.begin();
|
||||
auto end = sv.streams.end();
|
||||
|
||||
// #7409 - shards must be returned in lexicographical order,
|
||||
std::sort(lo, end, id_cmp);
|
||||
|
||||
if (shard_start) {
|
||||
// find next shard position
|
||||
lo = std::upper_bound(lo, end, shard_start->id, id_cmp);
|
||||
shard_start = std::nullopt;
|
||||
}
|
||||
|
||||
if (lo != end && prev != e) {
|
||||
// We want older stuff sorted in token order so we can find matching
|
||||
// token range when determining parent shard.
|
||||
std::stable_sort(prev->second.streams.begin(), prev->second.streams.end(), token_cmp);
|
||||
}
|
||||
|
||||
auto expired = [&]() -> std::optional<db_clock::time_point> {
|
||||
auto j = std::next(i);
|
||||
if (j == e) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// add this so we sort of match potential
|
||||
// sequence numbers in get_records result.
|
||||
return j->first + confidence_interval(db);
|
||||
}();
|
||||
|
||||
while (lo != end) {
|
||||
auto& id = *lo++;
|
||||
|
||||
auto shard = rjson::empty_object();
|
||||
|
||||
if (prev != e) {
|
||||
auto& pids = prev->second.streams;
|
||||
auto pid = std::upper_bound(pids.begin(), pids.end(), id.token(), [](const dht::token& t, const cdc::stream_id& id) {
|
||||
return t < id.token();
|
||||
});
|
||||
if (pid != pids.begin()) {
|
||||
pid = std::prev(pid);
|
||||
}
|
||||
if (pid != pids.end()) {
|
||||
rjson::add(shard, "ParentShardId", shard_id(prev->first, *pid));
|
||||
}
|
||||
}
|
||||
|
||||
last.emplace(ts, id);
|
||||
rjson::add(shard, "ShardId", *last);
|
||||
auto range = rjson::empty_object();
|
||||
rjson::add(range, "StartingSequenceNumber", sequence_number(utils::UUID_gen::min_time_UUID(ts.time_since_epoch())));
|
||||
if (expired) {
|
||||
rjson::add(range, "EndingSequenceNumber", sequence_number(utils::UUID_gen::min_time_UUID(expired->time_since_epoch())));
|
||||
}
|
||||
|
||||
rjson::add(shard, "SequenceNumberRange", std::move(range));
|
||||
rjson::push_back(shards, std::move(shard));
|
||||
|
||||
if (--limit == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
last = std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (last) {
|
||||
rjson::add(stream_desc, "LastEvaluatedShardId", *last);
|
||||
}
|
||||
if (last) {
|
||||
rjson::add(stream_desc, "LastEvaluatedShardId", *last);
|
||||
}
|
||||
|
||||
rjson::add(stream_desc, "Shards", std::move(shards));
|
||||
rjson::add(ret, "StreamDescription", std::move(stream_desc));
|
||||
|
||||
co_return rjson::print(std::move(ret));
|
||||
rjson::add(stream_desc, "Shards", std::move(shards));
|
||||
rjson::add(ret, "StreamDescription", std::move(stream_desc));
|
||||
|
||||
return make_ready_future<executor::request_return_type>(make_jsonable(std::move(ret)));
|
||||
});
|
||||
}
|
||||
|
||||
enum class shard_iterator_type {
|
||||
@@ -725,7 +714,7 @@ future<executor::request_return_type> executor::get_shard_iterator(client_state&
|
||||
|
||||
auto type = rjson::get<shard_iterator_type>(request, "ShardIteratorType");
|
||||
auto seq_num = rjson::get_opt<sequence_number>(request, "SequenceNumber");
|
||||
|
||||
|
||||
if (type < shard_iterator_type::TRIM_HORIZON && !seq_num) {
|
||||
throw api_error::validation("Missing required parameter \"SequenceNumber\"");
|
||||
}
|
||||
@@ -735,7 +724,7 @@ future<executor::request_return_type> executor::get_shard_iterator(client_state&
|
||||
|
||||
auto stream_arn = rjson::get<alternator::stream_arn>(request, "StreamArn");
|
||||
auto db = _proxy.data_dictionary();
|
||||
|
||||
|
||||
schema_ptr schema = nullptr;
|
||||
std::optional<shard_id> sid;
|
||||
|
||||
@@ -781,7 +770,7 @@ future<executor::request_return_type> executor::get_shard_iterator(client_state&
|
||||
auto ret = rjson::empty_object();
|
||||
rjson::add(ret, "ShardIterator", iter);
|
||||
|
||||
return make_ready_future<executor::request_return_type>(rjson::print(std::move(ret)));
|
||||
return make_ready_future<executor::request_return_type>(make_jsonable(std::move(ret)));
|
||||
}
|
||||
|
||||
struct event_id {
|
||||
@@ -800,7 +789,7 @@ struct event_id {
|
||||
return os;
|
||||
}
|
||||
};
|
||||
} // namespace alternator
|
||||
}
|
||||
|
||||
template<typename ValueType>
|
||||
struct rapidjson::internal::TypeHelper<ValueType, alternator::event_id>
|
||||
@@ -819,9 +808,6 @@ future<executor::request_return_type> executor::get_records(client_state& client
|
||||
if (limit < 1) {
|
||||
throw api_error::validation("Limit must be 1 or more");
|
||||
}
|
||||
if (limit > 1000) {
|
||||
throw api_error::validation("Limit must be less than or equal to 1000");
|
||||
}
|
||||
|
||||
auto db = _proxy.data_dictionary();
|
||||
schema_ptr schema, base;
|
||||
@@ -838,7 +824,7 @@ future<executor::request_return_type> executor::get_records(client_state& client
|
||||
|
||||
tracing::add_table_name(trace_state, schema->ks_name(), schema->cf_name());
|
||||
|
||||
co_await verify_permission(_enforce_authorization, _warn_authorization, client_state, schema, auth::permission::SELECT, _stats);
|
||||
co_await verify_permission(_enforce_authorization, client_state, schema, auth::permission::SELECT);
|
||||
|
||||
db::consistency_level cl = db::consistency_level::LOCAL_QUORUM;
|
||||
partition_key pk = iter.shard.id.to_partition_key(*schema);
|
||||
@@ -882,12 +868,10 @@ future<executor::request_return_type> executor::get_records(client_state& client
|
||||
|
||||
std::transform(pks.begin(), pks.end(), std::back_inserter(columns), [](auto& c) { return &c; });
|
||||
std::transform(cks.begin(), cks.end(), std::back_inserter(columns), [](auto& c) { return &c; });
|
||||
auto regular_column_start_idx = columns.size();
|
||||
auto regular_column_filter = std::views::filter([](const column_definition& cdef) { return cdef.name() == op_column_name || cdef.name() == eor_column_name || !cdc::is_cdc_metacolumn_name(cdef.name_as_text()); });
|
||||
std::ranges::transform(schema->regular_columns() | regular_column_filter, std::back_inserter(columns), [](auto& c) { return &c; });
|
||||
|
||||
auto regular_columns = std::ranges::subrange(columns.begin() + regular_column_start_idx, columns.end())
|
||||
| std::views::transform(&column_definition::id)
|
||||
auto regular_columns = schema->regular_columns()
|
||||
| std::views::filter([](const column_definition& cdef) { return cdef.name() == op_column_name || cdef.name() == eor_column_name || !cdc::is_cdc_metacolumn_name(cdef.name_as_text()); })
|
||||
| std::views::transform([&] (const column_definition& cdef) { columns.emplace_back(&cdef); return cdef.id; })
|
||||
| std::ranges::to<query::column_id_vector>()
|
||||
;
|
||||
|
||||
@@ -909,179 +893,172 @@ future<executor::request_return_type> executor::get_records(client_state& client
|
||||
auto command = ::make_lw_shared<query::read_command>(schema->id(), schema->version(), partition_slice, _proxy.get_max_result_size(partition_slice),
|
||||
query::tombstone_limit(_proxy.get_tombstone_limit()), query::row_limit(limit * mul));
|
||||
|
||||
service::storage_proxy::coordinator_query_result qr = co_await _proxy.query(schema, std::move(command), std::move(partition_ranges), cl, service::storage_proxy::coordinator_query_options(default_timeout(), std::move(permit), client_state));
|
||||
cql3::selection::result_set_builder builder(*selection, gc_clock::now());
|
||||
query::result_view::consume(*qr.query_result, partition_slice, cql3::selection::result_set_builder::visitor(builder, *schema, *selection));
|
||||
co_return co_await _proxy.query(schema, std::move(command), std::move(partition_ranges), cl, service::storage_proxy::coordinator_query_options(default_timeout(), std::move(permit), client_state)).then(
|
||||
[this, schema, partition_slice = std::move(partition_slice), selection = std::move(selection), start_time = std::move(start_time), limit, key_names = std::move(key_names), attr_names = std::move(attr_names), type, iter, high_ts] (service::storage_proxy::coordinator_query_result qr) mutable {
|
||||
cql3::selection::result_set_builder builder(*selection, gc_clock::now());
|
||||
query::result_view::consume(*qr.query_result, partition_slice, cql3::selection::result_set_builder::visitor(builder, *schema, *selection));
|
||||
|
||||
auto result_set = builder.build();
|
||||
auto records = rjson::empty_array();
|
||||
auto result_set = builder.build();
|
||||
auto records = rjson::empty_array();
|
||||
|
||||
auto& metadata = result_set->get_metadata();
|
||||
auto& metadata = result_set->get_metadata();
|
||||
|
||||
auto op_index = std::distance(metadata.get_names().begin(),
|
||||
std::find_if(metadata.get_names().begin(), metadata.get_names().end(), [](const lw_shared_ptr<cql3::column_specification>& cdef) {
|
||||
return cdef->name->name() == op_column_name;
|
||||
})
|
||||
);
|
||||
auto ts_index = std::distance(metadata.get_names().begin(),
|
||||
std::find_if(metadata.get_names().begin(), metadata.get_names().end(), [](const lw_shared_ptr<cql3::column_specification>& cdef) {
|
||||
return cdef->name->name() == timestamp_column_name;
|
||||
})
|
||||
);
|
||||
auto eor_index = std::distance(metadata.get_names().begin(),
|
||||
std::find_if(metadata.get_names().begin(), metadata.get_names().end(), [](const lw_shared_ptr<cql3::column_specification>& cdef) {
|
||||
return cdef->name->name() == eor_column_name;
|
||||
})
|
||||
);
|
||||
auto op_index = std::distance(metadata.get_names().begin(),
|
||||
std::find_if(metadata.get_names().begin(), metadata.get_names().end(), [](const lw_shared_ptr<cql3::column_specification>& cdef) {
|
||||
return cdef->name->name() == op_column_name;
|
||||
})
|
||||
);
|
||||
auto ts_index = std::distance(metadata.get_names().begin(),
|
||||
std::find_if(metadata.get_names().begin(), metadata.get_names().end(), [](const lw_shared_ptr<cql3::column_specification>& cdef) {
|
||||
return cdef->name->name() == timestamp_column_name;
|
||||
})
|
||||
);
|
||||
auto eor_index = std::distance(metadata.get_names().begin(),
|
||||
std::find_if(metadata.get_names().begin(), metadata.get_names().end(), [](const lw_shared_ptr<cql3::column_specification>& cdef) {
|
||||
return cdef->name->name() == eor_column_name;
|
||||
})
|
||||
);
|
||||
|
||||
std::optional<utils::UUID> timestamp;
|
||||
auto dynamodb = rjson::empty_object();
|
||||
auto record = rjson::empty_object();
|
||||
const auto dc_name = _proxy.get_token_metadata_ptr()->get_topology().get_datacenter();
|
||||
std::optional<utils::UUID> timestamp;
|
||||
auto dynamodb = rjson::empty_object();
|
||||
auto record = rjson::empty_object();
|
||||
|
||||
using op_utype = std::underlying_type_t<cdc::operation>;
|
||||
using op_utype = std::underlying_type_t<cdc::operation>;
|
||||
|
||||
auto maybe_add_record = [&] {
|
||||
if (!dynamodb.ObjectEmpty()) {
|
||||
rjson::add(record, "dynamodb", std::move(dynamodb));
|
||||
dynamodb = rjson::empty_object();
|
||||
}
|
||||
if (!record.ObjectEmpty()) {
|
||||
rjson::add(record, "awsRegion", rjson::from_string(dc_name));
|
||||
rjson::add(record, "eventID", event_id(iter.shard.id, *timestamp));
|
||||
rjson::add(record, "eventSource", "scylladb:alternator");
|
||||
rjson::add(record, "eventVersion", "1.1");
|
||||
rjson::push_back(records, std::move(record));
|
||||
record = rjson::empty_object();
|
||||
--limit;
|
||||
}
|
||||
};
|
||||
auto maybe_add_record = [&] {
|
||||
if (!dynamodb.ObjectEmpty()) {
|
||||
rjson::add(record, "dynamodb", std::move(dynamodb));
|
||||
dynamodb = rjson::empty_object();
|
||||
}
|
||||
if (!record.ObjectEmpty()) {
|
||||
// TODO: awsRegion?
|
||||
rjson::add(record, "eventID", event_id(iter.shard.id, *timestamp));
|
||||
rjson::add(record, "eventSource", "scylladb:alternator");
|
||||
rjson::push_back(records, std::move(record));
|
||||
record = rjson::empty_object();
|
||||
--limit;
|
||||
}
|
||||
};
|
||||
|
||||
for (auto& row : result_set->rows()) {
|
||||
auto op = static_cast<cdc::operation>(value_cast<op_utype>(data_type_for<op_utype>()->deserialize(*row[op_index])));
|
||||
auto ts = value_cast<utils::UUID>(data_type_for<utils::UUID>()->deserialize(*row[ts_index]));
|
||||
auto eor = row[eor_index].has_value() ? value_cast<bool>(boolean_type->deserialize(*row[eor_index])) : false;
|
||||
for (auto& row : result_set->rows()) {
|
||||
auto op = static_cast<cdc::operation>(value_cast<op_utype>(data_type_for<op_utype>()->deserialize(*row[op_index])));
|
||||
auto ts = value_cast<utils::UUID>(data_type_for<utils::UUID>()->deserialize(*row[ts_index]));
|
||||
auto eor = row[eor_index].has_value() ? value_cast<bool>(boolean_type->deserialize(*row[eor_index])) : false;
|
||||
|
||||
if (!dynamodb.HasMember("Keys")) {
|
||||
auto keys = rjson::empty_object();
|
||||
describe_single_item(*selection, row, key_names, keys);
|
||||
rjson::add(dynamodb, "Keys", std::move(keys));
|
||||
rjson::add(dynamodb, "ApproximateCreationDateTime", utils::UUID_gen::unix_timestamp_in_sec(ts).count());
|
||||
rjson::add(dynamodb, "SequenceNumber", sequence_number(ts));
|
||||
rjson::add(dynamodb, "StreamViewType", type);
|
||||
// TODO: SizeBytes
|
||||
}
|
||||
if (!dynamodb.HasMember("Keys")) {
|
||||
auto keys = rjson::empty_object();
|
||||
describe_single_item(*selection, row, key_names, keys);
|
||||
rjson::add(dynamodb, "Keys", std::move(keys));
|
||||
rjson::add(dynamodb, "ApproximateCreationDateTime", utils::UUID_gen::unix_timestamp_in_sec(ts).count());
|
||||
rjson::add(dynamodb, "SequenceNumber", sequence_number(ts));
|
||||
rjson::add(dynamodb, "StreamViewType", type);
|
||||
//TODO: SizeInBytes
|
||||
}
|
||||
|
||||
/**
|
||||
* We merge rows with same timestamp into a single event.
|
||||
* This is pretty much needed, because a CDC row typically
|
||||
* encodes ~half the info of an alternator write.
|
||||
*
|
||||
* A big, big downside to how alternator records are written
|
||||
* (i.e. CQL), is that the distinction between INSERT and UPDATE
|
||||
* is somewhat lost/unmappable to actual eventName.
|
||||
* A write (currently) always looks like an insert+modify
|
||||
* regardless whether we wrote existing record or not.
|
||||
*
|
||||
* Maybe RMW ops could be done slightly differently so
|
||||
* we can distinguish them here...
|
||||
*
|
||||
* For now, all writes will become MODIFY.
|
||||
*
|
||||
* Note: we do not check the current pre/post
|
||||
* flags on CDC log, instead we use data to
|
||||
* drive what is returned. This is (afaict)
|
||||
* consistent with dynamo streams
|
||||
*/
|
||||
switch (op) {
|
||||
case cdc::operation::pre_image:
|
||||
case cdc::operation::post_image:
|
||||
{
|
||||
auto item = rjson::empty_object();
|
||||
describe_single_item(*selection, row, attr_names, item, nullptr, true);
|
||||
describe_single_item(*selection, row, key_names, item);
|
||||
rjson::add(dynamodb, op == cdc::operation::pre_image ? "OldImage" : "NewImage", std::move(item));
|
||||
break;
|
||||
}
|
||||
case cdc::operation::update:
|
||||
rjson::add(record, "eventName", "MODIFY");
|
||||
break;
|
||||
case cdc::operation::insert:
|
||||
rjson::add(record, "eventName", "INSERT");
|
||||
break;
|
||||
case cdc::operation::service_row_delete:
|
||||
case cdc::operation::service_partition_delete:
|
||||
{
|
||||
auto user_identity = rjson::empty_object();
|
||||
rjson::add(user_identity, "Type", "Service");
|
||||
rjson::add(user_identity, "PrincipalId", "dynamodb.amazonaws.com");
|
||||
rjson::add(record, "userIdentity", std::move(user_identity));
|
||||
rjson::add(record, "eventName", "REMOVE");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
rjson::add(record, "eventName", "REMOVE");
|
||||
break;
|
||||
}
|
||||
if (eor) {
|
||||
maybe_add_record();
|
||||
timestamp = ts;
|
||||
if (limit == 0) {
|
||||
/**
|
||||
* We merge rows with same timestamp into a single event.
|
||||
* This is pretty much needed, because a CDC row typically
|
||||
* encodes ~half the info of an alternator write.
|
||||
*
|
||||
* A big, big downside to how alternator records are written
|
||||
* (i.e. CQL), is that the distinction between INSERT and UPDATE
|
||||
* is somewhat lost/unmappable to actual eventName.
|
||||
* A write (currently) always looks like an insert+modify
|
||||
* regardless whether we wrote existing record or not.
|
||||
*
|
||||
* Maybe RMW ops could be done slightly differently so
|
||||
* we can distinguish them here...
|
||||
*
|
||||
* For now, all writes will become MODIFY.
|
||||
*
|
||||
* Note: we do not check the current pre/post
|
||||
* flags on CDC log, instead we use data to
|
||||
* drive what is returned. This is (afaict)
|
||||
* consistent with dynamo streams
|
||||
*/
|
||||
switch (op) {
|
||||
case cdc::operation::pre_image:
|
||||
case cdc::operation::post_image:
|
||||
{
|
||||
auto item = rjson::empty_object();
|
||||
describe_single_item(*selection, row, attr_names, item, nullptr, true);
|
||||
describe_single_item(*selection, row, key_names, item);
|
||||
rjson::add(dynamodb, op == cdc::operation::pre_image ? "OldImage" : "NewImage", std::move(item));
|
||||
break;
|
||||
}
|
||||
case cdc::operation::update:
|
||||
rjson::add(record, "eventName", "MODIFY");
|
||||
break;
|
||||
case cdc::operation::insert:
|
||||
rjson::add(record, "eventName", "INSERT");
|
||||
break;
|
||||
default:
|
||||
rjson::add(record, "eventName", "REMOVE");
|
||||
break;
|
||||
}
|
||||
if (eor) {
|
||||
maybe_add_record();
|
||||
timestamp = ts;
|
||||
if (limit == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto ret = rjson::empty_object();
|
||||
auto nrecords = records.Size();
|
||||
rjson::add(ret, "Records", std::move(records));
|
||||
auto ret = rjson::empty_object();
|
||||
auto nrecords = records.Size();
|
||||
rjson::add(ret, "Records", std::move(records));
|
||||
|
||||
if (nrecords != 0) {
|
||||
// #9642. Set next iterators threshold to > last
|
||||
shard_iterator next_iter(iter.table, iter.shard, *timestamp, false);
|
||||
// Note that here we unconditionally return NextShardIterator,
|
||||
// without checking if maybe we reached the end-of-shard. If the
|
||||
// shard did end, then the next read will have nrecords == 0 and
|
||||
// will notice end end of shard and not return NextShardIterator.
|
||||
rjson::add(ret, "NextShardIterator", next_iter);
|
||||
_stats.api_operations.get_records_latency.mark(std::chrono::steady_clock::now() - start_time);
|
||||
co_return rjson::print(std::move(ret));
|
||||
}
|
||||
if (nrecords != 0) {
|
||||
// #9642. Set next iterators threshold to > last
|
||||
shard_iterator next_iter(iter.table, iter.shard, *timestamp, false);
|
||||
// Note that here we unconditionally return NextShardIterator,
|
||||
// without checking if maybe we reached the end-of-shard. If the
|
||||
// shard did end, then the next read will have nrecords == 0 and
|
||||
// will notice end end of shard and not return NextShardIterator.
|
||||
rjson::add(ret, "NextShardIterator", next_iter);
|
||||
_stats.api_operations.get_records_latency.mark(std::chrono::steady_clock::now() - start_time);
|
||||
return make_ready_future<executor::request_return_type>(make_jsonable(std::move(ret)));
|
||||
}
|
||||
|
||||
// ugh. figure out if we are and end-of-shard
|
||||
auto normal_token_owners = _proxy.get_token_metadata_ptr()->count_normal_token_owners();
|
||||
// ugh. figure out if we are and end-of-shard
|
||||
auto normal_token_owners = _proxy.get_token_metadata_ptr()->count_normal_token_owners();
|
||||
|
||||
db_clock::time_point ts = co_await _sdks.cdc_current_generation_timestamp({ normal_token_owners });
|
||||
auto& shard = iter.shard;
|
||||
return _sdks.cdc_current_generation_timestamp({ normal_token_owners }).then([this, iter, high_ts, start_time, ret = std::move(ret)](db_clock::time_point ts) mutable {
|
||||
auto& shard = iter.shard;
|
||||
|
||||
if (shard.time < ts && ts < high_ts) {
|
||||
// The DynamoDB documentation states that when a shard is
|
||||
// closed, reading it until the end has NextShardIterator
|
||||
// "set to null". Our test test_streams_closed_read
|
||||
// confirms that by "null" they meant not set at all.
|
||||
} else {
|
||||
// We could have return the same iterator again, but we did
|
||||
// a search from it until high_ts and found nothing, so we
|
||||
// can also start the next search from high_ts.
|
||||
// TODO: but why? It's simpler just to leave the iterator be.
|
||||
shard_iterator next_iter(iter.table, iter.shard, utils::UUID_gen::min_time_UUID(high_ts.time_since_epoch()), true);
|
||||
rjson::add(ret, "NextShardIterator", iter);
|
||||
}
|
||||
_stats.api_operations.get_records_latency.mark(std::chrono::steady_clock::now() - start_time);
|
||||
if (is_big(ret)) {
|
||||
co_return make_streamed(std::move(ret));
|
||||
}
|
||||
co_return rjson::print(std::move(ret));
|
||||
if (shard.time < ts && ts < high_ts) {
|
||||
// The DynamoDB documentation states that when a shard is
|
||||
// closed, reading it until the end has NextShardIterator
|
||||
// "set to null". Our test test_streams_closed_read
|
||||
// confirms that by "null" they meant not set at all.
|
||||
} else {
|
||||
// We could have return the same iterator again, but we did
|
||||
// a search from it until high_ts and found nothing, so we
|
||||
// can also start the next search from high_ts.
|
||||
// TODO: but why? It's simpler just to leave the iterator be.
|
||||
shard_iterator next_iter(iter.table, iter.shard, utils::UUID_gen::min_time_UUID(high_ts.time_since_epoch()), true);
|
||||
rjson::add(ret, "NextShardIterator", iter);
|
||||
}
|
||||
_stats.api_operations.get_records_latency.mark(std::chrono::steady_clock::now() - start_time);
|
||||
if (is_big(ret)) {
|
||||
return make_ready_future<executor::request_return_type>(make_streamed(std::move(ret)));
|
||||
}
|
||||
return make_ready_future<executor::request_return_type>(make_jsonable(std::move(ret)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bool executor::add_stream_options(const rjson::value& stream_specification, schema_builder& builder, service::storage_proxy& sp) {
|
||||
void executor::add_stream_options(const rjson::value& stream_specification, schema_builder& builder, service::storage_proxy& sp) {
|
||||
auto stream_enabled = rjson::find(stream_specification, "StreamEnabled");
|
||||
if (!stream_enabled || !stream_enabled->IsBool()) {
|
||||
throw api_error::validation("StreamSpecification needs boolean StreamEnabled");
|
||||
}
|
||||
|
||||
if (stream_enabled->GetBool()) {
|
||||
if (!sp.features().alternator_streams) {
|
||||
auto db = sp.data_dictionary();
|
||||
|
||||
if (!db.features().alternator_streams) {
|
||||
throw api_error::validation("StreamSpecification: alternator streams feature not enabled in cluster.");
|
||||
}
|
||||
|
||||
@@ -1106,12 +1083,10 @@ bool executor::add_stream_options(const rjson::value& stream_specification, sche
|
||||
break;
|
||||
}
|
||||
builder.with_cdc_options(opts);
|
||||
return true;
|
||||
} else {
|
||||
cdc::options opts;
|
||||
opts.enabled(false);
|
||||
builder.with_cdc_options(opts);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1140,4 +1115,4 @@ void executor::supplement_table_stream_info(rjson::value& descr, const schema& s
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace alternator
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/core/lowres_clock.hh>
|
||||
#include <seastar/coroutine/maybe_yield.hh>
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
|
||||
#include "cdc/log.hh"
|
||||
#include "exceptions/exceptions.hh"
|
||||
#include "gms/gossiper.hh"
|
||||
#include "gms/inet_address.hh"
|
||||
@@ -28,7 +28,7 @@
|
||||
#include "replica/database.hh"
|
||||
#include "service/client_state.hh"
|
||||
#include "service_permit.hh"
|
||||
#include "mutation/timestamp.hh"
|
||||
#include "timestamp.hh"
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "service/pager/paging_state.hh"
|
||||
#include "service/pager/query_pagers.hh"
|
||||
@@ -46,11 +46,9 @@
|
||||
#include "alternator/executor.hh"
|
||||
#include "alternator/controller.hh"
|
||||
#include "alternator/serialization.hh"
|
||||
#include "alternator/ttl_tag.hh"
|
||||
#include "dht/sharder.hh"
|
||||
#include "db/config.hh"
|
||||
#include "db/tags/utils.hh"
|
||||
#include "utils/labels.hh"
|
||||
|
||||
#include "ttl.hh"
|
||||
|
||||
@@ -58,10 +56,19 @@ static logging::logger tlogger("alternator_ttl");
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// We write the expiration-time attribute enabled on a table using a
|
||||
// tag TTL_TAG_KEY.
|
||||
// Currently, the *value* of this tag is simply the name of the attribute,
|
||||
// and the expiration scanner interprets it as an Alternator attribute name -
|
||||
// It can refer to a real column or if that doesn't exist, to a member of
|
||||
// the ":attrs" map column. Although this is designed for Alternator, it may
|
||||
// be good enough for CQL as well (there, the ":attrs" column won't exist).
|
||||
static const sstring TTL_TAG_KEY("system:ttl_attribute");
|
||||
|
||||
future<executor::request_return_type> executor::update_time_to_live(client_state& client_state, service_permit permit, rjson::value request) {
|
||||
_stats.api_operations.update_time_to_live++;
|
||||
if (!_proxy.features().alternator_ttl) {
|
||||
co_return api_error::unknown_operation("UpdateTimeToLive not yet supported. Upgrade all nodes to a version that supports it.");
|
||||
if (!_proxy.data_dictionary().features().alternator_ttl) {
|
||||
co_return api_error::unknown_operation("UpdateTimeToLive not yet supported. Experimental support is available if the 'alternator-ttl' experimental feature is enabled on all nodes.");
|
||||
}
|
||||
|
||||
schema_ptr schema = get_table(_proxy, request);
|
||||
@@ -74,6 +81,11 @@ future<executor::request_return_type> executor::update_time_to_live(client_state
|
||||
co_return api_error::validation("UpdateTimeToLive requires boolean Enabled");
|
||||
}
|
||||
bool enabled = v->GetBool();
|
||||
// Alternator TTL doesn't yet work when the table uses tablets (#16567)
|
||||
if (enabled && _proxy.local_db().find_keyspace(schema->ks_name()).get_replication_strategy().uses_tablets()) {
|
||||
co_return api_error::validation("TTL not yet supported on a table using tablets (issue #16567). "
|
||||
"Create a table with the tag 'experimental:initial_tablets' set to 'none' to use vnodes.");
|
||||
}
|
||||
v = rjson::find(*spec, "AttributeName");
|
||||
if (!v || !v->IsString()) {
|
||||
co_return api_error::validation("UpdateTimeToLive requires string AttributeName");
|
||||
@@ -85,9 +97,9 @@ future<executor::request_return_type> executor::update_time_to_live(client_state
|
||||
if (v->GetStringLength() < 1 || v->GetStringLength() > 255) {
|
||||
co_return api_error::validation("The length of AttributeName must be between 1 and 255");
|
||||
}
|
||||
sstring attribute_name = rjson::to_sstring(*v);
|
||||
sstring attribute_name(v->GetString(), v->GetStringLength());
|
||||
|
||||
co_await verify_permission(_enforce_authorization, _warn_authorization, client_state, schema, auth::permission::ALTER, _stats);
|
||||
co_await verify_permission(_enforce_authorization, client_state, schema, auth::permission::ALTER);
|
||||
co_await db::modify_tags(_mm, schema->ks_name(), schema->cf_name(), [&](std::map<sstring, sstring>& tags_map) {
|
||||
if (enabled) {
|
||||
if (tags_map.contains(TTL_TAG_KEY)) {
|
||||
@@ -111,7 +123,7 @@ future<executor::request_return_type> executor::update_time_to_live(client_state
|
||||
// basically identical to the request's
|
||||
rjson::value response = rjson::empty_object();
|
||||
rjson::add(response, "TimeToLiveSpecification", std::move(*spec));
|
||||
co_return rjson::print(std::move(response));
|
||||
co_return make_jsonable(std::move(response));
|
||||
}
|
||||
|
||||
future<executor::request_return_type> executor::describe_time_to_live(client_state& client_state, service_permit permit, rjson::value request) {
|
||||
@@ -128,12 +140,12 @@ future<executor::request_return_type> executor::describe_time_to_live(client_sta
|
||||
}
|
||||
rjson::value response = rjson::empty_object();
|
||||
rjson::add(response, "TimeToLiveDescription", std::move(desc));
|
||||
co_return rjson::print(std::move(response));
|
||||
co_return make_jsonable(std::move(response));
|
||||
}
|
||||
|
||||
// expiration_service is a sharded service responsible for cleaning up expired
|
||||
// items in all tables with per-item expiration enabled. Currently, this means
|
||||
// Alternator tables with TTL configured via an UpdateTimeToLive request.
|
||||
// Alternator tables with TTL configured via a UpdateTimeToLive request.
|
||||
//
|
||||
// Here is a brief overview of how the expiration service works:
|
||||
//
|
||||
@@ -279,18 +291,13 @@ static future<> expire_item(service::storage_proxy& proxy,
|
||||
auto ck = clustering_key::from_exploded(exploded_ck);
|
||||
m.partition().clustered_row(*schema, ck).apply(tombstone(ts, gc_clock::now()));
|
||||
}
|
||||
utils::chunked_vector<mutation> mutations;
|
||||
std::vector<mutation> mutations;
|
||||
mutations.push_back(std::move(m));
|
||||
return proxy.mutate(std::move(mutations),
|
||||
db::consistency_level::LOCAL_QUORUM,
|
||||
executor::default_timeout(), // FIXME - which timeout?
|
||||
qs.get_trace_state(), qs.get_permit(),
|
||||
db::allow_per_partition_rate_limit::no,
|
||||
false,
|
||||
cdc::per_request_options{
|
||||
.is_system_originated = true,
|
||||
}
|
||||
);
|
||||
db::allow_per_partition_rate_limit::no);
|
||||
}
|
||||
|
||||
static size_t random_offset(size_t min, size_t max) {
|
||||
@@ -308,19 +315,18 @@ static size_t random_offset(size_t min, size_t max) {
|
||||
// this range's primary node is down. For this we need to return not just
|
||||
// a list of this node's secondary ranges - but also the primary owner of
|
||||
// each of those ranges.
|
||||
//
|
||||
// The function is to be used with vnodes only
|
||||
static future<std::vector<std::pair<dht::token_range, locator::host_id>>> get_secondary_ranges(
|
||||
const locator::effective_replication_map* erm,
|
||||
const locator::effective_replication_map_ptr& erm,
|
||||
locator::host_id ep) {
|
||||
const auto& tm = *erm->get_token_metadata_ptr();
|
||||
const auto& sorted_tokens = tm.sorted_tokens();
|
||||
std::vector<std::pair<dht::token_range, locator::host_id>> ret;
|
||||
throwing_assert(!sorted_tokens.empty());
|
||||
if (sorted_tokens.empty()) {
|
||||
on_internal_error(tlogger, "Token metadata is empty");
|
||||
}
|
||||
auto prev_tok = sorted_tokens.back();
|
||||
for (const auto& tok : sorted_tokens) {
|
||||
co_await coroutine::maybe_yield();
|
||||
// FIXME: pass is_vnode=true to get_natural_replicas since the token is in tm.sorted_tokens()
|
||||
host_id_vector_replica_set eps = erm->get_natural_replicas(tok);
|
||||
if (eps.size() <= 1 || eps[1] != ep) {
|
||||
prev_tok = tok;
|
||||
@@ -390,7 +396,7 @@ class ranges_holder_primary {
|
||||
dht::token_range_vector _token_ranges;
|
||||
public:
|
||||
explicit ranges_holder_primary(dht::token_range_vector token_ranges) : _token_ranges(std::move(token_ranges)) {}
|
||||
static future<ranges_holder_primary> make(const locator::vnode_effective_replication_map* erm, locator::host_id ep) {
|
||||
static future<ranges_holder_primary> make(const locator::vnode_effective_replication_map_ptr& erm, locator::host_id ep) {
|
||||
co_return ranges_holder_primary(co_await erm->get_primary_ranges(ep));
|
||||
}
|
||||
std::size_t size() const { return _token_ranges.size(); }
|
||||
@@ -410,7 +416,7 @@ public:
|
||||
explicit ranges_holder_secondary(std::vector<std::pair<dht::token_range, locator::host_id>> token_ranges, const gms::gossiper& g)
|
||||
: _token_ranges(std::move(token_ranges))
|
||||
, _gossiper(g) {}
|
||||
static future<ranges_holder_secondary> make(const locator::vnode_effective_replication_map* erm, locator::host_id ep, const gms::gossiper& g) {
|
||||
static future<ranges_holder_secondary> make(const locator::effective_replication_map_ptr& erm, locator::host_id ep, const gms::gossiper& g) {
|
||||
co_return ranges_holder_secondary(co_await get_secondary_ranges(erm, ep), g);
|
||||
}
|
||||
std::size_t size() const { return _token_ranges.size(); }
|
||||
@@ -423,8 +429,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// The token_ranges_owned_by_this_shard class is only used for vnodes, where the vnodes give a partition range for the entire node
|
||||
// and such range still needs to be divided between the shards.
|
||||
template<class primary_or_secondary_t>
|
||||
class token_ranges_owned_by_this_shard {
|
||||
schema_ptr _s;
|
||||
@@ -518,7 +522,7 @@ struct scan_ranges_context {
|
||||
// should be possible (and a must for issue #7751!).
|
||||
lw_shared_ptr<service::pager::paging_state> paging_state = nullptr;
|
||||
auto regular_columns =
|
||||
s->regular_columns() | std::views::transform(&column_definition::id)
|
||||
s->regular_columns() | std::views::transform([] (const column_definition& cdef) { return cdef.id; })
|
||||
| std::ranges::to<query::column_id_vector>();
|
||||
selection = cql3::selection::selection::wildcard(s);
|
||||
query::partition_slice::option_set opts = selection->get_query_options();
|
||||
@@ -553,7 +557,7 @@ static future<> scan_table_ranges(
|
||||
expiration_service::stats& expiration_stats)
|
||||
{
|
||||
const schema_ptr& s = scan_ctx.s;
|
||||
throwing_assert(partition_ranges.size() == 1); // otherwise issue #9167 will cause incorrect results.
|
||||
SCYLLA_ASSERT (partition_ranges.size() == 1); // otherwise issue #9167 will cause incorrect results.
|
||||
auto p = service::pager::query_pagers::pager(proxy, s, scan_ctx.selection, *scan_ctx.query_state_ptr,
|
||||
*scan_ctx.query_options, scan_ctx.command, std::move(partition_ranges), nullptr);
|
||||
while (!p->is_exhausted()) {
|
||||
@@ -583,7 +587,7 @@ static future<> scan_table_ranges(
|
||||
if (retries >= 10) {
|
||||
// Don't get stuck forever asking the same page, maybe there's
|
||||
// a bug or a real problem in several replicas. Give up on
|
||||
// this scan and retry the scan from a random position later,
|
||||
// this scan an retry the scan from a random position later,
|
||||
// in the next scan period.
|
||||
throw runtime_exception("scanner thread failed after too many timeouts for the same page");
|
||||
}
|
||||
@@ -630,38 +634,13 @@ static future<> scan_table_ranges(
|
||||
}
|
||||
} else {
|
||||
// For a real column to contain an expiration time, it
|
||||
// must be a numeric type. We currently support decimal
|
||||
// (used by Alternator TTL) as well as bigint, int and
|
||||
// timestamp (used by CQL per-row TTL).
|
||||
switch (meta[*expiration_column]->type->get_kind()) {
|
||||
case abstract_type::kind::decimal:
|
||||
// Used by Alternator TTL for key columns not stored
|
||||
// in the map. The value is in seconds, fractional
|
||||
// part is ignored.
|
||||
expired = is_expired(value_cast<big_decimal>(v), now);
|
||||
break;
|
||||
case abstract_type::kind::long_kind:
|
||||
// Used by CQL per-row TTL. The value is in seconds.
|
||||
expired = is_expired(gc_clock::time_point(std::chrono::seconds(value_cast<int64_t>(v))), now);
|
||||
break;
|
||||
case abstract_type::kind::int32:
|
||||
// Used by CQL per-row TTL. The value is in seconds.
|
||||
// Using int type is not recommended because it will
|
||||
// overflow in 2038, but we support it to allow users
|
||||
// to use existing int columns for expiration.
|
||||
expired = is_expired(gc_clock::time_point(std::chrono::seconds(value_cast<int32_t>(v))), now);
|
||||
break;
|
||||
case abstract_type::kind::timestamp:
|
||||
// Used by CQL per-row TTL. The value is in milliseconds
|
||||
// but we truncate it to gc_clock's precision (whole seconds).
|
||||
expired = is_expired(gc_clock::time_point(std::chrono::duration_cast<gc_clock::duration>(value_cast<db_clock::time_point>(v).time_since_epoch())), now);
|
||||
break;
|
||||
default:
|
||||
// Should never happen - we verified the column's type
|
||||
// before starting the scan.
|
||||
[[unlikely]]
|
||||
on_internal_error(tlogger, format("expiration scanner value of unsupported type {} in column {}", meta[*expiration_column]->type->cql3_type_name(), scan_ctx.column_name) );
|
||||
}
|
||||
// must be a numeric type.
|
||||
// FIXME: Currently we only support decimal_type (which is
|
||||
// what Alternator uses), but other numeric types can be
|
||||
// supported as well to make this feature more useful in CQL.
|
||||
// Note that kind::decimal is also checked above.
|
||||
big_decimal n = value_cast<big_decimal>(v);
|
||||
expired = is_expired(n, now);
|
||||
}
|
||||
if (expired) {
|
||||
expiration_stats.items_deleted++;
|
||||
@@ -676,17 +655,6 @@ static future<> scan_table_ranges(
|
||||
}
|
||||
}
|
||||
|
||||
static future<> scan_tablet(locator::tablet_id tablet, service::storage_proxy& proxy, abort_source& abort_source, named_semaphore& page_sem,
|
||||
expiration_service::stats& expiration_stats, const scan_ranges_context& scan_ctx, const locator::tablet_map& tablet_map) {
|
||||
auto tablet_token_range = tablet_map.get_token_range(tablet);
|
||||
dht::ring_position tablet_start(tablet_token_range.start()->value(), dht::ring_position::token_bound::start),
|
||||
tablet_end(tablet_token_range.end()->value(), dht::ring_position::token_bound::end);
|
||||
auto partition_range = dht::partition_range::make(std::move(tablet_start), std::move(tablet_end));
|
||||
// Note that because of issue #9167 we need to run a separate query on each partition range, and can't pass
|
||||
// several of them into one partition_range_vector that is passed to scan_table_ranges().
|
||||
return scan_table_ranges(proxy, scan_ctx, {partition_range}, abort_source, page_sem, expiration_stats);
|
||||
}
|
||||
|
||||
// scan_table() scans, in one table, data "owned" by this shard, looking for
|
||||
// expired items and deleting them.
|
||||
// We consider each node to "own" its primary token ranges, i.e., the tokens
|
||||
@@ -723,12 +691,16 @@ static future<bool> scan_table(
|
||||
co_return false;
|
||||
}
|
||||
// attribute_name may be one of the schema's columns (in Alternator, this
|
||||
// means a key column, in CQL it's a regular column), or an element in
|
||||
// Alternator's attrs map encoded in Alternator's JSON encoding (which we
|
||||
// decode). If attribute_name is a real column, in Alternator it will have
|
||||
// the type decimal, counting seconds since the UNIX epoch, while in CQL
|
||||
// it will one of the types bigint or int (counting seconds) or timestamp
|
||||
// (counting milliseconds).
|
||||
// means it's a key column), or an element in Alternator's attrs map
|
||||
// encoded in Alternator's JSON encoding.
|
||||
// FIXME: To make this less Alternators-specific, we should encode in the
|
||||
// single key's value three things:
|
||||
// 1. The name of a column
|
||||
// 2. Optionally if column is a map, a member in the map
|
||||
// 3. The deserializer for the value: CQL or Alternator (JSON).
|
||||
// The deserializer can be guessed: If the given column or map item is
|
||||
// numeric, it can be used directly. If it is a "bytes" type, it needs to
|
||||
// be deserialized using Alternator's deserializer.
|
||||
bytes column_name = to_bytes(*attribute_name);
|
||||
const column_definition *cd = s->get_column_definition(column_name);
|
||||
std::optional<std::string> member;
|
||||
@@ -747,83 +719,45 @@ static future<bool> scan_table(
|
||||
data_type column_type = cd->type;
|
||||
// Verify that the column has the right type: If "member" exists
|
||||
// the column must be a map, and if it doesn't, the column must
|
||||
// be decimal_type (Alternator), bigint, int or timestamp (CQL).
|
||||
// If the column has the wrong type nothing can get expired in
|
||||
// this table, and it's pointless to scan it.
|
||||
// (currently) be a decimal_type. If the column has the wrong type
|
||||
// nothing can get expired in this table, and it's pointless to
|
||||
// scan it.
|
||||
if ((member && column_type->get_kind() != abstract_type::kind::map) ||
|
||||
(!member && column_type->get_kind() != abstract_type::kind::decimal &&
|
||||
column_type->get_kind() != abstract_type::kind::long_kind &&
|
||||
column_type->get_kind() != abstract_type::kind::int32 &&
|
||||
column_type->get_kind() != abstract_type::kind::timestamp)) {
|
||||
(!member && column_type->get_kind() != abstract_type::kind::decimal)) {
|
||||
tlogger.info("table {} TTL column has unsupported type, not scanning", s->cf_name());
|
||||
co_return false;
|
||||
}
|
||||
expiration_stats.scan_table++;
|
||||
// FIXME: need to pace the scan, not do it all at once.
|
||||
scan_ranges_context scan_ctx{s, proxy, std::move(column_name), std::move(member)};
|
||||
|
||||
if (s->table().uses_tablets()) {
|
||||
locator::effective_replication_map_ptr erm = s->table().get_effective_replication_map();
|
||||
auto my_host_id = erm->get_topology().my_host_id();
|
||||
const auto &tablet_map = erm->get_token_metadata().tablets().get_tablet_map(s->id());
|
||||
for (std::optional tablet = tablet_map.first_tablet(); tablet; tablet = tablet_map.next_tablet(*tablet)) {
|
||||
auto tablet_primary_replica = tablet_map.get_primary_replica(*tablet, erm->get_topology());
|
||||
// check if this is the primary replica for the current tablet
|
||||
if (tablet_primary_replica.host == my_host_id && tablet_primary_replica.shard == this_shard_id()) {
|
||||
co_await scan_tablet(*tablet, proxy, abort_source, page_sem, expiration_stats, scan_ctx, tablet_map);
|
||||
} else if(erm->get_replication_factor() > 1) {
|
||||
// Check if this is the secondary replica for the current tablet
|
||||
// and if the primary replica is down which means we will take over this work.
|
||||
// If each node only scans its own primary ranges, then when any node is
|
||||
// down part of the token range will not get scanned. This can be viewed
|
||||
// as acceptable (when the comes back online, it will resume its scan),
|
||||
// but as noted in issue #9787, we can allow more prompt expiration
|
||||
// by tasking another node to take over scanning of the dead node's primary
|
||||
// ranges. What we do here is that this node will also check expiration
|
||||
// on its *secondary* ranges - but only those whose primary owner is down.
|
||||
auto tablet_secondary_replica = tablet_map.get_secondary_replica(*tablet, erm->get_topology()); // throws if no secondary replica
|
||||
if (tablet_secondary_replica.host == my_host_id && tablet_secondary_replica.shard == this_shard_id()) {
|
||||
if (!gossiper.is_alive(tablet_primary_replica.host)) {
|
||||
co_await scan_tablet(*tablet, proxy, abort_source, page_sem, expiration_stats, scan_ctx, tablet_map);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // VNodes
|
||||
locator::static_effective_replication_map_ptr ermp =
|
||||
db.real_database().find_keyspace(s->ks_name()).get_static_effective_replication_map();
|
||||
auto* erm = ermp->maybe_as_vnode_effective_replication_map();
|
||||
if (!erm) {
|
||||
on_internal_error(tlogger, format("Keyspace {} is local", s->ks_name()));
|
||||
}
|
||||
auto my_host_id = erm->get_topology().my_host_id();
|
||||
token_ranges_owned_by_this_shard my_ranges(s, co_await ranges_holder_primary::make(erm, my_host_id));
|
||||
while (std::optional<dht::partition_range> range = my_ranges.next_partition_range()) {
|
||||
// Note that because of issue #9167 we need to run a separate
|
||||
// query on each partition range, and can't pass several of
|
||||
// them into one partition_range_vector.
|
||||
dht::partition_range_vector partition_ranges;
|
||||
partition_ranges.push_back(std::move(*range));
|
||||
// FIXME: if scanning a single range fails, including network errors,
|
||||
// we fail the entire scan (and rescan from the beginning). Need to
|
||||
// reconsider this. Saving the scan position might be a good enough
|
||||
// solution for this problem.
|
||||
co_await scan_table_ranges(proxy, scan_ctx, std::move(partition_ranges), abort_source, page_sem, expiration_stats);
|
||||
}
|
||||
// If each node only scans its own primary ranges, then when any node is
|
||||
// down part of the token range will not get scanned. This can be viewed
|
||||
// as acceptable (when the comes back online, it will resume its scan),
|
||||
// but as noted in issue #9787, we can allow more prompt expiration
|
||||
// by tasking another node to take over scanning of the dead node's primary
|
||||
// ranges. What we do here is that this node will also check expiration
|
||||
// on its *secondary* ranges - but only those whose primary owner is down.
|
||||
token_ranges_owned_by_this_shard my_secondary_ranges(s, co_await ranges_holder_secondary::make(erm, my_host_id, gossiper));
|
||||
while (std::optional<dht::partition_range> range = my_secondary_ranges.next_partition_range()) {
|
||||
expiration_stats.secondary_ranges_scanned++;
|
||||
dht::partition_range_vector partition_ranges;
|
||||
partition_ranges.push_back(std::move(*range));
|
||||
co_await scan_table_ranges(proxy, scan_ctx, std::move(partition_ranges), abort_source, page_sem, expiration_stats);
|
||||
}
|
||||
auto erm = db.real_database().find_keyspace(s->ks_name()).get_vnode_effective_replication_map();
|
||||
auto my_host_id = erm->get_topology().my_host_id();
|
||||
token_ranges_owned_by_this_shard my_ranges(s, co_await ranges_holder_primary::make(erm, my_host_id));
|
||||
while (std::optional<dht::partition_range> range = my_ranges.next_partition_range()) {
|
||||
// Note that because of issue #9167 we need to run a separate
|
||||
// query on each partition range, and can't pass several of
|
||||
// them into one partition_range_vector.
|
||||
dht::partition_range_vector partition_ranges;
|
||||
partition_ranges.push_back(std::move(*range));
|
||||
// FIXME: if scanning a single range fails, including network errors,
|
||||
// we fail the entire scan (and rescan from the beginning). Need to
|
||||
// reconsider this. Saving the scan position might be a good enough
|
||||
// solution for this problem.
|
||||
co_await scan_table_ranges(proxy, scan_ctx, std::move(partition_ranges), abort_source, page_sem, expiration_stats);
|
||||
}
|
||||
// If each node only scans its own primary ranges, then when any node is
|
||||
// down part of the token range will not get scanned. This can be viewed
|
||||
// as acceptable (when the comes back online, it will resume its scan),
|
||||
// but as noted in issue #9787, we can allow more prompt expiration
|
||||
// by tasking another node to take over scanning of the dead node's primary
|
||||
// ranges. What we do here is that this node will also check expiration
|
||||
// on its *secondary* ranges - but only those whose primary owner is down.
|
||||
token_ranges_owned_by_this_shard my_secondary_ranges(s, co_await ranges_holder_secondary::make(erm, my_host_id, gossiper));
|
||||
while (std::optional<dht::partition_range> range = my_secondary_ranges.next_partition_range()) {
|
||||
expiration_stats.secondary_ranges_scanned++;
|
||||
dht::partition_range_vector partition_ranges;
|
||||
partition_ranges.push_back(std::move(*range));
|
||||
co_await scan_table_ranges(proxy, scan_ctx, std::move(partition_ranges), abort_source, page_sem, expiration_stats);
|
||||
}
|
||||
co_return true;
|
||||
}
|
||||
@@ -892,10 +826,12 @@ future<> expiration_service::run() {
|
||||
future<> expiration_service::start() {
|
||||
// Called by main() on each shard to start the expiration-service
|
||||
// thread. Just runs run() in the background and allows stop().
|
||||
if (!shutting_down()) {
|
||||
_end = run().handle_exception([] (std::exception_ptr ep) {
|
||||
tlogger.error("expiration_service failed: {}", ep);
|
||||
});
|
||||
if (_db.features().alternator_ttl) {
|
||||
if (!shutting_down()) {
|
||||
_end = run().handle_exception([] (std::exception_ptr ep) {
|
||||
tlogger.error("expiration_service failed: {}", ep);
|
||||
});
|
||||
}
|
||||
}
|
||||
return make_ready_future<>();
|
||||
}
|
||||
@@ -915,13 +851,13 @@ future<> expiration_service::stop() {
|
||||
expiration_service::stats::stats() {
|
||||
_metrics.add_group("expiration", {
|
||||
seastar::metrics::make_total_operations("scan_passes", scan_passes,
|
||||
seastar::metrics::description("number of passes over the database"))(alternator_label).set_skip_when_empty(),
|
||||
seastar::metrics::description("number of passes over the database")),
|
||||
seastar::metrics::make_total_operations("scan_table", scan_table,
|
||||
seastar::metrics::description("number of table scans (counting each scan of each table that enabled expiration)"))(alternator_label).set_skip_when_empty(),
|
||||
seastar::metrics::description("number of table scans (counting each scan of each table that enabled expiration)")),
|
||||
seastar::metrics::make_total_operations("items_deleted", items_deleted,
|
||||
seastar::metrics::description("number of items deleted after expiration"))(basic_level)(alternator_label).set_skip_when_empty(),
|
||||
seastar::metrics::description("number of items deleted after expiration")),
|
||||
seastar::metrics::make_total_operations("secondary_ranges_scanned", secondary_ranges_scanned,
|
||||
seastar::metrics::description("number of token ranges scanned by this node while their primary owner was down"))(alternator_label).set_skip_when_empty(),
|
||||
seastar::metrics::description("number of token ranges scanned by this node while their primary owner was down")),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace alternator {
|
||||
|
||||
// expiration_service is a sharded service responsible for cleaning up expired
|
||||
// items in all tables with per-item expiration enabled. Currently, this means
|
||||
// Alternator tables with TTL configured via an UpdateTimeToLive request.
|
||||
// Alternator tables with TTL configured via a UpdateTimeToLeave request.
|
||||
class expiration_service final : public seastar::peering_sharded_service<expiration_service> {
|
||||
public:
|
||||
// Object holding per-shard statistics related to the expiration service.
|
||||
@@ -52,7 +52,7 @@ private:
|
||||
data_dictionary::database _db;
|
||||
service::storage_proxy& _proxy;
|
||||
gms::gossiper& _gossiper;
|
||||
// _end is set by start(), and resolves when the background service
|
||||
// _end is set by start(), and resolves when the the background service
|
||||
// started by it ends. To ask the background service to end, _abort_source
|
||||
// should be triggered. stop() below uses both _abort_source and _end.
|
||||
std::optional<future<>> _end;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026-present ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "seastarx.hh"
|
||||
#include <seastar/core/sstring.hh>
|
||||
|
||||
namespace alternator {
|
||||
// We use the table tag TTL_TAG_KEY ("system:ttl_attribute") to remember
|
||||
// which attribute was chosen as the expiration-time attribute for
|
||||
// Alternator's TTL and CQL's per-row TTL features.
|
||||
// Currently, the *value* of this tag is simply the name of the attribute:
|
||||
// It can refer to a real column or if that doesn't exist, to a member of
|
||||
// the ":attrs" map column (which Alternator uses).
|
||||
extern const sstring TTL_TAG_KEY;
|
||||
} // namespace alternator
|
||||
|
||||
// let users use TTL_TAG_KEY without the "alternator::" prefix,
|
||||
// to make it easier to move it to a different namespace later.
|
||||
using alternator::TTL_TAG_KEY;
|
||||
@@ -31,7 +31,6 @@ set(swagger_files
|
||||
api-doc/column_family.json
|
||||
api-doc/commitlog.json
|
||||
api-doc/compaction_manager.json
|
||||
api-doc/client_routes.json
|
||||
api-doc/config.json
|
||||
api-doc/cql_server_test.json
|
||||
api-doc/endpoint_snitch_info.json
|
||||
@@ -43,7 +42,6 @@ set(swagger_files
|
||||
api-doc/messaging_service.json
|
||||
api-doc/metrics.json
|
||||
api-doc/raft.json
|
||||
api-doc/service_levels.json
|
||||
api-doc/storage_proxy.json
|
||||
api-doc/storage_service.json
|
||||
api-doc/stream_manager.json
|
||||
@@ -69,7 +67,6 @@ target_sources(api
|
||||
PRIVATE
|
||||
api.cc
|
||||
cache_service.cc
|
||||
client_routes.cc
|
||||
collectd.cc
|
||||
column_family.cc
|
||||
commitlog.cc
|
||||
@@ -85,7 +82,6 @@ target_sources(api
|
||||
lsa.cc
|
||||
messaging_service.cc
|
||||
raft.cc
|
||||
service_levels.cc
|
||||
storage_proxy.cc
|
||||
storage_service.cc
|
||||
stream_manager.cc
|
||||
@@ -108,8 +104,5 @@ target_link_libraries(api
|
||||
wasmtime_bindings
|
||||
absl::headers)
|
||||
|
||||
if (Scylla_USE_PRECOMPILED_HEADER_USE)
|
||||
target_precompile_headers(api REUSE_FROM scylla-precompiled-header)
|
||||
endif()
|
||||
check_headers(check-headers api
|
||||
GLOB_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/*.hh)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Resets authorized prepared statements cache",
|
||||
"summary":"Reset cache",
|
||||
"type":"void",
|
||||
"nickname":"authorization_cache_reset",
|
||||
"produces":[
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
, "client_routes_entry": {
|
||||
"id": "client_routes_entry",
|
||||
"summary": "An entry storing client routes",
|
||||
"properties": {
|
||||
"connection_id": {"type": "string"},
|
||||
"host_id": {"type": "string", "format": "uuid"},
|
||||
"address": {"type": "string"},
|
||||
"port": {"type": "integer"},
|
||||
"tls_port": {"type": "integer"},
|
||||
"alternator_port": {"type": "integer"},
|
||||
"alternator_https_port": {"type": "integer"}
|
||||
},
|
||||
"required": ["connection_id", "host_id", "address"]
|
||||
}
|
||||
, "client_routes_key": {
|
||||
"id": "client_routes_key",
|
||||
"summary": "A key of client_routes_entry",
|
||||
"properties": {
|
||||
"connection_id": {"type": "string"},
|
||||
"host_id": {"type": "string", "format": "uuid"}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
, "/v2/client-routes":{
|
||||
"get": {
|
||||
"description":"List all client route entries",
|
||||
"operationId":"get_client_routes",
|
||||
"tags":["client_routes"],
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[],
|
||||
"responses":{
|
||||
"200":{
|
||||
"schema":{
|
||||
"type":"array",
|
||||
"items":{ "$ref":"#/definitions/client_routes_entry" }
|
||||
}
|
||||
},
|
||||
"default":{
|
||||
"description":"unexpected error",
|
||||
"schema":{"$ref":"#/definitions/ErrorModel"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description":"Upsert one or more client route entries",
|
||||
"operationId":"set_client_routes",
|
||||
"tags":["client_routes"],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"body",
|
||||
"in":"body",
|
||||
"required":true,
|
||||
"schema":{
|
||||
"type":"array",
|
||||
"items":{ "$ref":"#/definitions/client_routes_entry" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses":{
|
||||
"200":{ "description": "OK" },
|
||||
"default":{
|
||||
"description":"unexpected error",
|
||||
"schema":{ "$ref":"#/definitions/ErrorModel" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description":"Delete one or more client route entries",
|
||||
"operationId":"delete_client_routes",
|
||||
"tags":["client_routes"],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"body",
|
||||
"in":"body",
|
||||
"required":true,
|
||||
"schema":{
|
||||
"type":"array",
|
||||
"items":{ "$ref":"#/definitions/client_routes_key" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses":{
|
||||
"200":{
|
||||
"description": "OK"
|
||||
},
|
||||
"default":{
|
||||
"description":"unexpected error",
|
||||
"schema":{
|
||||
"$ref":"#/definitions/ErrorModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,24 +246,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sstableinfo":{
|
||||
"id":"sstableinfo",
|
||||
"description":"Compacted sstable information",
|
||||
"properties":{
|
||||
"generation":{
|
||||
"type": "string",
|
||||
"description":"Generation of the sstable"
|
||||
},
|
||||
"origin":{
|
||||
"type":"string",
|
||||
"description":"Origin of the sstable"
|
||||
},
|
||||
"size":{
|
||||
"type":"long",
|
||||
"description":"Size of the sstable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"compaction_info" :{
|
||||
"id": "compaction_info",
|
||||
"description":"A key value mapping",
|
||||
@@ -345,10 +327,6 @@
|
||||
"type":"string",
|
||||
"description":"The UUID"
|
||||
},
|
||||
"shard_id":{
|
||||
"type":"int",
|
||||
"description":"The shard id the compaction was executed on"
|
||||
},
|
||||
"cf":{
|
||||
"type":"string",
|
||||
"description":"The column family name"
|
||||
@@ -357,17 +335,9 @@
|
||||
"type":"string",
|
||||
"description":"The keyspace name"
|
||||
},
|
||||
"compaction_type":{
|
||||
"type":"string",
|
||||
"description":"Type of compaction"
|
||||
},
|
||||
"started_at":{
|
||||
"type":"long",
|
||||
"description":"The time compaction started"
|
||||
},
|
||||
"compacted_at":{
|
||||
"type":"long",
|
||||
"description":"The time compaction completed"
|
||||
"description":"The time of compaction"
|
||||
},
|
||||
"bytes_in":{
|
||||
"type":"long",
|
||||
@@ -383,32 +353,6 @@
|
||||
"type":"row_merged"
|
||||
},
|
||||
"description":"The merged rows"
|
||||
},
|
||||
"sstables_in": {
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"sstableinfo"
|
||||
},
|
||||
"description":"List of input sstables for compaction"
|
||||
},
|
||||
"sstables_out": {
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"sstableinfo"
|
||||
},
|
||||
"description":"List of output sstables from compaction"
|
||||
},
|
||||
"total_tombstone_purge_attempt":{
|
||||
"type":"long",
|
||||
"description":"Total number of tombstone purge attempts"
|
||||
},
|
||||
"total_tombstone_purge_failure_due_to_overlapping_with_memtable":{
|
||||
"type":"long",
|
||||
"description":"Number of tombstone purge failures due to data overlapping with memtables"
|
||||
},
|
||||
"total_tombstone_purge_failure_due_to_overlapping_with_uncompacting_sstable":{
|
||||
"type":"long",
|
||||
"description":"Number of tombstone purge failures due to data overlapping with non-compacting sstables"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,14 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
},
|
||||
{
|
||||
"name":"unsafe",
|
||||
"description":"Set to True to perform an unsafe assassination",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
"GOSSIP_DIGEST_SYN",
|
||||
"GOSSIP_DIGEST_ACK2",
|
||||
"GOSSIP_SHUTDOWN",
|
||||
"UNUSED__DEFINITIONS_UPDATE",
|
||||
"DEFINITIONS_UPDATE",
|
||||
"TRUNCATE",
|
||||
"UNUSED__REPLICATION_FINISHED",
|
||||
"MIGRATION_REQUEST",
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"apiVersion":"0.0.1",
|
||||
"swaggerVersion":"1.2",
|
||||
"basePath":"{{Protocol}}://{{Host}}",
|
||||
"resourcePath":"/service_levels",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"apis":[
|
||||
{
|
||||
"path":"/service_levels/switch_tenants",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Switch tenants on all opened connections if needed",
|
||||
"type":"void",
|
||||
"nickname":"do_switch_tenants",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/service_levels/count_connections",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Count opened CQL connections per scheduling group per user",
|
||||
"type":"connections_count_map",
|
||||
"nickname":"count_connections",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models":{},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"connections_count_map": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -220,25 +220,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/nodes/excluded",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Retrieve host ids of nodes which are marked as excluded",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"string"
|
||||
},
|
||||
"nickname":"get_excluded_nodes",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/nodes/joining",
|
||||
"operations":[
|
||||
@@ -613,50 +594,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_service/natural_endpoints/v2/{keyspace}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary":"This method returns the N endpoints that are responsible for storing the specified key i.e for replication. the endpoint responsible for this key",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nickname": "get_natural_endpoints_v2",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "keyspace",
|
||||
"description": "The keyspace to query about.",
|
||||
"required": true,
|
||||
"allowMultiple": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "cf",
|
||||
"description": "Column family name.",
|
||||
"required": true,
|
||||
"allowMultiple": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "key_component",
|
||||
"description": "Each component of the key for which we need to find the endpoint (e.g. ?key_component=part1&key_component=part2).",
|
||||
"required": true,
|
||||
"allowMultiple": true,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"path":"/storage_service/cdc_streams_check_and_repair",
|
||||
"operations":[
|
||||
@@ -876,14 +813,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"move_files",
|
||||
"description":"Move component files instead of copying them",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -952,23 +881,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"scope",
|
||||
"description":"Defines the set of nodes to which mutations can be streamed",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query",
|
||||
"enum": ["all", "dc", "rack", "node"]
|
||||
},
|
||||
{
|
||||
"name":"primary_replica_only",
|
||||
"description":"Load the sstables and stream to the primary replica node within the scope, if one is specified. If not, stream to the global primary replica.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1055,7 +967,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/cleanup_all/",
|
||||
"path":"/storage_service/cleanup_all",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
@@ -1065,30 +977,6 @@
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"global",
|
||||
"description":"true if cleanup of entire cluster is requested",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/mark_node_as_clean",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Mark the node as clean. After that the node will not be considered as needing cleanup during automatic cleanup which is triggered by some topology operations",
|
||||
"type":"void",
|
||||
"nickname":"reset_cleanup_needed",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
@@ -1195,14 +1083,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name": "drop_unfixable_sstables",
|
||||
"description": "When set to true, drop unfixable sstables. Applies only to scrub mode SEGREGATE.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1295,45 +1175,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/logstor_compaction",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Trigger compaction of the key-value storage",
|
||||
"type":"void",
|
||||
"nickname":"logstor_compaction",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"major",
|
||||
"description":"When true, perform a major compaction",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/logstor_flush",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Trigger flush of logstor storage",
|
||||
"type":"void",
|
||||
"nickname":"logstor_flush",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/active_repair/",
|
||||
"operations":[
|
||||
@@ -1661,30 +1502,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/exclude_node",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Marks the node as permanently down (excluded).",
|
||||
"type":"void",
|
||||
"nickname":"exclude_node",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"hosts",
|
||||
"description":"Comma-separated list of host ids to exclude",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/removal_status",
|
||||
"operations":[
|
||||
@@ -1822,6 +1639,38 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/truncate/{keyspace}",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Truncates (deletes) the given columnFamily from the provided keyspace. Calling truncate results in actual deletion of all data in the cluster under the given columnFamily and it will fail unless all hosts are up. All data in the given column family will be deleted, but its definition will not be affected.",
|
||||
"type":"void",
|
||||
"nickname":"truncate",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"keyspace",
|
||||
"description":"The keyspace",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
},
|
||||
{
|
||||
"name":"cf",
|
||||
"description":"Column family name",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/keyspaces",
|
||||
"operations":[
|
||||
@@ -2310,31 +2159,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"skip_cleanup",
|
||||
"description":"Don't cleanup keys from loaded sstables. Invalid if load_and_stream is true",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"skip_reshape",
|
||||
"description":"Don't reshape the loaded sstables. Invalid if load_and_stream is true",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"scope",
|
||||
"description":"Defines the set of nodes to which mutations can be streamed",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query",
|
||||
"enum": ["all", "dc", "rack", "node"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3035,7 +2859,7 @@
|
||||
"nickname":"repair_tablet",
|
||||
"method":"POST",
|
||||
"summary":"Repair a tablet",
|
||||
"type":"tablet_repair_result",
|
||||
"type":"void",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
@@ -3063,38 +2887,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"hosts_filter",
|
||||
"description":"Repair replicas listed in the comma-separated host_id list.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"dcs_filter",
|
||||
"description":"Repair replicas listed in the comma-separated DC list",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"await_completion",
|
||||
"description":"Set true to wait for the repair to complete. Set false to skip waiting for the repair to complete. When the option is not provided, it defaults to false.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"incremental_mode",
|
||||
"description":"Set the incremental repair mode. Can be 'disabled', 'incremental', or 'full'. 'incremental': The incremental repair logic is enabled. Unrepaired sstables will be included for repair. Repaired sstables will be skipped. The incremental repair states will be updated after repair. 'full': The incremental repair logic is enabled. Both repaired and unrepaired sstables will be included for repair. The incremental repair states will be updated after repair. 'disabled': The incremental repair logic is disabled completely. The incremental repair states, e.g., repaired_at in sstables and sstables_repaired_at in the system.tablets table, will not be updated after repair. When the option is not provided, it defaults to incremental mode.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3124,48 +2916,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"path":"/storage_service/tablets/snapshots",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Takes the snapshot for the given keyspaces/tables. A snapshot name must be specified.",
|
||||
"type":"void",
|
||||
"nickname":"take_cluster_snapshot",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"tag",
|
||||
"description":"the tag given to the snapshot",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"keyspace",
|
||||
"description":"Keyspace(s) to snapshot. Multiple keyspaces can be provided using a comma-separated list. If omitted, snapshot all keyspaces.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"table",
|
||||
"description":"Table(s) to snapshot. Multiple tables (in a single keyspace) can be provided using a comma-separated list. If omitted, snapshot all tables in the given keyspace(s).",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"path":"/storage_service/quiesce_topology",
|
||||
"operations":[
|
||||
@@ -3268,105 +3018,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/logstor_info",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Logstor segment information for one table",
|
||||
"type":"table_logstor_info",
|
||||
"nickname":"logstor_info",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"keyspace",
|
||||
"description":"The keyspace",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"table",
|
||||
"description":"table name",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/retrain_dict",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Retrain the SSTable compression dictionary for the target table.",
|
||||
"type":"void",
|
||||
"nickname":"retrain_dict",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"keyspace",
|
||||
"description":"Name of the keyspace containing the target table.",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"cf",
|
||||
"description":"Name of the target table.",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/estimate_compression_ratios",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Compute an estimated compression ratio for SSTables of the given table, for various compression configurations.",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"compression_config_result"
|
||||
},
|
||||
"nickname":"estimate_compression_ratios",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"keyspace",
|
||||
"description":"Name of the keyspace containing the target table.",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"cf",
|
||||
"description":"Name of the target table.",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/raft_topology/reload",
|
||||
"operations":[
|
||||
@@ -3409,54 +3060,6 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/raft_topology/cmd_rpc_status",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get information about currently running topology cmd rpc",
|
||||
"type":"string",
|
||||
"nickname":"raft_topology_get_cmd_status",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/drop_quarantined_sstables",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Drops all quarantined sstables in all keyspaces or specified keyspace and tables",
|
||||
"type":"void",
|
||||
"nickname":"drop_quarantined_sstables",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"keyspace",
|
||||
"description":"The keyspace name to drop quarantined sstables from.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"tables",
|
||||
"description":"Comma-separated table names to drop quarantined sstables from.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models":{
|
||||
@@ -3593,11 +3196,11 @@
|
||||
"properties":{
|
||||
"start_token":{
|
||||
"type":"string",
|
||||
"description":"The range start token (exclusive)"
|
||||
"description":"The range start token"
|
||||
},
|
||||
"end_token":{
|
||||
"type":"string",
|
||||
"description":"The range end token (inclusive)"
|
||||
"description":"The range start token"
|
||||
},
|
||||
"endpoints":{
|
||||
"type":"array",
|
||||
@@ -3670,7 +3273,7 @@
|
||||
"version":{
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"ka", "la", "mc", "md", "me", "ms"
|
||||
"ka", "la", "mc", "md", "me"
|
||||
],
|
||||
"description":"SSTable version"
|
||||
},
|
||||
@@ -3707,82 +3310,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logstor_hist_bucket":{
|
||||
"id":"logstor_hist_bucket",
|
||||
"properties":{
|
||||
"bucket":{
|
||||
"type":"long"
|
||||
},
|
||||
"count":{
|
||||
"type":"long"
|
||||
},
|
||||
"min_data_size":{
|
||||
"type":"long"
|
||||
},
|
||||
"max_data_size":{
|
||||
"type":"long"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table_logstor_info":{
|
||||
"id":"table_logstor_info",
|
||||
"description":"Per-table logstor segment distribution",
|
||||
"properties":{
|
||||
"keyspace":{
|
||||
"type":"string"
|
||||
},
|
||||
"table":{
|
||||
"type":"string"
|
||||
},
|
||||
"compaction_groups":{
|
||||
"type":"long"
|
||||
},
|
||||
"segments":{
|
||||
"type":"long"
|
||||
},
|
||||
"data_size_histogram":{
|
||||
"type":"array",
|
||||
"items":{
|
||||
"$ref":"logstor_hist_bucket"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tablet_repair_result":{
|
||||
"id":"tablet_repair_result",
|
||||
"description":"Tablet repair result",
|
||||
"properties":{
|
||||
"tablet_task_id":{
|
||||
"type":"string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"compression_config_result":{
|
||||
"id":"compression_config_result",
|
||||
"description":"Compression ratio estimation result for one config",
|
||||
"properties":{
|
||||
"level":{
|
||||
"type":"long",
|
||||
"description":"The used value of `compression_level`"
|
||||
},
|
||||
"chunk_length_in_kb":{
|
||||
"type":"long",
|
||||
"description":"The used value of `chunk_length_in_kb`"
|
||||
},
|
||||
"dict":{
|
||||
"type":"string",
|
||||
"description":"The used dictionary: `none`, `past` (== current), or `future`"
|
||||
},
|
||||
"sstable_compression":{
|
||||
"type":"string",
|
||||
"description":"The used compressor name (aka `sstable_compression`)"
|
||||
},
|
||||
"ratio":{
|
||||
"type":"float",
|
||||
"description":"The resulting compression ratio (estimated on a random sample of files)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,21 +209,6 @@
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/system/chosen_sstable_version",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get sstable version currently chosen for use in new sstables",
|
||||
"type":"string",
|
||||
"nickname":"get_chosen_sstable_version",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -253,30 +253,6 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/task_manager/drain/{module}",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Drain finished local tasks",
|
||||
"type":"void",
|
||||
"nickname":"drain_tasks",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"module",
|
||||
"description":"The module to drain",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models":{
|
||||
@@ -308,8 +284,7 @@
|
||||
"created",
|
||||
"running",
|
||||
"done",
|
||||
"failed",
|
||||
"suspended"
|
||||
"failed"
|
||||
],
|
||||
"description":"The state of a task"
|
||||
},
|
||||
@@ -344,18 +319,6 @@
|
||||
"sequence_number":{
|
||||
"type":"long",
|
||||
"description":"The running sequence number of the task"
|
||||
},
|
||||
"shard":{
|
||||
"type":"long",
|
||||
"description":"The shard the task is running on"
|
||||
},
|
||||
"start_time":{
|
||||
"type":"datetime",
|
||||
"description":"The start time of the task; unspecified (equal to epoch) when state == created"
|
||||
},
|
||||
"end_time":{
|
||||
"type":"datetime",
|
||||
"description":"The end time of the task; unspecified (equal to epoch) when the task is not completed"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -389,8 +352,7 @@
|
||||
"created",
|
||||
"running",
|
||||
"done",
|
||||
"failed",
|
||||
"suspended"
|
||||
"failed"
|
||||
],
|
||||
"description":"The state of the task"
|
||||
},
|
||||
@@ -447,7 +409,7 @@
|
||||
"description":"The number of units completed so far"
|
||||
},
|
||||
"children_ids":{
|
||||
"type":"chunked_array",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"task_identity"
|
||||
},
|
||||
|
||||
@@ -42,14 +42,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"consider_only_existing_data",
|
||||
"description":"Set to \"true\" to flush all memtables and force tombstone garbage collection to check only the sstables being compacted (false by default). The memtable, commitlog and other uncompacted sstables will not be checked during tombstone garbage collection.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
111
api/api.cc
111
api/api.cc
@@ -36,8 +36,6 @@
|
||||
#include "tasks.hh"
|
||||
#include "raft.hh"
|
||||
#include "gms/gossip_address_map.hh"
|
||||
#include "service_levels.hh"
|
||||
#include "client_routes.hh"
|
||||
|
||||
logging::logger apilog("api");
|
||||
|
||||
@@ -68,11 +66,9 @@ future<> set_server_init(http_context& ctx) {
|
||||
rb02->set_api_doc(r);
|
||||
rb02->register_api_file(r, "swagger20_header");
|
||||
rb02->register_api_file(r, "metrics");
|
||||
rb02->register_api_file(r, "client_routes");
|
||||
rb->register_function(r, "system",
|
||||
"The system related API");
|
||||
rb02->add_definitions_file(r, "metrics");
|
||||
rb02->add_definitions_file(r, "client_routes");
|
||||
set_system(ctx, r);
|
||||
rb->register_function(r, "error_injection",
|
||||
"The error injection API");
|
||||
@@ -84,7 +80,7 @@ future<> set_server_init(http_context& ctx) {
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_server_config(http_context& ctx, db::config& cfg) {
|
||||
future<> set_server_config(http_context& ctx, const db::config& cfg) {
|
||||
auto rb02 = std::make_shared < api_registry_builder20 > (ctx.api_doc, "/v2");
|
||||
return ctx.http_server.set_routes([&ctx, &cfg, rb02](routes& r) {
|
||||
set_config(rb02, ctx, r, cfg, false);
|
||||
@@ -122,9 +118,9 @@ future<> unset_thrift_controller(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_thrift_controller(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_storage_service(http_context& ctx, sharded<service::storage_service>& ss, sharded<db::snapshot_ctl>& ssc, service::raft_group0_client& group0_client) {
|
||||
return ctx.http_server.set_routes([&ctx, &ss, &ssc, &group0_client] (routes& r) {
|
||||
set_storage_service(ctx, r, ss, ssc, group0_client);
|
||||
future<> set_server_storage_service(http_context& ctx, sharded<service::storage_service>& ss, service::raft_group0_client& group0_client) {
|
||||
return ctx.http_server.set_routes([&ctx, &ss, &group0_client] (routes& r) {
|
||||
set_storage_service(ctx, r, ss, group0_client);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,16 +128,6 @@ future<> unset_server_storage_service(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_storage_service(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_client_routes(http_context& ctx, sharded<service::client_routes_service>& cr) {
|
||||
return ctx.http_server.set_routes([&ctx, &cr] (routes& r) {
|
||||
set_client_routes(ctx, r, cr);
|
||||
});
|
||||
}
|
||||
|
||||
future<> unset_server_client_routes(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_client_routes(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_load_meter(http_context& ctx, service::load_meter& lm) {
|
||||
return ctx.http_server.set_routes([&ctx, &lm] (routes& r) { set_load_meter(ctx, r, lm); });
|
||||
}
|
||||
@@ -150,6 +136,14 @@ future<> unset_load_meter(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_load_meter(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_format_selector(http_context& ctx, db::sstables_format_selector& sel) {
|
||||
return ctx.http_server.set_routes([&ctx, &sel] (routes& r) { set_format_selector(ctx, r, sel); });
|
||||
}
|
||||
|
||||
future<> unset_format_selector(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_format_selector(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_sstables_loader(http_context& ctx, sharded<sstables_loader>& sst_loader) {
|
||||
return ctx.http_server.set_routes([&ctx, &sst_loader] (routes& r) { set_sstables_loader(ctx, r, sst_loader); });
|
||||
}
|
||||
@@ -158,8 +152,8 @@ future<> unset_server_sstables_loader(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_sstables_loader(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_view_builder(http_context& ctx, sharded<db::view::view_builder>& vb, sharded<gms::gossiper>& g) {
|
||||
return ctx.http_server.set_routes([&ctx, &vb, &g] (routes& r) { set_view_builder(ctx, r, vb, g); });
|
||||
future<> set_server_view_builder(http_context& ctx, sharded<db::view::view_builder>& vb) {
|
||||
return ctx.http_server.set_routes([&ctx, &vb] (routes& r) { set_view_builder(ctx, r, vb); });
|
||||
}
|
||||
|
||||
future<> unset_server_view_builder(http_context& ctx) {
|
||||
@@ -193,8 +187,8 @@ future<> unset_server_snapshot(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_snapshot(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_token_metadata(http_context& ctx, sharded<locator::shared_token_metadata>& tm, sharded<gms::gossiper>& g) {
|
||||
return ctx.http_server.set_routes([&ctx, &tm, &g] (routes& r) { set_token_metadata(ctx, r, tm, g); });
|
||||
future<> set_server_token_metadata(http_context& ctx, sharded<locator::shared_token_metadata>& tm) {
|
||||
return ctx.http_server.set_routes([&ctx, &tm] (routes& r) { set_token_metadata(ctx, r, tm); });
|
||||
}
|
||||
|
||||
future<> unset_server_token_metadata(http_context& ctx) {
|
||||
@@ -229,22 +223,15 @@ future<> unset_server_gossip(http_context& ctx) {
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_server_column_family(http_context& ctx, sharded<replica::database>& db) {
|
||||
co_await register_api(ctx, "column_family",
|
||||
"The column family API", [&db] (http_context& ctx, routes& r) {
|
||||
set_column_family(ctx, r, db);
|
||||
});
|
||||
co_await register_api(ctx, "cache_service",
|
||||
"The cache service API", [&db] (http_context& ctx, routes& r) {
|
||||
set_cache_service(ctx, db, r);
|
||||
future<> set_server_column_family(http_context& ctx, sharded<db::system_keyspace>& sys_ks) {
|
||||
return register_api(ctx, "column_family",
|
||||
"The column family API", [&sys_ks] (http_context& ctx, routes& r) {
|
||||
set_column_family(ctx, r, sys_ks);
|
||||
});
|
||||
}
|
||||
|
||||
future<> unset_server_column_family(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) {
|
||||
unset_column_family(ctx, r);
|
||||
unset_cache_service(ctx, r);
|
||||
});
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_column_family(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_messaging_service(http_context& ctx, sharded<netw::messaging_service>& ms) {
|
||||
@@ -276,10 +263,19 @@ future<> unset_server_stream_manager(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_stream_manager(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_hinted_handoff(http_context& ctx, sharded<service::storage_proxy>& proxy, sharded<gms::gossiper>& g) {
|
||||
future<> set_server_cache(http_context& ctx) {
|
||||
return register_api(ctx, "cache_service",
|
||||
"The cache service API", set_cache_service);
|
||||
}
|
||||
|
||||
future<> unset_server_cache(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_cache_service(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_hinted_handoff(http_context& ctx, sharded<service::storage_proxy>& proxy) {
|
||||
return register_api(ctx, "hinted_handoff",
|
||||
"The hinted handoff API", [&proxy, &g] (http_context& ctx, routes& r) {
|
||||
set_hinted_handoff(ctx, r, proxy, g);
|
||||
"The hinted handoff API", [&proxy] (http_context& ctx, routes& r) {
|
||||
set_hinted_handoff(ctx, r, proxy);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -287,7 +283,7 @@ future<> unset_hinted_handoff(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_hinted_handoff(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_compaction_manager(http_context& ctx, sharded<compaction::compaction_manager>& cm) {
|
||||
future<> set_server_compaction_manager(http_context& ctx, sharded<compaction_manager>& cm) {
|
||||
return register_api(ctx, "compaction_manager", "The Compaction manager API", [&cm] (http_context& ctx, routes& r) {
|
||||
set_compaction_manager(ctx, r, cm);
|
||||
});
|
||||
@@ -320,13 +316,13 @@ future<> unset_server_commitlog(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_commitlog(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_task_manager(http_context& ctx, sharded<tasks::task_manager>& tm, lw_shared_ptr<db::config> cfg, sharded<gms::gossiper>& gossiper) {
|
||||
future<> set_server_task_manager(http_context& ctx, sharded<tasks::task_manager>& tm, lw_shared_ptr<db::config> cfg) {
|
||||
auto rb = std::make_shared < api_registry_builder > (ctx.api_doc);
|
||||
|
||||
return ctx.http_server.set_routes([rb, &ctx, &tm, &cfg = *cfg, &gossiper](routes& r) {
|
||||
return ctx.http_server.set_routes([rb, &ctx, &tm, &cfg = *cfg](routes& r) {
|
||||
rb->register_function(r, "task_manager",
|
||||
"The task manager API");
|
||||
set_task_manager(ctx, r, tm, cfg, gossiper);
|
||||
set_task_manager(ctx, r, tm, cfg);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -362,12 +358,6 @@ future<> unset_server_cql_server_test(http_context& ctx) {
|
||||
|
||||
#endif
|
||||
|
||||
future<> set_server_service_levels(http_context &ctx, cql_transport::controller& ctl, sharded<cql3::query_processor>& qp) {
|
||||
return register_api(ctx, "service_levels", "The service levels API", [&ctl, &qp] (http_context& ctx, routes& r) {
|
||||
set_service_levels(ctx, r, ctl, qp);
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_server_tasks_compaction_module(http_context& ctx, sharded<service::storage_service>& ss, sharded<db::snapshot_ctl>& snap_ctl) {
|
||||
auto rb = std::make_shared < api_registry_builder > (ctx.api_doc);
|
||||
|
||||
@@ -394,5 +384,32 @@ future<> unset_server_raft(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_raft(ctx, r); });
|
||||
}
|
||||
|
||||
void req_params::process(const request& req) {
|
||||
// Process mandatory parameters
|
||||
for (auto& [name, ent] : params) {
|
||||
if (!ent.is_mandatory) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ent.value = req.get_path_param(name);
|
||||
} catch (std::out_of_range&) {
|
||||
throw httpd::bad_param_exception(fmt::format("Mandatory parameter '{}' was not provided", name));
|
||||
}
|
||||
}
|
||||
|
||||
// Process optional parameters
|
||||
for (auto& [name, value] : req.query_parameters) {
|
||||
try {
|
||||
auto& ent = params.at(name);
|
||||
if (ent.is_mandatory) {
|
||||
throw httpd::bad_param_exception(fmt::format("Parameter '{}' is expected to be provided as part of the request url", name));
|
||||
}
|
||||
ent.value = value;
|
||||
} catch (std::out_of_range&) {
|
||||
throw httpd::bad_param_exception(fmt::format("Unsupported optional parameter '{}'", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
116
api/api.hh
116
api/api.hh
@@ -23,6 +23,42 @@
|
||||
|
||||
namespace api {
|
||||
|
||||
template<class T>
|
||||
std::vector<sstring> container_to_vec(const T& container) {
|
||||
std::vector<sstring> res;
|
||||
res.reserve(std::size(container));
|
||||
|
||||
for (const auto& i : container) {
|
||||
res.push_back(fmt::to_string(i));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
std::vector<T> map_to_key_value(const std::map<sstring, sstring>& map) {
|
||||
std::vector<T> res;
|
||||
res.reserve(map.size());
|
||||
|
||||
for (const auto& [key, value] : map) {
|
||||
res.push_back(T());
|
||||
res.back().key = key;
|
||||
res.back().value = value;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template<class T, class MAP>
|
||||
std::vector<T>& map_to_key_value(const MAP& map, std::vector<T>& res) {
|
||||
res.reserve(res.size() + std::size(map));
|
||||
|
||||
for (const auto& [key, value] : map) {
|
||||
T val;
|
||||
val.key = fmt::to_string(key);
|
||||
val.value = fmt::to_string(value);
|
||||
res.push_back(val);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
template <typename T, typename S = T>
|
||||
T map_sum(T&& dest, const S& src) {
|
||||
for (const auto& i : src) {
|
||||
@@ -31,6 +67,17 @@ T map_sum(T&& dest, const S& src) {
|
||||
return std::move(dest);
|
||||
}
|
||||
|
||||
template <typename MAP>
|
||||
std::vector<sstring> map_keys(const MAP& map) {
|
||||
std::vector<sstring> res;
|
||||
res.reserve(std::size(map));
|
||||
|
||||
for (const auto& i : map) {
|
||||
res.push_back(fmt::to_string(i.first));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* General sstring splitting function
|
||||
*/
|
||||
@@ -48,7 +95,7 @@ inline std::vector<sstring> split(const sstring& text, const char* separator) {
|
||||
*
|
||||
*/
|
||||
template<class T, class F, class V>
|
||||
future<json::json_return_type> sum_stats(sharded<T>& d, V F::*f) {
|
||||
future<json::json_return_type> sum_stats(distributed<T>& d, V F::*f) {
|
||||
return d.map_reduce0([f](const T& p) {return p.get_stats().*f;}, 0,
|
||||
std::plus<V>()).then([](V val) {
|
||||
return make_ready_future<json::json_return_type>(val);
|
||||
@@ -81,7 +128,7 @@ httpd::utils_json::rate_moving_average_and_histogram timer_to_json(const utils::
|
||||
}
|
||||
|
||||
template<class T, class F>
|
||||
future<json::json_return_type> sum_histogram_stats(sharded<T>& d, utils::timed_rate_moving_average_and_histogram F::*f) {
|
||||
future<json::json_return_type> sum_histogram_stats(distributed<T>& d, utils::timed_rate_moving_average_and_histogram F::*f) {
|
||||
|
||||
return d.map_reduce0([f](const T& p) {return (p.get_stats().*f).hist;}, utils::ihistogram(),
|
||||
std::plus<utils::ihistogram>()).then([](const utils::ihistogram& val) {
|
||||
@@ -90,7 +137,7 @@ future<json::json_return_type> sum_histogram_stats(sharded<T>& d, utils::timed_
|
||||
}
|
||||
|
||||
template<class T, class F>
|
||||
future<json::json_return_type> sum_timer_stats(sharded<T>& d, utils::timed_rate_moving_average_and_histogram F::*f) {
|
||||
future<json::json_return_type> sum_timer_stats(distributed<T>& d, utils::timed_rate_moving_average_and_histogram F::*f) {
|
||||
|
||||
return d.map_reduce0([f](const T& p) {return (p.get_stats().*f).rate();}, utils::rate_moving_average_and_histogram(),
|
||||
std::plus<utils::rate_moving_average_and_histogram>()).then([](const utils::rate_moving_average_and_histogram& val) {
|
||||
@@ -99,7 +146,7 @@ future<json::json_return_type> sum_timer_stats(sharded<T>& d, utils::timed_rate
|
||||
}
|
||||
|
||||
template<class T, class F>
|
||||
future<json::json_return_type> sum_timer_stats(sharded<T>& d, utils::timed_rate_moving_average_summary_and_histogram F::*f) {
|
||||
future<json::json_return_type> sum_timer_stats(distributed<T>& d, utils::timed_rate_moving_average_summary_and_histogram F::*f) {
|
||||
return d.map_reduce0([f](const T& p) {return (p.get_stats().*f).rate();}, utils::rate_moving_average_and_histogram(),
|
||||
std::plus<utils::rate_moving_average_and_histogram>()).then([](const utils::rate_moving_average_and_histogram& val) {
|
||||
return make_ready_future<json::json_return_type>(timer_to_json(val));
|
||||
@@ -205,6 +252,67 @@ public:
|
||||
operator T() const { return value; }
|
||||
};
|
||||
|
||||
using mandatory = bool_class<struct mandatory_tag>;
|
||||
|
||||
class req_params {
|
||||
public:
|
||||
struct def {
|
||||
std::optional<sstring> value;
|
||||
mandatory is_mandatory = mandatory::no;
|
||||
|
||||
def(std::optional<sstring> value_ = std::nullopt, mandatory is_mandatory_ = mandatory::no)
|
||||
: value(std::move(value_))
|
||||
, is_mandatory(is_mandatory_)
|
||||
{ }
|
||||
|
||||
def(mandatory is_mandatory_)
|
||||
: is_mandatory(is_mandatory_)
|
||||
{ }
|
||||
};
|
||||
|
||||
private:
|
||||
std::unordered_map<sstring, def> params;
|
||||
|
||||
public:
|
||||
req_params(std::initializer_list<std::pair<sstring, def>> l) {
|
||||
for (const auto& [name, ent] : l) {
|
||||
add(std::move(name), std::move(ent));
|
||||
}
|
||||
}
|
||||
|
||||
void add(sstring name, def ent) {
|
||||
params.emplace(std::move(name), std::move(ent));
|
||||
}
|
||||
|
||||
void process(const request& req);
|
||||
|
||||
const std::optional<sstring>& get(const char* name) const {
|
||||
return params.at(name).value;
|
||||
}
|
||||
|
||||
template <typename T = sstring>
|
||||
const std::optional<T> get_as(const char* name) const {
|
||||
return get(name);
|
||||
}
|
||||
|
||||
template <typename T = sstring>
|
||||
requires std::same_as<T, bool>
|
||||
const std::optional<bool> get_as(const char* name) const {
|
||||
auto value = get(name);
|
||||
if (!value) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::transform(value->begin(), value->end(), value->begin(), ::tolower);
|
||||
if (value == "true" || value == "yes" || value == "1") {
|
||||
return true;
|
||||
}
|
||||
if (value == "false" || value == "no" || value == "0") {
|
||||
return false;
|
||||
}
|
||||
throw boost::bad_lexical_cast{};
|
||||
}
|
||||
};
|
||||
|
||||
httpd::utils_json::estimated_histogram time_to_json_histogram(const utils::time_estimated_histogram& val);
|
||||
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
using request = http::request;
|
||||
using reply = http::reply;
|
||||
|
||||
namespace compaction {
|
||||
class compaction_manager;
|
||||
}
|
||||
|
||||
namespace service {
|
||||
|
||||
@@ -29,7 +27,6 @@ class storage_proxy;
|
||||
class storage_service;
|
||||
class raft_group0_client;
|
||||
class raft_group_registry;
|
||||
class client_routes_service;
|
||||
|
||||
} // namespace service
|
||||
|
||||
@@ -59,6 +56,7 @@ class sstables_format_selector;
|
||||
namespace view {
|
||||
class view_builder;
|
||||
}
|
||||
class system_keyspace;
|
||||
}
|
||||
namespace netw { class messaging_service; }
|
||||
class repair_service;
|
||||
@@ -75,36 +73,30 @@ namespace tasks {
|
||||
class task_manager;
|
||||
}
|
||||
|
||||
namespace cql3 {
|
||||
class query_processor;
|
||||
}
|
||||
|
||||
namespace api {
|
||||
|
||||
struct http_context {
|
||||
sstring api_dir;
|
||||
sstring api_doc;
|
||||
httpd::http_server_control http_server;
|
||||
sharded<replica::database>& db;
|
||||
distributed<replica::database>& db;
|
||||
|
||||
http_context(sharded<replica::database>& _db)
|
||||
http_context(distributed<replica::database>& _db)
|
||||
: db(_db)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
future<> set_server_init(http_context& ctx);
|
||||
future<> set_server_config(http_context& ctx, db::config& cfg);
|
||||
future<> set_server_config(http_context& ctx, const db::config& cfg);
|
||||
future<> unset_server_config(http_context& ctx);
|
||||
future<> set_server_snitch(http_context& ctx, sharded<locator::snitch_ptr>& snitch);
|
||||
future<> unset_server_snitch(http_context& ctx);
|
||||
future<> set_server_storage_service(http_context& ctx, sharded<service::storage_service>& ss, sharded<db::snapshot_ctl>&, service::raft_group0_client&);
|
||||
future<> set_server_storage_service(http_context& ctx, sharded<service::storage_service>& ss, service::raft_group0_client&);
|
||||
future<> unset_server_storage_service(http_context& ctx);
|
||||
future<> set_server_client_routes(http_context& ctx, sharded<service::client_routes_service>& cr);
|
||||
future<> unset_server_client_routes(http_context& ctx);
|
||||
future<> set_server_sstables_loader(http_context& ctx, sharded<sstables_loader>& sst_loader);
|
||||
future<> unset_server_sstables_loader(http_context& ctx);
|
||||
future<> set_server_view_builder(http_context& ctx, sharded<db::view::view_builder>& vb, sharded<gms::gossiper>& g);
|
||||
future<> set_server_view_builder(http_context& ctx, sharded<db::view::view_builder>& vb);
|
||||
future<> unset_server_view_builder(http_context& ctx);
|
||||
future<> set_server_repair(http_context& ctx, sharded<repair_service>& repair, sharded<gms::gossip_address_map>& am);
|
||||
future<> unset_server_repair(http_context& ctx);
|
||||
@@ -116,11 +108,11 @@ future<> set_server_authorization_cache(http_context& ctx, sharded<auth::service
|
||||
future<> unset_server_authorization_cache(http_context& ctx);
|
||||
future<> set_server_snapshot(http_context& ctx, sharded<db::snapshot_ctl>& snap_ctl);
|
||||
future<> unset_server_snapshot(http_context& ctx);
|
||||
future<> set_server_token_metadata(http_context& ctx, sharded<locator::shared_token_metadata>& tm, sharded<gms::gossiper>& g);
|
||||
future<> set_server_token_metadata(http_context& ctx, sharded<locator::shared_token_metadata>& tm);
|
||||
future<> unset_server_token_metadata(http_context& ctx);
|
||||
future<> set_server_gossip(http_context& ctx, sharded<gms::gossiper>& g);
|
||||
future<> unset_server_gossip(http_context& ctx);
|
||||
future<> set_server_column_family(http_context& ctx, sharded<replica::database>& db);
|
||||
future<> set_server_column_family(http_context& ctx, sharded<db::system_keyspace>& sys_ks);
|
||||
future<> unset_server_column_family(http_context& ctx);
|
||||
future<> set_server_messaging_service(http_context& ctx, sharded<netw::messaging_service>& ms);
|
||||
future<> unset_server_messaging_service(http_context& ctx);
|
||||
@@ -128,12 +120,14 @@ future<> set_server_storage_proxy(http_context& ctx, sharded<service::storage_pr
|
||||
future<> unset_server_storage_proxy(http_context& ctx);
|
||||
future<> set_server_stream_manager(http_context& ctx, sharded<streaming::stream_manager>& sm);
|
||||
future<> unset_server_stream_manager(http_context& ctx);
|
||||
future<> set_hinted_handoff(http_context& ctx, sharded<service::storage_proxy>& p, sharded<gms::gossiper>& g);
|
||||
future<> set_hinted_handoff(http_context& ctx, sharded<service::storage_proxy>& p);
|
||||
future<> unset_hinted_handoff(http_context& ctx);
|
||||
future<> set_server_compaction_manager(http_context& ctx, sharded<compaction::compaction_manager>& cm);
|
||||
future<> set_server_cache(http_context& ctx);
|
||||
future<> unset_server_cache(http_context& ctx);
|
||||
future<> set_server_compaction_manager(http_context& ctx, sharded<compaction_manager>& cm);
|
||||
future<> unset_server_compaction_manager(http_context& ctx);
|
||||
future<> set_server_done(http_context& ctx);
|
||||
future<> set_server_task_manager(http_context& ctx, sharded<tasks::task_manager>& tm, lw_shared_ptr<db::config> cfg, sharded<gms::gossiper>& gossiper);
|
||||
future<> set_server_task_manager(http_context& ctx, sharded<tasks::task_manager>& tm, lw_shared_ptr<db::config> cfg);
|
||||
future<> unset_server_task_manager(http_context& ctx);
|
||||
future<> set_server_task_manager_test(http_context& ctx, sharded<tasks::task_manager>& tm);
|
||||
future<> unset_server_task_manager_test(http_context& ctx);
|
||||
@@ -143,9 +137,10 @@ future<> set_server_raft(http_context&, sharded<service::raft_group_registry>&);
|
||||
future<> unset_server_raft(http_context&);
|
||||
future<> set_load_meter(http_context& ctx, service::load_meter& lm);
|
||||
future<> unset_load_meter(http_context& ctx);
|
||||
future<> set_format_selector(http_context& ctx, db::sstables_format_selector& sel);
|
||||
future<> unset_format_selector(http_context& ctx);
|
||||
future<> set_server_cql_server_test(http_context& ctx, cql_transport::controller& ctl);
|
||||
future<> unset_server_cql_server_test(http_context& ctx);
|
||||
future<> set_server_service_levels(http_context& ctx, cql_transport::controller& ctl, sharded<cql3::query_processor>& qp);
|
||||
future<> set_server_commitlog(http_context& ctx, sharded<replica::database>&);
|
||||
future<> unset_server_commitlog(http_context& ctx);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace json;
|
||||
using namespace seastar::httpd;
|
||||
namespace cs = httpd::cache_service_json;
|
||||
|
||||
void set_cache_service(http_context& ctx, sharded<replica::database>& db, routes& r) {
|
||||
void set_cache_service(http_context& ctx, routes& r) {
|
||||
cs::get_row_cache_save_period_in_seconds.set(r, [](std::unique_ptr<http::request> req) {
|
||||
// We never save the cache
|
||||
// Origin uses 0 for never
|
||||
@@ -204,53 +204,53 @@ void set_cache_service(http_context& ctx, sharded<replica::database>& db, routes
|
||||
});
|
||||
});
|
||||
|
||||
cs::get_row_hits.set(r, [&db] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf(db, uint64_t(0), [](const replica::column_family& cf) {
|
||||
cs::get_row_hits.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf(ctx, uint64_t(0), [](const replica::column_family& cf) {
|
||||
return cf.get_row_cache().stats().hits.count();
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cs::get_row_requests.set(r, [&db] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf(db, uint64_t(0), [](const replica::column_family& cf) {
|
||||
cs::get_row_requests.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf(ctx, uint64_t(0), [](const replica::column_family& cf) {
|
||||
return cf.get_row_cache().stats().hits.count() + cf.get_row_cache().stats().misses.count();
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cs::get_row_hit_rate.set(r, [&db] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf(db, ratio_holder(), [](const replica::column_family& cf) {
|
||||
cs::get_row_hit_rate.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf(ctx, ratio_holder(), [](const replica::column_family& cf) {
|
||||
return ratio_holder(cf.get_row_cache().stats().hits.count() + cf.get_row_cache().stats().misses.count(),
|
||||
cf.get_row_cache().stats().hits.count());
|
||||
}, std::plus<ratio_holder>());
|
||||
});
|
||||
|
||||
cs::get_row_hits_moving_avrage.set(r, [&db] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf_raw(db, utils::rate_moving_average(), [](const replica::column_family& cf) {
|
||||
cs::get_row_hits_moving_avrage.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf_raw(ctx, utils::rate_moving_average(), [](const replica::column_family& cf) {
|
||||
return cf.get_row_cache().stats().hits.rate();
|
||||
}, std::plus<utils::rate_moving_average>()).then([](const utils::rate_moving_average& m) {
|
||||
return make_ready_future<json::json_return_type>(meter_to_json(m));
|
||||
});
|
||||
});
|
||||
|
||||
cs::get_row_requests_moving_avrage.set(r, [&db] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf_raw(db, utils::rate_moving_average(), [](const replica::column_family& cf) {
|
||||
cs::get_row_requests_moving_avrage.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf_raw(ctx, utils::rate_moving_average(), [](const replica::column_family& cf) {
|
||||
return cf.get_row_cache().stats().hits.rate() + cf.get_row_cache().stats().misses.rate();
|
||||
}, std::plus<utils::rate_moving_average>()).then([](const utils::rate_moving_average& m) {
|
||||
return make_ready_future<json::json_return_type>(meter_to_json(m));
|
||||
});
|
||||
});
|
||||
|
||||
cs::get_row_size.set(r, [&db] (std::unique_ptr<http::request> req) {
|
||||
cs::get_row_size.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
// In origin row size is the weighted size.
|
||||
// We currently do not support weights, so we use raw size in bytes instead
|
||||
return db.map_reduce0([](replica::database& db) -> uint64_t {
|
||||
return ctx.db.map_reduce0([](replica::database& db) -> uint64_t {
|
||||
return db.row_cache_tracker().region().occupancy().used_space();
|
||||
}, uint64_t(0), std::plus<uint64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
cs::get_row_entries.set(r, [&db] (std::unique_ptr<http::request> req) {
|
||||
return db.map_reduce0([](replica::database& db) -> uint64_t {
|
||||
cs::get_row_entries.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
return ctx.db.map_reduce0([](replica::database& db) -> uint64_t {
|
||||
return db.row_cache_tracker().partitions();
|
||||
}, uint64_t(0), std::plus<uint64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
|
||||
@@ -7,20 +7,15 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <seastar/core/sharded.hh>
|
||||
|
||||
namespace seastar::httpd {
|
||||
class routes;
|
||||
}
|
||||
|
||||
namespace replica {
|
||||
class database;
|
||||
}
|
||||
|
||||
namespace api {
|
||||
|
||||
struct http_context;
|
||||
void set_cache_service(http_context& ctx, seastar::sharded<replica::database>& db, seastar::httpd::routes& r);
|
||||
void set_cache_service(http_context& ctx, seastar::httpd::routes& r);
|
||||
void unset_cache_service(http_context& ctx, seastar::httpd::routes& r);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025-present ScyllaDB
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
*/
|
||||
|
||||
#include <seastar/http/short_streams.hh>
|
||||
|
||||
#include "client_routes.hh"
|
||||
#include "api/api.hh"
|
||||
#include "service/storage_service.hh"
|
||||
#include "service/client_routes.hh"
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
|
||||
#include "api/api-doc/client_routes.json.hh"
|
||||
|
||||
using namespace seastar::httpd;
|
||||
using namespace std::chrono_literals;
|
||||
using namespace json;
|
||||
|
||||
extern logging::logger apilog;
|
||||
|
||||
namespace api {
|
||||
|
||||
static void validate_client_routes_endpoint(sharded<service::client_routes_service>& cr, sstring endpoint_name) {
|
||||
if (!cr.local().get_feature_service().client_routes) {
|
||||
apilog.warn("{}: called before the cluster feature was enabled", endpoint_name);
|
||||
throw std::runtime_error(fmt::format("{} requires all nodes to support the CLIENT_ROUTES cluster feature", endpoint_name));
|
||||
}
|
||||
}
|
||||
|
||||
static sstring parse_string(const char* name, rapidjson::Value const& v) {
|
||||
const auto it = v.FindMember(name);
|
||||
if (it == v.MemberEnd()) {
|
||||
throw bad_param_exception(fmt::format("Missing '{}'", name));
|
||||
}
|
||||
if (!it->value.IsString()) {
|
||||
throw bad_param_exception(fmt::format("'{}' must be a string", name));
|
||||
}
|
||||
return {it->value.GetString(), it->value.GetStringLength()};
|
||||
}
|
||||
|
||||
static std::optional<uint32_t> parse_port(const char* name, rapidjson::Value const& v) {
|
||||
const auto it = v.FindMember(name);
|
||||
if (it == v.MemberEnd()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!it->value.IsInt()) {
|
||||
throw bad_param_exception(fmt::format("'{}' must be an integer", name));
|
||||
}
|
||||
auto port = it->value.GetInt();
|
||||
if (port < 1 || port > 65535) {
|
||||
throw bad_param_exception(fmt::format("'{}' value={} is outside the allowed port range", name, port));
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
static std::vector<service::client_routes_service::client_route_entry> parse_set_client_array(const rapidjson::Document& root) {
|
||||
if (!root.IsArray()) {
|
||||
throw bad_param_exception("Body must be a JSON array");
|
||||
}
|
||||
|
||||
std::vector<service::client_routes_service::client_route_entry> v;
|
||||
v.reserve(root.GetArray().Size());
|
||||
for (const auto& element : root.GetArray()) {
|
||||
if (!element.IsObject()) { throw bad_param_exception("Each element must be object"); }
|
||||
|
||||
const auto port = parse_port("port", element);
|
||||
const auto tls_port = parse_port("tls_port", element);
|
||||
const auto alternator_port = parse_port("alternator_port", element);
|
||||
const auto alternator_https_port = parse_port("alternator_https_port", element);
|
||||
|
||||
if (!port.has_value() && !tls_port.has_value() && !alternator_port.has_value() && !alternator_https_port.has_value()) {
|
||||
throw bad_param_exception("At least one port field ('port', 'tls_port', 'alternator_port', 'alternator_https_port') must be specified");
|
||||
}
|
||||
|
||||
v.emplace_back(
|
||||
parse_string("connection_id", element),
|
||||
utils::UUID{parse_string("host_id", element)},
|
||||
parse_string("address", element),
|
||||
port,
|
||||
tls_port,
|
||||
alternator_port,
|
||||
alternator_https_port
|
||||
);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
static
|
||||
future<json::json_return_type>
|
||||
rest_set_client_routes(http_context& ctx, sharded<service::client_routes_service>& cr, std::unique_ptr<http::request> req) {
|
||||
validate_client_routes_endpoint(cr, "rest_set_client_routes");
|
||||
|
||||
rapidjson::Document root;
|
||||
auto content = co_await util::read_entire_stream_contiguous(*req->content_stream);
|
||||
root.Parse(content.c_str());
|
||||
|
||||
co_await cr.local().set_client_routes(parse_set_client_array(root));
|
||||
co_return seastar::json::json_void();
|
||||
}
|
||||
|
||||
static std::vector<service::client_routes_service::client_route_key> parse_delete_client_array(const rapidjson::Document& root) {
|
||||
if (!root.IsArray()) {
|
||||
throw bad_param_exception("Body must be a JSON array");
|
||||
}
|
||||
|
||||
std::vector<service::client_routes_service::client_route_key> v;
|
||||
v.reserve(root.GetArray().Size());
|
||||
for (const auto& element : root.GetArray()) {
|
||||
v.emplace_back(
|
||||
parse_string("connection_id", element),
|
||||
utils::UUID{parse_string("host_id", element)}
|
||||
);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
static
|
||||
future<json::json_return_type>
|
||||
rest_delete_client_routes(http_context& ctx, sharded<service::client_routes_service>& cr, std::unique_ptr<http::request> req) {
|
||||
validate_client_routes_endpoint(cr, "delete_client_routes");
|
||||
|
||||
rapidjson::Document root;
|
||||
auto content = co_await util::read_entire_stream_contiguous(*req->content_stream);
|
||||
root.Parse(content.c_str());
|
||||
|
||||
co_await cr.local().delete_client_routes(parse_delete_client_array(root));
|
||||
co_return seastar::json::json_void();
|
||||
}
|
||||
|
||||
static
|
||||
future<json::json_return_type>
|
||||
rest_get_client_routes(http_context& ctx, sharded<service::client_routes_service>& cr, std::unique_ptr<http::request> req) {
|
||||
validate_client_routes_endpoint(cr, "get_client_routes");
|
||||
|
||||
co_return co_await cr.invoke_on(0, [] (service::client_routes_service& cr) -> future<json::json_return_type> {
|
||||
co_return json::json_return_type(stream_range_as_array(co_await cr.get_client_routes(), [](const service::client_routes_service::client_route_entry & entry) {
|
||||
seastar::httpd::client_routes_json::client_routes_entry obj;
|
||||
obj.connection_id = entry.connection_id;
|
||||
obj.host_id = fmt::to_string(entry.host_id);
|
||||
obj.address = entry.address;
|
||||
if (entry.port.has_value()) { obj.port = entry.port.value(); }
|
||||
if (entry.tls_port.has_value()) { obj.tls_port = entry.tls_port.value(); }
|
||||
if (entry.alternator_port.has_value()) { obj.alternator_port = entry.alternator_port.value(); }
|
||||
if (entry.alternator_https_port.has_value()) { obj.alternator_https_port = entry.alternator_https_port.value(); }
|
||||
return obj;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
void set_client_routes(http_context& ctx, routes& r, sharded<service::client_routes_service>& cr) {
|
||||
seastar::httpd::client_routes_json::set_client_routes.set(r, [&ctx, &cr] (std::unique_ptr<seastar::http::request> req) {
|
||||
return rest_set_client_routes(ctx, cr, std::move(req));
|
||||
});
|
||||
seastar::httpd::client_routes_json::delete_client_routes.set(r, [&ctx, &cr] (std::unique_ptr<seastar::http::request> req) {
|
||||
return rest_delete_client_routes(ctx, cr, std::move(req));
|
||||
});
|
||||
seastar::httpd::client_routes_json::get_client_routes.set(r, [&ctx, &cr] (std::unique_ptr<seastar::http::request> req) {
|
||||
return rest_get_client_routes(ctx, cr, std::move(req));
|
||||
});
|
||||
}
|
||||
|
||||
void unset_client_routes(http_context& ctx, routes& r) {
|
||||
seastar::httpd::client_routes_json::set_client_routes.unset(r);
|
||||
seastar::httpd::client_routes_json::delete_client_routes.unset(r);
|
||||
seastar::httpd::client_routes_json::get_client_routes.unset(r);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025-present ScyllaDB
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <seastar/core/sharded.hh>
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include "api/api_init.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_client_routes(http_context& ctx, httpd::routes& r, sharded<service::client_routes_service>& cr);
|
||||
void unset_client_routes(http_context& ctx, httpd::routes& r);
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "api/api-doc/collectd.json.hh"
|
||||
#include <seastar/core/scollectd.hh>
|
||||
#include <seastar/core/scollectd_api.hh>
|
||||
#include <boost/range/irange.hpp>
|
||||
#include <ranges>
|
||||
#include <regex>
|
||||
#include "api/api_init.hh"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,25 +13,25 @@
|
||||
#include <any>
|
||||
#include "api/api_init.hh"
|
||||
|
||||
namespace db {
|
||||
class system_keyspace;
|
||||
}
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_column_family(http_context& ctx, httpd::routes& r, sharded<replica::database>& db);
|
||||
void set_column_family(http_context& ctx, httpd::routes& r, sharded<db::system_keyspace>& sys_ks);
|
||||
void unset_column_family(http_context& ctx, httpd::routes& r);
|
||||
|
||||
table_info parse_table_info(const sstring& name, const replica::database& db);
|
||||
table_id get_uuid(const sstring& name, const replica::database& db);
|
||||
|
||||
template<class Mapper, class I, class Reducer>
|
||||
future<I> map_reduce_cf_raw(sharded<replica::database>& db, const sstring& name, I init,
|
||||
future<I> map_reduce_cf_raw(http_context& ctx, const sstring& name, I init,
|
||||
Mapper mapper, Reducer reducer) {
|
||||
auto uuid = parse_table_info(name, db.local()).id;
|
||||
using mapper_type = std::function<future<std::unique_ptr<std::any>>(replica::database&)>;
|
||||
auto uuid = get_uuid(name, ctx.db.local());
|
||||
using mapper_type = std::function<std::unique_ptr<std::any>(replica::database&)>;
|
||||
using reducer_type = std::function<std::unique_ptr<std::any>(std::unique_ptr<std::any>, std::unique_ptr<std::any>)>;
|
||||
return db.map_reduce0(mapper_type([mapper, uuid](replica::database& db) {
|
||||
return futurize_invoke([mapper, &db, uuid] {
|
||||
return mapper(db.find_column_family(uuid));
|
||||
}).then([] (auto result) {
|
||||
return std::make_unique<std::any>(I(std::move(result)));
|
||||
});
|
||||
return ctx.db.map_reduce0(mapper_type([mapper, uuid](replica::database& db) {
|
||||
return std::make_unique<std::any>(I(mapper(db.find_column_family(uuid))));
|
||||
}), std::make_unique<std::any>(std::move(init)), reducer_type([reducer = std::move(reducer)] (std::unique_ptr<std::any> a, std::unique_ptr<std::any> b) mutable {
|
||||
return std::make_unique<std::any>(I(reducer(std::any_cast<I>(std::move(*a)), std::any_cast<I>(std::move(*b)))));
|
||||
})).then([] (std::unique_ptr<std::any> r) {
|
||||
@@ -41,30 +41,33 @@ future<I> map_reduce_cf_raw(sharded<replica::database>& db, const sstring& name,
|
||||
|
||||
|
||||
template<class Mapper, class I, class Reducer>
|
||||
future<json::json_return_type> map_reduce_cf(sharded<replica::database>& db, const sstring& name, I init,
|
||||
future<json::json_return_type> map_reduce_cf(http_context& ctx, const sstring& name, I init,
|
||||
Mapper mapper, Reducer reducer) {
|
||||
return map_reduce_cf_raw(db, name, init, mapper, reducer).then([](const I& res) {
|
||||
return map_reduce_cf_raw(ctx, name, init, mapper, reducer).then([](const I& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
}
|
||||
|
||||
template<class Mapper, class I, class Reducer, class Result>
|
||||
future<json::json_return_type> map_reduce_cf(sharded<replica::database>& db, const sstring& name, I init,
|
||||
future<json::json_return_type> map_reduce_cf(http_context& ctx, const sstring& name, I init,
|
||||
Mapper mapper, Reducer reducer, Result result) {
|
||||
return map_reduce_cf_raw(db, name, init, mapper, reducer).then([result](const I& res) mutable {
|
||||
return map_reduce_cf_raw(ctx, name, init, mapper, reducer).then([result](const I& res) mutable {
|
||||
result = res;
|
||||
return make_ready_future<json::json_return_type>(result);
|
||||
});
|
||||
}
|
||||
|
||||
future<json::json_return_type> map_reduce_cf_time_histogram(http_context& ctx, const sstring& name, std::function<utils::time_estimated_histogram(const replica::column_family&)> f);
|
||||
|
||||
struct map_reduce_column_families_locally {
|
||||
std::any init;
|
||||
std::function<future<std::unique_ptr<std::any>>(replica::column_family&)> mapper;
|
||||
std::function<std::unique_ptr<std::any>(replica::column_family&)> mapper;
|
||||
std::function<std::unique_ptr<std::any>(std::unique_ptr<std::any>, std::unique_ptr<std::any>)> reducer;
|
||||
future<std::unique_ptr<std::any>> operator()(replica::database& db) const {
|
||||
auto res = seastar::make_lw_shared<std::unique_ptr<std::any>>(std::make_unique<std::any>(init));
|
||||
return db.get_tables_metadata().for_each_table_gently([res, this] (table_id, seastar::lw_shared_ptr<replica::table> table) -> future<> {
|
||||
*res = reducer(std::move(*res), co_await mapper(*table.get()));
|
||||
return db.get_tables_metadata().for_each_table_gently([res, this] (table_id, seastar::lw_shared_ptr<replica::table> table) {
|
||||
*res = reducer(std::move(*res), mapper(*table.get()));
|
||||
return make_ready_future();
|
||||
}).then([res] () {
|
||||
return std::move(*res);
|
||||
});
|
||||
@@ -72,21 +75,17 @@ struct map_reduce_column_families_locally {
|
||||
};
|
||||
|
||||
template<class Mapper, class I, class Reducer>
|
||||
future<I> map_reduce_cf_raw(sharded<replica::database>& db, I init,
|
||||
future<I> map_reduce_cf_raw(http_context& ctx, I init,
|
||||
Mapper mapper, Reducer reducer) {
|
||||
using mapper_type = std::function<future<std::unique_ptr<std::any>>(replica::column_family&)>;
|
||||
using mapper_type = std::function<std::unique_ptr<std::any>(replica::column_family&)>;
|
||||
using reducer_type = std::function<std::unique_ptr<std::any>(std::unique_ptr<std::any>, std::unique_ptr<std::any>)>;
|
||||
auto wrapped_mapper = mapper_type([mapper = std::move(mapper)] (replica::column_family& cf) mutable {
|
||||
return futurize_invoke([&cf, mapper] {
|
||||
return mapper(cf);
|
||||
}).then([] (auto result) {
|
||||
return std::make_unique<std::any>(I(std::move(result)));
|
||||
});
|
||||
return std::make_unique<std::any>(I(mapper(cf)));
|
||||
});
|
||||
auto wrapped_reducer = reducer_type([reducer = std::move(reducer)] (std::unique_ptr<std::any> a, std::unique_ptr<std::any> b) mutable {
|
||||
return std::make_unique<std::any>(I(reducer(std::any_cast<I>(std::move(*a)), std::any_cast<I>(std::move(*b)))));
|
||||
});
|
||||
return db.map_reduce0(map_reduce_column_families_locally{init,
|
||||
return ctx.db.map_reduce0(map_reduce_column_families_locally{init,
|
||||
std::move(wrapped_mapper), wrapped_reducer}, std::make_unique<std::any>(init), wrapped_reducer).then([] (std::unique_ptr<std::any> res) {
|
||||
return std::any_cast<I>(std::move(*res));
|
||||
});
|
||||
@@ -94,13 +93,20 @@ future<I> map_reduce_cf_raw(sharded<replica::database>& db, I init,
|
||||
|
||||
|
||||
template<class Mapper, class I, class Reducer>
|
||||
future<json::json_return_type> map_reduce_cf(sharded<replica::database>& db, I init,
|
||||
future<json::json_return_type> map_reduce_cf(http_context& ctx, I init,
|
||||
Mapper mapper, Reducer reducer) {
|
||||
return map_reduce_cf_raw(db, init, mapper, reducer).then([](const I& res) {
|
||||
return map_reduce_cf_raw(ctx, init, mapper, reducer).then([](const I& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
}
|
||||
|
||||
future<json::json_return_type> get_cf_stats(http_context& ctx, const sstring& name,
|
||||
int64_t replica::column_family_stats::*f);
|
||||
|
||||
future<json::json_return_type> get_cf_stats(http_context& ctx,
|
||||
int64_t replica::column_family_stats::*f);
|
||||
|
||||
|
||||
std::tuple<sstring, sstring> parse_fully_qualified_cf_name(sstring name);
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include "api/api.hh"
|
||||
#include "api/api-doc/compaction_manager.json.hh"
|
||||
#include "api/api-doc/storage_service.json.hh"
|
||||
#include "db/compaction_history_entry.hh"
|
||||
#include "db/system_keyspace.hh"
|
||||
#include "column_family.hh"
|
||||
#include "unimplemented.hh"
|
||||
@@ -29,9 +28,9 @@ namespace ss = httpd::storage_service_json;
|
||||
using namespace json;
|
||||
using namespace seastar::httpd;
|
||||
|
||||
static future<json::json_return_type> get_cm_stats(sharded<compaction::compaction_manager>& cm,
|
||||
int64_t compaction::compaction_manager::stats::*f) {
|
||||
return cm.map_reduce0([f](compaction::compaction_manager& cm) {
|
||||
static future<json::json_return_type> get_cm_stats(sharded<compaction_manager>& cm,
|
||||
int64_t compaction_manager::stats::*f) {
|
||||
return cm.map_reduce0([f](compaction_manager& cm) {
|
||||
return cm.get_stats().*f;
|
||||
}, int64_t(0), std::plus<int64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
@@ -47,9 +46,9 @@ static std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_ha
|
||||
return std::move(a);
|
||||
}
|
||||
|
||||
void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::compaction_manager>& cm) {
|
||||
void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction_manager>& cm) {
|
||||
cm::get_compactions.set(r, [&cm] (std::unique_ptr<http::request> req) {
|
||||
return cm.map_reduce0([](compaction::compaction_manager& cm) {
|
||||
return cm.map_reduce0([](compaction_manager& cm) {
|
||||
std::vector<cm::summary> summaries;
|
||||
|
||||
for (const auto& c : cm.get_compactions()) {
|
||||
@@ -58,7 +57,7 @@ void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::co
|
||||
s.ks = c.ks_name;
|
||||
s.cf = c.cf_name;
|
||||
s.unit = "keys";
|
||||
s.task_type = compaction::compaction_name(c.type);
|
||||
s.task_type = sstables::compaction_name(c.type);
|
||||
s.completed = c.total_keys_written;
|
||||
s.total = c.total_partitions;
|
||||
summaries.push_back(std::move(s));
|
||||
@@ -72,9 +71,10 @@ void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::co
|
||||
cm::get_pending_tasks_by_table.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
return ctx.db.map_reduce0([](replica::database& db) {
|
||||
return do_with(std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash>(), [&db](std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash>& tasks) {
|
||||
return db.get_tables_metadata().for_each_table_gently([&tasks] (table_id, lw_shared_ptr<replica::table> table) -> future<> {
|
||||
return db.get_tables_metadata().for_each_table_gently([&tasks] (table_id, lw_shared_ptr<replica::table> table) {
|
||||
replica::table& cf = *table.get();
|
||||
tasks[std::make_pair(cf.schema()->ks_name(), cf.schema()->cf_name())] = co_await cf.estimate_pending_compactions();
|
||||
tasks[std::make_pair(cf.schema()->ks_name(), cf.schema()->cf_name())] = cf.estimate_pending_compactions();
|
||||
return make_ready_future<>();
|
||||
}).then([&tasks] {
|
||||
return std::move(tasks);
|
||||
});
|
||||
@@ -103,20 +103,23 @@ void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::co
|
||||
|
||||
cm::stop_compaction.set(r, [&cm] (std::unique_ptr<http::request> req) {
|
||||
auto type = req->get_query_param("type");
|
||||
return cm.invoke_on_all([type] (compaction::compaction_manager& cm) {
|
||||
return cm.invoke_on_all([type] (compaction_manager& cm) {
|
||||
return cm.stop_compaction(type);
|
||||
}).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
cm::stop_keyspace_compaction.set(r, [&ctx, &cm] (std::unique_ptr<http::request> req) -> future<json::json_return_type> {
|
||||
auto [ks_name, tables] = parse_table_infos(ctx, *req, "tables");
|
||||
cm::stop_keyspace_compaction.set(r, [&ctx] (std::unique_ptr<http::request> req) -> future<json::json_return_type> {
|
||||
auto ks_name = validate_keyspace(ctx, req);
|
||||
auto table_names = parse_tables(ks_name, ctx, req->query_parameters, "tables");
|
||||
auto type = req->get_query_param("type");
|
||||
co_await cm.invoke_on_all([&] (compaction::compaction_manager& cm) {
|
||||
return parallel_for_each(tables, [&] (const table_info& ti) {
|
||||
return cm.stop_compaction(type, [id = ti.id] (const compaction::compaction_group_view* x) {
|
||||
return x->schema()->id() == id;
|
||||
co_await ctx.db.invoke_on_all([&] (replica::database& db) {
|
||||
auto& cm = db.get_compaction_manager();
|
||||
return parallel_for_each(table_names, [&] (sstring& table_name) {
|
||||
auto& t = db.find_column_family(ks_name, table_name);
|
||||
return t.parallel_foreach_table_state([&] (compaction::table_state& ts) {
|
||||
return cm.stop_compaction(type, &ts);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -124,13 +127,13 @@ void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::co
|
||||
});
|
||||
|
||||
cm::get_pending_tasks.set(r, [&ctx] (std::unique_ptr<http::request> req) {
|
||||
return map_reduce_cf(ctx.db, int64_t(0), [](replica::column_family& cf) {
|
||||
return map_reduce_cf(ctx, int64_t(0), [](replica::column_family& cf) {
|
||||
return cf.estimate_pending_compactions();
|
||||
}, std::plus<int64_t>());
|
||||
});
|
||||
|
||||
cm::get_completed_tasks.set(r, [&cm] (std::unique_ptr<http::request> req) {
|
||||
return get_cm_stats(cm, &compaction::compaction_manager::stats::completed_tasks);
|
||||
return get_cm_stats(cm, &compaction_manager::stats::completed_tasks);
|
||||
});
|
||||
|
||||
cm::get_total_compactions_completed.set(r, [] (std::unique_ptr<http::request> req) {
|
||||
@@ -148,7 +151,7 @@ void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::co
|
||||
});
|
||||
|
||||
cm::get_compaction_history.set(r, [&cm] (std::unique_ptr<http::request> req) {
|
||||
noncopyable_function<future<>(output_stream<char>&&)> f = [&cm] (output_stream<char>&& out) -> future<> {
|
||||
std::function<future<>(output_stream<char>&&)> f = [&cm] (output_stream<char>&& out) -> future<> {
|
||||
auto s = std::move(out);
|
||||
bool first = true;
|
||||
std::exception_ptr ex;
|
||||
@@ -157,11 +160,8 @@ void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::co
|
||||
co_await cm.local().get_compaction_history([&s, &first](const db::compaction_history_entry& entry) mutable -> future<> {
|
||||
cm::history h;
|
||||
h.id = fmt::to_string(entry.id);
|
||||
h.shard_id = entry.shard_id;
|
||||
h.ks = std::move(entry.ks);
|
||||
h.cf = std::move(entry.cf);
|
||||
h.compaction_type = entry.compaction_type;
|
||||
h.started_at = entry.started_at;
|
||||
h.compacted_at = entry.compacted_at;
|
||||
h.bytes_in = entry.bytes_in;
|
||||
h.bytes_out = entry.bytes_out;
|
||||
@@ -173,24 +173,6 @@ void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::co
|
||||
e.value = it.second;
|
||||
h.rows_merged.push(std::move(e));
|
||||
}
|
||||
for (const auto& data : entry.sstables_in) {
|
||||
httpd::compaction_manager_json::sstableinfo sstable;
|
||||
sstable.generation = fmt::to_string(data.generation),
|
||||
sstable.origin = data.origin,
|
||||
sstable.size = data.size,
|
||||
h.sstables_in.push(std::move(sstable));
|
||||
}
|
||||
for (const auto& data : entry.sstables_out) {
|
||||
httpd::compaction_manager_json::sstableinfo sstable;
|
||||
sstable.generation = fmt::to_string(data.generation),
|
||||
sstable.origin = data.origin,
|
||||
sstable.size = data.size,
|
||||
h.sstables_out.push(std::move(sstable));
|
||||
}
|
||||
h.total_tombstone_purge_attempt = entry.total_tombstone_purge_attempt;
|
||||
h.total_tombstone_purge_failure_due_to_overlapping_with_memtable = entry.total_tombstone_purge_failure_due_to_overlapping_with_memtable;
|
||||
h.total_tombstone_purge_failure_due_to_overlapping_with_uncompacting_sstable = entry.total_tombstone_purge_failure_due_to_overlapping_with_uncompacting_sstable;
|
||||
|
||||
if (!first) {
|
||||
co_await s.write(", ");
|
||||
}
|
||||
@@ -222,6 +204,14 @@ void set_compaction_manager(http_context& ctx, routes& r, sharded<compaction::co
|
||||
int value = cm.local().throughput_mbs();
|
||||
return make_ready_future<json::json_return_type>(value);
|
||||
});
|
||||
|
||||
ss::set_compaction_throughput_mb_per_sec.set(r, [](std::unique_ptr<http::request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
auto value = req->get_query_param("value");
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void unset_compaction_manager(http_context& ctx, routes& r) {
|
||||
@@ -237,6 +227,7 @@ void unset_compaction_manager(http_context& ctx, routes& r) {
|
||||
cm::get_compaction_history.unset(r);
|
||||
cm::get_compaction_info.unset(r);
|
||||
ss::get_compaction_throughput_mb_per_sec.unset(r);
|
||||
ss::set_compaction_throughput_mb_per_sec.unset(r);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,13 +13,11 @@ namespace seastar::httpd {
|
||||
class routes;
|
||||
}
|
||||
|
||||
namespace compaction {
|
||||
class compaction_manager;
|
||||
}
|
||||
|
||||
namespace api {
|
||||
struct http_context;
|
||||
void set_compaction_manager(http_context& ctx, seastar::httpd::routes& r, seastar::sharded<compaction::compaction_manager>& cm);
|
||||
void set_compaction_manager(http_context& ctx, seastar::httpd::routes& r, seastar::sharded<compaction_manager>& cm);
|
||||
void unset_compaction_manager(http_context& ctx, seastar::httpd::routes& r);
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user