-
Notifications
You must be signed in to change notification settings - Fork 92
đ (ZENKO-5303) make voting and priority right #2452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weâll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development/2.15
Are you sure you want to change the base?
Changes from all commits
e4d966a
f29e6c9
da83b20
c0202e3
346d789
8f83d19
7236e42
35054f9
0f2d0ea
709d40e
f313b49
5f71df4
cc5c73c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,20 @@ | ||
| BITNAMI_REMOTE := bitnami-containers | ||
| BITNAMI_REPO := https://github.com/bitnami/containers.git | ||
| BITNAMI_UPSTREAM_MAIN_REF := $(BITNAMI_REMOTE)/main | ||
|
|
||
| IMAGES := mongodb-sharded mongodb-exporter os-shell | ||
|
|
||
| BITNAMI_mongodb_sharded_PATH := 8.0/debian-12 | ||
| # Latest commit on upstream main known to still contain | ||
| # bitnami/mongodb-sharded/8.0/debian-12. | ||
| BITNAMI_mongodb_sharded_REF := 48a109547d39cd8cf8a5d4058d832ecb5844829e | ||
| BITNAMI_mongodb_sharded_REF := 657585595c550d4dc107a4e6cd3a598a9d284eec | ||
|
|
||
| BITNAMI_mongodb_exporter_PATH := 0/debian-12 | ||
| BITNAMI_mongodb_exporter_REF := $(BITNAMI_UPSTREAM_MAIN_REF) | ||
| BITNAMI_mongodb_exporter_REF := main | ||
|
|
||
| BITNAMI_os_shell_PATH := 12/debian-12 | ||
| BITNAMI_os_shell_REF := $(BITNAMI_UPSTREAM_MAIN_REF) | ||
| BITNAMI_os_shell_REF := main | ||
|
|
||
| .PHONY: create-remote fetch-remote vendor-sync $(addprefix vendor-sync-,$(IMAGES)) $(addprefix update-vendor-branch-,$(IMAGES)) | ||
| .PHONY: create-remote vendor-sync | ||
|
|
||
| normalize = $(subst -,_,$1) | ||
| bitnami_path = $(BITNAMI_$(call normalize,$1)_PATH) | ||
|
|
@@ -25,15 +24,20 @@ vendor_branch = vendor/$1/$(call bitnami_path,$1) | |
| create-remote: | ||
| @git remote get-url $(BITNAMI_REMOTE) >/dev/null 2>&1 || git remote add $(BITNAMI_REMOTE) $(BITNAMI_REPO) | ||
|
|
||
| fetch-remote: create-remote | ||
| # Fetch full history from Bitnami main so subtree split can see full subtree history. | ||
| git fetch $(BITNAMI_REMOTE) main | ||
|
|
||
| update-vendor-branch-%: fetch-remote | ||
| update-vendor-branch-%: create-remote | ||
| # Shallow-fetch only the pinned ref; full upstream history is unnecessary for a --squash merge. | ||
| git fetch --depth 1 --filter=blob:none $(BITNAMI_REMOTE) $(call bitnami_ref,$*) | ||
| -git branch -D $(call vendor_branch,$*) | ||
| git subtree split --prefix=bitnami/$*/$(call bitnami_path,$*) $(call bitnami_ref,$*) -b $(call vendor_branch,$*) | ||
| sha=$$(git rev-parse FETCH_HEAD) && \ | ||
| wt=$$(mktemp -d) && \ | ||
| trap 'git worktree remove --force "$$wt" 2>/dev/null' EXIT && \ | ||
| git worktree add --no-checkout --detach "$$wt" "$$sha" && \ | ||
| git -C "$$wt" sparse-checkout set --no-cone bitnami/$*/$(call bitnami_path,$*) && \ | ||
| git -C "$$wt" checkout && \ | ||
|
Comment on lines
+33
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need a checkout? |
||
| git -C "$$wt" subtree split --prefix=bitnami/$*/$(call bitnami_path,$*) "$$sha" -b $(call vendor_branch,$*) | ||
|
|
||
| vendor-sync-%: update-vendor-branch-% | ||
| cd "$$(git rev-parse --show-toplevel)" && \ | ||
| git subtree merge --prefix=solution-base/images/$*/debian-12 $(call vendor_branch,$*) --squash | ||
|
|
||
| vendor-sync: $(addprefix vendor-sync-,$(IMAGES)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -910,14 +910,17 @@ mongodb_is_secondary_node_ready() { | |
| local -r port="${2:?port is required}" | ||
|
|
||
| debug "Waiting for the node to be marked as secondary" | ||
| # mongosh prints a connection banner holding 'directConnection=true', so matching | ||
| # on 'true' would always succeed. Match a dedicated sentinel instead, built at | ||
| # runtime so the positive value never appears verbatim in the submitted script. | ||
| result=$( | ||
| mongodb_execute_print_output "$MONGODB_INITIAL_PRIMARY_ROOT_USER" "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" "admin" "$MONGODB_INITIAL_PRIMARY_HOST" "$MONGODB_INITIAL_PRIMARY_PORT_NUMBER" <<EOF | ||
| rs.status().members.filter(m => m.name === '$node:$port' && m.stateStr === 'SECONDARY').length === 1 | ||
| print("IS_SECONDARY_" + (rs.status().members.some(m => m.name === '$node:$port' && m.stateStr === 'SECONDARY') ? "YES" : "NO")) | ||
| EOF | ||
| ) | ||
| debug "$result" | ||
|
|
||
| grep -q "true" <<<"$result" | ||
| grep -q "IS_SECONDARY_YES" <<<"$result" | ||
| } | ||
|
|
||
| ######################## | ||
|
|
@@ -951,6 +954,35 @@ EOF | |
| grep -q "ok: 1" <<<"$result" | ||
| } | ||
|
|
||
| ######################## | ||
| # Get if secondary node already has voting rights | ||
|
DarkIsDude marked this conversation as resolved.
|
||
| # Globals: | ||
| # MONGODB_* | ||
| # Arguments: | ||
| # $1 - node | ||
| # $2 - port | ||
| # Returns: | ||
| # Boolean | ||
| ######################### | ||
| mongodb_secondary_node_has_voting_rights() { | ||
| local -r node="${1:?node is required}" | ||
| local -r port="${2:?port is required}" | ||
| local result | ||
|
|
||
| debug "Checking voting rights of the node" | ||
| # mongosh prints a connection banner holding 'directConnection=true', so matching | ||
| # on 'true' would always succeed. Match a dedicated sentinel instead, built at | ||
| # runtime so the positive value never appears verbatim in the submitted script. | ||
| result=$( | ||
| mongodb_execute_print_output "$MONGODB_INITIAL_PRIMARY_ROOT_USER" "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" "admin" "$MONGODB_INITIAL_PRIMARY_HOST" "$MONGODB_INITIAL_PRIMARY_PORT_NUMBER" <<EOF | ||
| print("HAS_VOTES_" + (rs.conf().members.some(m => m.host === '$node:$port' && m.votes > 0 && m.priority > 0) ? "YES" : "NO")) | ||
| EOF | ||
| ) | ||
| debug "$result" | ||
|
|
||
| grep -q "HAS_VOTES_YES" <<<"$result" | ||
| } | ||
|
|
||
| ######################## | ||
| # Get if hidden node is pending | ||
| # Globals: | ||
|
|
@@ -1187,7 +1219,15 @@ mongodb_configure_secondary() { | |
| exit 1 | ||
| fi | ||
| mongodb_wait_confirmation "$node" "$port" | ||
| fi | ||
|
|
||
| # Grant voting rights to the node if it does not have them yet. This must be | ||
| # done even when the node is already in the cluster: a previous attempt may | ||
| # have added it with votes/priority 0 and failed (or been restarted) before | ||
| # granting voting rights, leaving the node stuck without them. | ||
| if mongodb_secondary_node_has_voting_rights "$node" "$port"; then | ||
| info "Node already has voting rights" | ||
| else | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will not behave properly when we have 9-replicas, where 2 nodes are always "non voting". This is expected, and we must not try to change that (in particular, the voting nodes must be in precisely the expected DC for proper redundancy) â the change will make the startup slower on these extra secondaries -(re-)trying to make them voting-, possibly with the extra risk of changing the set of voters...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @DarkIsDude did you try that case? Looking at the code below, it seems that the script would exit if it fails to configure secondary node voting: does that function already silently handle the case where we reached the max number of voters already?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes â tested on a real 9-replica shard, both with the current development/2.15 image and with this branch. Short answer to your question: no, it is not silently handled â it exits. But that is pre-existing behaviour, not something this PR introduces. First: your review caught a real bug, just not the one we were discussing. Secondly: "the extra risk of changing the set of voters": The only path where this branch differs from development/2.15 is data dir empty and node already in rs.conf() â i.e. PVC loss on an existing member. Last one: "does that function silently handle reaching the max number of voters?" No. Reproduced, scaling 3 â 9: It does recover: on restart the data dir is no longer empty, config is skipped, and the member settles as a working non-voting secondary. Net cost is one crash + ~3 min per extra member, and the final topology is the expected 9 members / 7 voters.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ack for the unchanged behavior, but I guess it's best to fix it, right? |
||
| # Ensure that secondary nodes do not count as voting members until they are fully initialized | ||
| # https://docs.mongodb.com/manual/reference/method/rs.add/#behavior | ||
| if ! retry_while "mongodb_is_secondary_node_ready $node $port" "$MONGODB_INIT_RETRY_ATTEMPTS" "$MONGODB_INIT_RETRY_DELAY"; then | ||
|
|
@@ -1198,17 +1238,16 @@ mongodb_configure_secondary() { | |
| # Grant voting rights to node | ||
| # https://docs.mongodb.com/manual/tutorial/modify-psa-replica-set-safely/ | ||
| if ! retry_while "mongodb_configure_secondary_node_voting $node $port" "$MONGODB_INIT_RETRY_ATTEMPTS" "$MONGODB_INIT_RETRY_DELAY"; then | ||
| error "Secondary node did not get marked as secondary" | ||
| error "Secondary node did not get granted voting rights" | ||
| exit 1 | ||
| fi | ||
| fi | ||
|
|
||
| # Mark node as readable. This is necessary in cases where the PVC is lost | ||
| if is_boolean_yes "$MONGODB_SET_SECONDARY_OK"; then | ||
| mongodb_execute_print_output "$MONGODB_INITIAL_PRIMARY_ROOT_USER" "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" "admin" <<EOF | ||
| # Mark node as readable. This is necessary in cases where the PVC is lost | ||
| if is_boolean_yes "$MONGODB_SET_SECONDARY_OK"; then | ||
| mongodb_execute_print_output "$MONGODB_INITIAL_PRIMARY_ROOT_USER" "$MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD" "admin" <<EOF | ||
| rs.secondaryOk() | ||
| EOF | ||
| fi | ||
|
|
||
| fi | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| """Regression tests for the vendored bitnami ``libmongodb.sh``. | ||
|
|
||
| These guard against a class of bug where a helper decides a boolean by | ||
| substring-matching mongosh's output. mongosh always prints a connection | ||
| banner containing ``directConnection=true`` on stdout, so ``grep -q "true"`` | ||
| matches unconditionally and the helper can never return false. | ||
|
|
||
| That is exactly how ``mongodb_secondary_node_has_voting_rights`` shipped | ||
| (bitnami/containers#95156): the voting-rights grant became unreachable and | ||
| secondaries stayed at votes:0/priority:0 forever. | ||
| """ | ||
|
|
||
| import re | ||
| import subprocess | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| LIBMONGODB = ( | ||
| Path(__file__).resolve().parents[2] | ||
| / "solution-base" | ||
| / "images" | ||
| / "mongodb-sharded" | ||
| / "debian-12" | ||
| / "rootfs" | ||
| / "opt" | ||
| / "bitnami" | ||
| / "scripts" | ||
| / "libmongodb.sh" | ||
| ) | ||
|
|
||
| # Verbatim mongosh 2.9.2 preamble. The ``directConnection=true`` substring is | ||
| # the whole point of these tests: a fixture without it would happily pass on | ||
| # the broken implementation. | ||
| MONGOSH_BANNER = ( | ||
| "Current Mongosh Log ID:\t6a7ddf270f5d3c6ed07e2c0c\n" | ||
| "Connecting to:\t\tmongodb://h:27017/admin" | ||
| "?directConnection=true&appName=mongosh+2.9.2\n" | ||
| "Using MongoDB:\t\t8.0.13\n" | ||
| "Using Mongosh:\t\t2.9.2\n" | ||
| "\n" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def source() -> str: | ||
| return LIBMONGODB.read_text(encoding="utf-8") | ||
|
|
||
|
|
||
| def extract_function(name: str, source: str) -> str: | ||
| """Return the shell source of ``name``. | ||
|
|
||
| ``libmongodb.sh`` sources its dependencies from absolute ``/opt/bitnami`` | ||
| paths, so it cannot be sourced outside the image; the function under test | ||
| is lifted out instead. | ||
| """ | ||
| match = re.search(rf"^{re.escape(name)}\(\) \{{$", source, re.MULTILINE) | ||
| assert match is not None, f"{name} not found in {LIBMONGODB.name}" | ||
|
|
||
| body = [] | ||
| for line in source[match.start():].splitlines(keepends=True): | ||
| body.append(line) | ||
| if line.rstrip("\n") == "}": | ||
| return "".join(body) | ||
| raise AssertionError(f"unterminated function {name}") | ||
|
|
||
|
|
||
| def call_with_mongosh_output(function: str, call: str, answer: str) -> int: | ||
| """Run ``call`` with ``mongodb_execute_print_output`` stubbed out. | ||
|
|
||
| The stub replays what mongosh actually writes to stdout: the connection | ||
| banner followed by the evaluated result. | ||
| """ | ||
| script = f""" | ||
| set -uo pipefail | ||
| debug() {{ :; }} | ||
| mongodb_execute_print_output() {{ | ||
| cat <<'MONGOSH_OUTPUT' | ||
| {MONGOSH_BANNER}{answer} | ||
| MONGOSH_OUTPUT | ||
| }} | ||
| MONGODB_INITIAL_PRIMARY_ROOT_USER=root | ||
| MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=password | ||
| MONGODB_INITIAL_PRIMARY_HOST=h | ||
| MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017 | ||
| {function} | ||
| {call} | ||
| """ | ||
| return subprocess.run(["bash", "-c", script], capture_output=True, text=True).returncode | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("answer", "expected_rc"), | ||
| [("HAS_VOTES_YES", 0), ("HAS_VOTES_NO", 1)], | ||
| ) | ||
| def test_has_voting_rights_reflects_the_query_result(source, answer, expected_rc): | ||
| """A node without votes must be reported as such, banner notwithstanding.""" | ||
| rc = call_with_mongosh_output( | ||
| extract_function("mongodb_secondary_node_has_voting_rights", source), | ||
| "mongodb_secondary_node_has_voting_rights node 27017", | ||
| answer, | ||
| ) | ||
| assert rc == expected_rc | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("answer", "expected_rc"), | ||
| [("IS_SECONDARY_YES", 0), ("IS_SECONDARY_NO", 1)], | ||
| ) | ||
| def test_is_secondary_node_ready_reflects_the_query_result(source, answer, expected_rc): | ||
| rc = call_with_mongosh_output( | ||
| extract_function("mongodb_is_secondary_node_ready", source), | ||
| "mongodb_is_secondary_node_ready node 27017", | ||
| answer, | ||
| ) | ||
| assert rc == expected_rc | ||
|
|
||
|
|
||
| def test_no_helper_matches_a_bare_boolean_in_mongosh_output(source): | ||
| """Forbid the idiom that caused the regression. | ||
|
|
||
| A bare ``true``/``false`` match is always satisfied by the connection | ||
| banner. Deciding a boolean therefore requires either a dedicated sentinel | ||
| or a pattern anchored on the mongosh prompt, as | ||
| ``mongodb_is_primary_node_up`` already does. | ||
| """ | ||
| offenders = [ | ||
| (number, line.strip()) | ||
| for number, line in enumerate(source.splitlines(), start=1) | ||
| if re.search(r"""grep\s+-q\w*\s+(["'])(true|false)\1""", line) | ||
| ] | ||
| assert not offenders, ( | ||
| "mongosh prints 'directConnection=true' in its connection banner, so " | ||
| "these matches always succeed:\n" | ||
| + "\n".join(f" {LIBMONGODB.name}:{n}: {t}" for n, t in offenders) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for Git, this is actually
$(call bitnami_ref,$*).So best to do it the other way: