diff --git a/.github/benchmark/benchmark.jmx b/.github/benchmark/benchmark.jmx new file mode 100644 index 0000000000..97aecff820 --- /dev/null +++ b/.github/benchmark/benchmark.jmx @@ -0,0 +1,327 @@ + + + + + + Parametrized LDAP benchmark. Admin bind is cached once per thread (Once Only Controller, labelled ADMIN_CONNECT, excluded from metrics). Data ops (ADD/SEARCH/COMPARE/MODIFY/DELETE/READD) reuse the cached admin connection. Entries use mail as the naming/searchable attribute (equality-indexed by default on BOTH OpenDJ and OpenLDAP); only mail + objectClass (both indexed on both servers) are stored, keeping the write cost symmetric. Every created value is unique: ADD uses a per-iteration counter, READD uses a UUID. The measured user authentication is a single bind/unbind (test=sbind, own connection) after MODIFY has set the userPassword. + false + true + false + + + + + + + + continue + + false + -1 + + ${__P(threads,200)} + ${__P(rampup,0)} + true + ${__P(duration,300)} + + + + + + 1 + + 1 + iter + + true + false + + + + + + + + ${__P(host,localhost)} + ${__P(port,1389)} + ou=People,${__P(basedn,dc=example\,dc=com)} + 2 + + + + false + false + 60000 + false + false + ${__P(adminbinddn,cn=Directory Manager)} + ${__P(adminbindpw,password)} + + + + + bind + + + + + + + + + + 2 + + + + false + false + + false + false + + + + + + + add + mail=u_${__threadNum}_${iter}@test.com + + + + mail + u_${__threadNum}_${iter}@test.com + = + + + objectClass + top + = + + + objectClass + locality + = + + + objectClass + extensibleObject + = + + + + + + + + + + + + 2 + 0 + 0 + mail:dn:objectClass + false + false + + false + false + + + + + + + search + + (mail=u_${__threadNum}_${iter}@test.com) + + + + + + + + + 2 + + + + false + false + + false + false + + + mail=u_${__threadNum}_${iter}@test.com + mail=u_${__threadNum}_${iter}@test.com + + + compare + + + + + + + + + 2 + + + + false + false + + false + false + + + + + + + modify + mail=u_${__threadNum}_${iter}@test.com + + + + description + mod_${__threadNum}_${iter} + replace + = + + + userPassword + ${__P(benchpw,benchPass1)} + replace + = + + + + + + + + + ${__P(host,localhost)} + ${__P(port,1389)} + ou=People,${__P(basedn,dc=example\,dc=com)} + 2 + + + + false + false + 60000 + false + false + mail=u_${__threadNum}_${iter}@test.com,ou=People,${__P(basedn,dc=example\,dc=com)} + ${__P(benchpw,benchPass1)} + + + + + sbind + + + + + + + + + 2 + + + + false + false + + false + false + + + + + + + delete + mail=u_${__threadNum}_${iter}@test.com + + + + + + + + + 2 + + + + false + false + + false + false + + + + + + + add + mail=u_${__UUID}@test.com + + + + mail + u_${__UUID}@test.com + = + + + objectClass + top + = + + + objectClass + locality + = + + + objectClass + extensibleObject + = + + + + + + + + + diff --git a/.github/benchmark/compare-opendj.sh b/.github/benchmark/compare-opendj.sh new file mode 100644 index 0000000000..1825921229 --- /dev/null +++ b/.github/benchmark/compare-opendj.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Copyright 2026 3A Systems, LLC. +# +# Compare two OpenDJ Docker images with the LDAP benchmark (.github/benchmark/benchmark.jmx) and +# append the comparison report to $GITHUB_STEP_SUMMARY. Both sides are OpenDJ, so no per-server +# hashing/index setup is needed (identical product => identical default password scheme). +# +# Usage: +# compare-opendj.sh +# Env: THREADS (default 200), DURATION (default 150), JMETER_VERSION (default 5.6.3). +set -euo pipefail + +A_NAME="${1:?server A name required}" +A_IMAGE="${2:?server A image required}" +B_NAME="${3:?server B name required}" +B_IMAGE="${4:?server B image required}" + +THREADS="${THREADS:-200}" +DURATION="${DURATION:-150}" +JMETER_VERSION="${JMETER_VERSION:-5.6.3}" +BASEDN="dc=example,dc=com" +BENCHPW="benchPass1" +HERE="$(cd "$(dirname "$0")" && pwd)" # .github/benchmark + +# ---------------------------------------------------------------- dependencies +if ! command -v ldapsearch >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y -qq ldap-utils jq +fi +JM="$HOME/jmeter/apache-jmeter-$JMETER_VERSION/bin/jmeter" +if [ ! -x "$JM" ]; then + mkdir -p "$HOME/jmeter" + curl -fsSL "https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-$JMETER_VERSION.tgz" -o /tmp/jmeter.tgz + tar -xzf /tmp/jmeter.tgz -C "$HOME/jmeter" +fi + +wait_dj() { # poll OpenDJ readiness on localhost:1389 + for _ in $(seq 1 90); do + ldapsearch -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \ + -b "$BASEDN" -s base dn >/dev/null 2>&1 && return 0 + sleep 2 + done + return 1 +} + +# bench_one -> prints the captured server version (stdout only) +bench_one() { + local image="$1" out="$2" ver="" + docker rm -f opendj-bench >/dev/null 2>&1 || true + if docker run -d --name opendj-bench -p 1389:1389 \ + -e ROOT_PASSWORD=password -e BASE_DN="$BASEDN" -e ADD_BASE_ENTRY=--addBaseEntry \ + "$image" >/dev/null 2>&1; then + wait_dj || echo "WARN: $image not ready in time" >&2 + ldapadd -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \ + -f "$HERE/people.ldif" >/dev/null 2>&1 || true + ver="$( { ldapsearch -x -LLL -H ldap://localhost:1389 -D 'cn=Directory Manager' -w password \ + -b '' -s base fullVendorVersion 2>/dev/null || true; } | sed -n 's/^fullVendorVersion: //p')" + rm -rf "$out" "$out.jtl" + HEAP="-Xms1g -Xmx2g" "$JM" -n -t "$HERE/benchmark.jmx" \ + -Jhost=localhost -Jport=1389 -Jbasedn="$BASEDN" \ + -Jadminbinddn="cn=Directory Manager" -Jadminbindpw=password -Jbenchpw="$BENCHPW" \ + -Jthreads="$THREADS" -Jduration="$DURATION" -Jrampup=0 \ + -Jjmeter.reportgenerator.sample_filter='^(?!ADMIN_CONNECT).*' \ + -l "$out.jtl" -e -o "$out" > "$out.jmeter.out" 2>&1 || true + docker logs opendj-bench > "$out.docker.log" 2>&1 || true + # surface distinct error messages to the step log (stderr; stdout carries the version) + if [ -f "$out.jtl" ]; then + local errs + errs="$(awk -F',' 'NR==1{for(i=1;i<=NF;i++)h[$i]=i; next} + tolower($h["success"])=="false"{print $h["label"]" | "$h["responseCode"]" | "$h["responseMessage"]}' \ + "$out.jtl" 2>/dev/null | sort | uniq -c | sort -rn | head -10)" + [ -z "$errs" ] || { echo "[$out] errors (count | op | code | message):" >&2; echo "$errs" >&2; } + fi + else + echo "ERROR: failed to start image $image" >&2 + fi + docker rm -f opendj-bench >/dev/null 2>&1 || true + [ -n "$ver" ] || ver="$image" + printf '%s' "$ver" +} + +echo "Benchmarking ${A_NAME} (${A_IMAGE}) @ ${THREADS} threads / ${DURATION}s ..." +A_VER="$(bench_one "$A_IMAGE" a)" +echo "Benchmarking ${B_NAME} (${B_IMAGE}) @ ${THREADS} threads / ${DURATION}s ..." +B_VER="$(bench_one "$B_IMAGE" b)" + +{ + bash "$HERE/summary.sh" \ + "$A_NAME" a/statistics.json "$A_VER" "$A_IMAGE" \ + "$B_NAME" b/statistics.json "$B_VER" "$B_IMAGE" + echo "" + echo "### Notes" + echo "" + echo "- **${A_NAME}** = freshly built image; **${B_NAME}** = latest released image. Both are" + echo " OpenDJ, so they share the same default password storage scheme (hashing parity is automatic)." + echo "- The admin connection bind (\`ADMIN_CONNECT\`) is cached per thread and excluded; \`BIND\` is" + echo " the measured user authentication (\`test=sbind\`, single bind/unbind)." +} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}" diff --git a/.github/benchmark/people.ldif b/.github/benchmark/people.ldif new file mode 100644 index 0000000000..d6a350093e --- /dev/null +++ b/.github/benchmark/people.ldif @@ -0,0 +1,17 @@ +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Copyright 2026 3A Systems, LLC. +dn: ou=People,dc=example,dc=com +objectClass: top +objectClass: organizationalUnit +ou: People diff --git a/.github/benchmark/summary.sh b/.github/benchmark/summary.sh new file mode 100644 index 0000000000..9ea5ce4fb1 --- /dev/null +++ b/.github/benchmark/summary.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Copyright 2026 3A Systems, LLC. +# +# Render an LDAP benchmark comparison report (versions + per-operation table + QuickChart charts) +# for two servers A and B to stdout โ€” intended to be appended to $GITHUB_STEP_SUMMARY. The report +# is generic: server names, versions and images are all parameters, and benchmark-specific notes +# are NOT included here (append them separately, e.g. `cat notes.md >> $GITHUB_STEP_SUMMARY`). +# +# Usage: +# summary.sh \ +# +# +# The admin connection bind is labelled ADMIN_CONNECT in the plan and is intentionally +# skipped here, so it never pollutes the per-operation comparison. +set -euo pipefail + +A_NAME="${1:?server A name required}" +A_JSON="${2:?server A statistics.json required}" +A_VER="${3:-unknown}" +A_IMG="${4:-}" +B_NAME="${5:?server B name required}" +B_JSON="${6:?server B statistics.json required}" +B_VER="${7:-unknown}" +B_IMG="${8:-}" + +A_COLOR="#4e79a7" # blue = server A +B_COLOR="#f28e2b" # orange = server B + +# Operations to compare, in workflow order. ADMIN_CONNECT and Total are excluded. +OPS=("ADD" "SEARCH" "COMPARE" "MODIFY" "BIND" "DELETE" "READD") + +# m