Skip to content

Commit e4731b7

Browse files
[CLIENT-3805] Add missing app_id attribute to Cluster class for extended metrics (#864)
There is missing code coverage for returning None from the Cluster class's app_id attribute, but that is fine because it is hard to reproduce that behavior and it's documented that as_cluster.app_id can be NULL in the C client Extra changes: - Allow client config option app_id to be set to None. - Docs: fix app_id and cluster_names' types to be Optional[str] since they can be set to None. - Docs: set default value for client config's app_id and cluster_name option to None. - Docs: Update outdated documentation for the behavior of client config's cluster_name. - Make a standalone script for user agent test to make easier to run locally - In user agent standalone test, also check the server received the app_id set in the Python client's client config --------- Co-authored-by: Julian Nguyen <109386615+juliannguyen4@users.noreply.github.com>
1 parent a040350 commit e4731b7

File tree

9 files changed

+183
-105
lines changed

9 files changed

+183
-105
lines changed

.github/workflows/smoke-tests.yml

Lines changed: 23 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ jobs:
250250
type:
251251
- sanitizer
252252
- dont_validate_keys
253+
- lowest_supported_server_version
253254
fail-fast: false
254255
runs-on: ubuntu-22.04
255256
needs: build
@@ -271,7 +272,7 @@ jobs:
271272
- if: ${{ matrix.type == 'sanitizer' }}
272273
run: echo WHEEL_GH_ARTIFACT_NAME=wheel-${{ env.LOWEST_SUPPORTED_PY_VERSION }}-sanitizer >> $GITHUB_ENV
273274

274-
- if: ${{ matrix.type == 'dont_validate_keys' }}
275+
- if: ${{ matrix.type != 'sanitizer' }}
275276
run: echo WHEEL_GH_ARTIFACT_NAME=wheel-${{ env.LOWEST_SUPPORTED_PY_VERSION }} >> $GITHUB_ENV
276277

277278
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
@@ -294,7 +295,7 @@ jobs:
294295
- uses: ./.github/actions/run-ee-server
295296
with:
296297
registry-name: ${{ env.REGISTRY_NAME }}
297-
server-tag: ${{ env.SERVER_TAG }}
298+
server-tag: ${{ matrix.type == 'lowest_supported_server_version' && vars.LOWEST_SUPPORTED_SERVER_VERSION || env.SERVER_TAG }}
298299
registry-username: ${{ env.REGISTRY_NAME == 'docker.io' && secrets.DOCKER_HUB_BOT_USERNAME || secrets.QE_DOCKER_REGISTRY_USERNAME }}
299300
registry-password: ${{ env.REGISTRY_NAME == 'docker.io' && secrets.DOCKER_HUB_BOT_PW || secrets.QE_DOCKER_REGISTRY_PASSWORD }}
300301

@@ -447,15 +448,16 @@ jobs:
447448
- run: python3 test_${{ matrix.suffix }}.py
448449
working-directory: test/standalone
449450

450-
test-misc:
451+
# This is an e2e test to check that the user agent was sent correctly to the server
452+
test-user-agent:
451453
needs: build
452454
strategy:
453455
fail-fast: false
454456
matrix:
455-
test-case:
456-
- "lowest_supported_server_version"
457-
- "user_agent_with_ce"
458-
- "user_agent_with_ee"
457+
test-script-args:
458+
- "false"
459+
- "true"
460+
- "true my_app_id"
459461
runs-on: ubuntu-22.04
460462
steps:
461463
- name: Harden the runner (Audit all outbound calls)
@@ -479,18 +481,8 @@ jobs:
479481
username: ${{ env.REGISTRY_NAME == 'docker.io' && secrets.DOCKER_HUB_BOT_USERNAME || secrets.QE_DOCKER_REGISTRY_USERNAME }}
480482
password: ${{ env.REGISTRY_NAME == 'docker.io' && secrets.DOCKER_HUB_BOT_PW || secrets.QE_DOCKER_REGISTRY_PASSWORD }}
481483

482-
- name: Run Aerospike server
483-
if: ${{ matrix.test-case != 'user_agent_with_ee' }}
484-
run: docker run -d --name aerospike -p 3000:3000 -e DEFAULT_TTL=2592000 ${{ env.REGISTRY_NAME }}/aerospike/aerospike-server:${{ env.SERVER_TAG }}
485-
486-
- uses: ./.github/actions/wait-for-ce-server-to-start
487-
if: ${{ matrix.test-case != 'user_agent_with_ee' }}
488-
with:
489-
container-name: aerospike
490-
491-
- id: run-ee-server
492-
uses: ./.github/actions/run-ee-server
493-
if: ${{ matrix.test-case == 'user_agent_with_ee' }}
484+
- uses: ./.github/actions/run-ee-server
485+
if: ${{ startsWith(matrix.test-script-args, 'true') }}
494486
with:
495487
registry-name: ${{ env.REGISTRY_NAME }}
496488
server-tag: ${{ env.SERVER_TAG }}
@@ -499,77 +491,18 @@ jobs:
499491
# The user agent can also send the client's username if app-id is not set by the client
500492
env-vars: 'SECURITY=1'
501493

502-
- name: Install test dependencies
503-
if: ${{ matrix.test-case == 'lowest_supported_server_version' }}
504-
run: pip install -r test/requirements.txt
505-
506-
- name: Create config.conf
507-
if: ${{ matrix.test-case == 'lowest_supported_server_version' }}
508-
run: cp config.conf.template config.conf
509-
working-directory: test
494+
- if: ${{ startsWith(matrix.test-script-args, 'false') }}
495+
run: docker run -d -p 3000:3000 --name aerospike -e DEFAULT_TTL=2592000 ${{ env.REGISTRY_NAME }}/aerospike/aerospike-server:${{ env.SERVER_TAG }}
510496

511-
- name: Run tests
512-
if: ${{ matrix.test-case == 'lowest_supported_server_version' }}
513-
run: python -m pytest ./new_tests
514-
working-directory: test
497+
- if: ${{ startsWith(matrix.test-script-args, 'false') }}
498+
uses: ./.github/actions/wait-for-ce-server-to-start
499+
with:
500+
container-name: aerospike
515501

516-
- if: ${{ startsWith(matrix.test-case, 'user_agent') }}
517502
# Even for server versions < 8.1 that don't support user agent, this client version should still work on older servers
518-
name: Run client in background
519-
run: |
520-
set -x
521-
cat <<-EOF >> run-client-in-bg.py
522-
import aerospike
523-
import time
524-
import sys
525-
config = {
526-
"hosts": [
527-
("127.0.0.1", 3000)
528-
]
529-
}
530-
if "${{ endsWith(matrix.test-case, 'with_ee') }}" == "true":
531-
config["user"] = "superuser"
532-
config["password"] = "superuser"
533-
client = aerospike.client(config)
534-
while True:
535-
time.sleep(1)
536-
EOF
537-
# This shell will be closed once this step completes
538-
nohup python3 run-client-in-bg.py &
539-
# TODO: We want to check if the python script raises a syntax error or not. (should fail immediately)
540-
# When background processes fail, this step won't fail.
541-
542-
- if: ${{ startsWith(matrix.test-case, 'user_agent') }}
543-
name: Confirm that the user agent shows the correct client language and version (only for server 8.1)
544-
run: |
545-
if [[ "${{ endsWith(matrix.test-case, 'with_ee') }}" == "true" ]]; then
546-
CREDENTIALS="-U superuser -P superuser"
547-
fi
548-
549-
server_version=$(docker run --network host aerospike/aerospike-tools asinfo $CREDENTIALS -v "build")
550-
major_num=$(echo $server_version | cut -d '.' -f 1)
551-
minor_num=$(echo $server_version | cut -d '.' -f 2)
552-
if [[ $major_num -lt 8 || ( $major_num -eq 8 && $minor_num -lt 1 ) ]]; then
553-
exit 0
554-
fi
555-
556-
info_cmd_response=$(docker run --network host aerospike/aerospike-tools asinfo $CREDENTIALS -v "user-agents" | head -n 1)
557-
echo "Info command response: $info_cmd_response"
558-
# Response format: user-agent=<base64 encoded string>:...
559-
user_agent_base64_encoded=$(echo $info_cmd_response | perl -n -E 'say $1 if m/user-agent= ([a-zA-Z0-9+\/=]+) :/x')
560-
echo $user_agent_base64_encoded
561-
user_agent=$(echo $user_agent_base64_encoded | base64 -d)
562-
echo $user_agent
563-
# TODO: combine into one regex
564-
# User agent format: <format-version>,<client language>-<version>,...
565-
client_language=$(echo $user_agent | perl -n -E 'say $1 if m/[0-9]+, ([a-z]+) -/x')
566-
client_version=$(echo $user_agent | perl -n -E 'say $1 if m/[0-9]+,[a-z]+- ([0-9.a-zA-Z+]+),/x')
567-
echo $client_language
568-
echo $client_version
569-
570-
test "$client_language" = "python"
571-
572-
# Client version from user agent
573-
expected_client_version=$(python3 -m pip show aerospike | grep -i version | cut -d " " -f 2-)
574-
test "$client_version" = "$expected_client_version"
575-
shell: bash
503+
- name: Run client in background
504+
run: ./test-user-agent-e2e.bash ${{ matrix.test-script-args }}
505+
working-directory: test/standalone
506+
507+
- if: ${{ !cancelled() }}
508+
run: docker logs aerospike

aerospike_helpers/metrics/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ class Cluster:
105105
106106
Attributes:
107107
cluster_name (Optional[str]): Expected cluster name for all nodes. May be :py:obj:`None`.
108+
app_id (str): Application identifier. Will be set to the client's username if not set to a string in the client
109+
config's ``app_id`` option. If the client does not have a username, this will be set to ``not-set``.
108110
invalid_node_count (int): Count of add node failures in the most recent cluster tend iteration.
109111
command_count (int): Command count. The value is cumulative and not reset per metrics interval.
110112
retry_count (int): Command retry count. There can be multiple retries for a single command.

doc/aerospike.rst

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -767,10 +767,20 @@ Only the `hosts` key is required; the rest of the keys are optional.
767767
Compress data for transmission if the object size is greater than a given number of bytes
768768

769769
Default: ``0``, meaning 'never compress'
770-
* **cluster_name** (:class:`str`)
771-
Only server nodes matching this name will be used when determining the cluster name.
772-
* **app_id** (:class:`str`)
770+
* **cluster_name** (:class:`Optional[str]`)
771+
Expected cluster name. If set to a string value, the ``cluster_name`` must match the cluster-name field
772+
in the service section in each server configuration. This ensures that the specified
773+
seed nodes belong to the expected cluster on startup. If not, the client will refuse
774+
to add the node to the client's view of the cluster.
775+
776+
Default: :py:obj:`None`
777+
* **app_id** (:class:`Optional[str]`)
773778
Application identifier.
779+
780+
If this is set to :py:obj:`None`, this is set to the client's username by default. If client doesn't have a username,
781+
this is set to ``not-set``.
782+
783+
Default: :py:obj:`None`
774784
* **rack_id** (:class:`int`)
775785
Rack id where this client instance resides.
776786

src/main/client/type.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ static int AerospikeClient_Type_Init(AerospikeClient *self, PyObject *args,
12201220

12211221
PyObject *py_app_id = NULL;
12221222
retval = PyDict_GetItemStringRef(py_config, "app_id", &py_app_id);
1223-
if (retval == 1) {
1223+
if (retval == 1 && !Py_IsNone(py_app_id)) {
12241224
const char *str = convert_pyobject_to_str(py_app_id);
12251225
if (!str) {
12261226
Py_DECREF(py_app_id);

src/main/conversions.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,24 @@ PyObject *create_py_cluster_from_as_cluster(as_error *error_p,
10541054
PyObject_SetAttrString(py_cluster, "cluster_name", Py_None);
10551055
}
10561056

1057+
// App Id is optional (declared in client config)
1058+
PyObject *py_app_id = NULL;
1059+
if (cluster->app_id) {
1060+
py_app_id = PyUnicode_FromString(cluster->app_id);
1061+
if (!py_app_id) {
1062+
goto error;
1063+
}
1064+
}
1065+
else {
1066+
py_app_id = Py_NewRef(Py_None);
1067+
}
1068+
1069+
int retval = PyObject_SetAttrString(py_cluster, "app_id", py_app_id);
1070+
Py_DECREF(py_app_id);
1071+
if (retval == -1) {
1072+
goto error;
1073+
}
1074+
10571075
PyObject *py_invalid_node_count =
10581076
PyLong_FromUnsignedLong(cluster->invalid_node_count);
10591077
PyObject_SetAttrString(py_cluster, "invalid_node_count",

test/new_tests/test_connect.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from aerospike import exception as e
88

99
import aerospike
10+
from contextlib import nullcontext
1011

1112

1213
@contextmanager
@@ -145,18 +146,28 @@ def test_connect_positive_shm_not_enabled(self):
145146
assert client.is_connected()
146147
assert client.shm_key() is None
147148

148-
def test_connect_positive_cluster_name(self):
149+
@pytest.mark.parametrize(
150+
"cluster_name",
151+
[
152+
None,
153+
# This test case is for code coverage purposes
154+
"invalid-cluster-name"
155+
]
156+
)
157+
def test_connect_with_cluster_name(self, cluster_name):
149158
"""
150-
Invoke connect() giving a cluster name. This is just a usage test (doesn't care if the server's cluster name
151-
matches or not)
159+
Invoke connect() giving a cluster name
152160
"""
153161
config = self.connection_config.copy()
154-
config["cluster_name"] = "test-cluster"
162+
config["cluster_name"] = cluster_name
155163

156-
try:
157-
self.client = aerospike.client(config).connect()
158-
except e.ClientError:
159-
pass
164+
if cluster_name is None:
165+
cm = nullcontext()
166+
else:
167+
cm = pytest.raises(e.ClientError)
168+
169+
with cm:
170+
self.client = aerospike.client(config)
160171

161172
def test_connect_positive_reconnect(self):
162173
"""

test/new_tests/test_metrics.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import time
88
from typing import Optional
99
import re
10+
import aerospike
11+
from .test_base_class import TestBaseClass
1012
from importlib.metadata import version
1113

1214
# Flags for testing callbacks
@@ -160,7 +162,19 @@ def test_metrics_writer(self):
160162
for item in metrics_log_filenames:
161163
os.remove(item)
162164

163-
def test_setting_metrics_policy_custom_settings(self):
165+
@pytest.fixture(scope="function", params=[None, "my_app"])
166+
def get_client_and_app_id(self, request):
167+
config = TestBaseClass.get_connection_config()
168+
config["app_id"] = request.param
169+
client = aerospike.client(config)
170+
171+
yield request.param, client
172+
173+
client.close()
174+
175+
def test_setting_metrics_policy_custom_settings(self, get_client_and_app_id):
176+
app_id, client = get_client_and_app_id
177+
164178
self.metrics_log_folder = "./metrics-logs"
165179

166180
# Save bucket count for testing later
@@ -175,9 +189,9 @@ def test_setting_metrics_policy_custom_settings(self):
175189
labels={"a": "b"},
176190
)
177191

178-
self.as_connection.enable_metrics(policy=policy)
192+
client.enable_metrics(policy=policy)
179193
time.sleep(3)
180-
self.as_connection.disable_metrics()
194+
client.disable_metrics()
181195

182196
# These callbacks should've been called
183197
assert enable_triggered is True
@@ -194,6 +208,14 @@ def test_setting_metrics_policy_custom_settings(self):
194208
assert type(cluster.command_count) == int
195209
assert type(cluster.retry_count) == int
196210
assert type(cluster.nodes) == list
211+
if type(app_id) == str:
212+
assert cluster.app_id == app_id
213+
elif TestBaseClass.auth_in_use():
214+
# Or username if the app_id is not set
215+
assert cluster.app_id == TestBaseClass.user
216+
else:
217+
assert cluster.app_id == "not-set"
218+
197219
# Also check the Node and ConnectionStats objects in the Cluster object were populated
198220
for node in cluster.nodes:
199221
assert type(node) == Node
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import aerospike
2+
import time
3+
import sys
4+
config = {
5+
"hosts": [
6+
("127.0.0.1", 3000)
7+
]
8+
}
9+
print(config)
10+
if sys.argv[1] == "true":
11+
config["user"] = "superuser"
12+
config["password"] = "superuser"
13+
if len(sys.argv) == 3:
14+
config["app_id"] = sys.argv[2]
15+
client = aerospike.client(config)
16+
while True:
17+
time.sleep(1)

0 commit comments

Comments
 (0)