mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valera V Harseko
2 days ago ff93f3bf42f4e2ed6e3cd90bb7983e4754e25bae
Refine LDAP benchmark: READD, SSHA-256 hashing, grouped charts

Five follow-up tweaks to the benchmark workflow and plan.

- Rename the "ADD WITHOUT DELETE" sampler to READD (benchmark.jmx, summary.sh OPS).
- Stop each Docker container right after its own benchmark (new Stop OpenLDAP /
Stop OpenDJ steps) to free resources and keep only one server under load at a
time; the final always() cleanup stays as the failure-path safety net.
- Make both servers hash passwords with the same scheme (SSHA-256), hashed
server-side on write: MODIFY sends the password in cleartext and each server
hashes it. OpenLDAP loads the pw-sha2 and ppolicy modules, sets
olcPasswordHash {SSHA256} and enables the ppolicy hash-cleartext overlay on the
mdb database; OpenDJ sets the Default Password Policy default storage scheme to
Salted SHA-256. Each server hashes/verifies with its own implementation, so
there is no cross-implementation hash-format dependency.
- Charts: render each operation as two adjacent, non-overlapping columns
(OpenLDAP / OpenDJ) in different colors instead of two overlapping series, using
an interleaved x-axis with zero-padded series and an explicit color palette
(Mermaid xychart-beta has no grouped bars).
- Bump actions/cache@v4 -> @v5 and actions/upload-artifact@v4 -> @v7 (latest used
elsewhere in this repo).

Validated locally with JMeter 5.6.3: the plan compiles, statistics.json carries
the READD label with ADMIN_CONNECT filtered out, summary.sh renders the two-column
charts, and actionlint passes.
3 files modified
118 ■■■■ changed files
.github/benchmark/benchmark.jmx 6 ●●●● patch | view | raw | blame | history
.github/benchmark/summary.sh 55 ●●●●● patch | view | raw | blame | history
.github/workflows/benchmark.yml 57 ●●●● patch | view | raw | blame | history
.github/benchmark/benchmark.jmx
@@ -17,7 +17,7 @@
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="OpenDJ vs OpenLDAP - LDAP benchmark" enabled="true">
      <stringProp name="TestPlan.comments">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/ADD WITHOUT DELETE) reuse the cached admin connection. The measured user authentication is a single bind/unbind (test=sbind, own connection) after MODIFY has set the userPassword.</stringProp>
      <stringProp name="TestPlan.comments">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. The measured user authentication is a single bind/unbind (test=sbind, own connection) after MODIFY has set the userPassword.</stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
@@ -268,8 +268,8 @@
        </LDAPExtSampler>
        <hashTree/>
        <!-- ADD WITHOUT DELETE (admin connection): accumulation; unique RDN via __UUID. -->
        <LDAPExtSampler guiclass="LdapExtTestSamplerGui" testclass="LDAPExtSampler" testname="ADD WITHOUT DELETE" enabled="true">
        <!-- READD (admin connection): re-add without delete (accumulation); unique RDN via __UUID. -->
        <LDAPExtSampler guiclass="LdapExtTestSamplerGui" testclass="LDAPExtSampler" testname="READD" enabled="true">
          <stringProp name="servername"></stringProp>
          <stringProp name="port"></stringProp>
          <stringProp name="rootdn"></stringProp>
.github/benchmark/summary.sh
@@ -32,7 +32,7 @@
DJ_IMG="${6:-}"
# Operations to compare, in workflow order. ADMIN_CONNECT and Total are excluded.
OPS=("ADD" "SEARCH" "COMPARE" "MODIFY" "BIND" "DELETE" "ADD WITHOUT DELETE")
OPS=("ADD" "SEARCH" "COMPARE" "MODIFY" "BIND" "DELETE" "READD")
# m <file> <label> <field>  -> numeric value (0 if absent), rounded to 1 decimal.
m() { jq -r --arg l "$2" --arg f "$3" '((.[$l][$f]) // 0) | (.*10 | round / 10)' "$1"; }
@@ -85,51 +85,63 @@
echo ""
# ---------------------------------------------------------------- Chart helpers
# Build a Mermaid list "x, y, z" of values for all OPS from <file> <field>, via <m|mi>.
series() { # <fn> <file> <field>
  local fn="$1" file="$2" field="$3" out="" v
  for op in "${OPS[@]}"; do
    v=$("$fn" "$file" "$op" "$field")
    out+="${out:+, }${v}"
  done
# Mermaid xychart-beta has no grouped bars/legend, so to get two adjacent (non-overlapping)
# columns per operation we repeat each op on the x-axis and zero-pad the two series: OpenLDAP
# bars land on the left tick of each pair, OpenDJ on the right tick.
#
# x-axis: each op twice -> "ADD", "ADD", "SEARCH", "SEARCH", ...
xaxis_pairs() {
  local out="" op
  for op in "${OPS[@]}"; do out+="${out:+, }\"${op}\", \"${op}\""; done
  printf '%s' "$out"
}
# x-axis with every label quoted (labels contain spaces).
xaxis() {
  local out="" op
  for op in "${OPS[@]}"; do out+="${out:+, }\"${op}\""; done
# OpenLDAP series: value then 0 per op (bar on the left tick of each pair).
series_ol() { # <fn> <file> <field>
  local fn="$1" file="$2" field="$3" out="" v
  for op in "${OPS[@]}"; do v=$("$fn" "$file" "$op" "$field"); out+="${out:+, }${v}, 0"; done
  printf '%s' "$out"
}
# OpenDJ series: 0 then value per op (bar on the right tick of each pair).
series_dj() { # <fn> <file> <field>
  local fn="$1" file="$2" field="$3" out="" v
  for op in "${OPS[@]}"; do v=$("$fn" "$file" "$op" "$field"); out+="${out:+, }0, ${v}"; done
  printf '%s' "$out"
}
XAXIS="$(xaxis)"
XAXIS="$(xaxis_pairs)"
# Fix the two series colors (series 1 = OpenLDAP blue, series 2 = OpenDJ orange).
PALETTE='%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#4e79a7, #f28e2b"}}}}%%'
CAPTION="_Each operation has two columns: ๐ŸŸฆ OpenLDAP (left) ยท ๐ŸŸง OpenDJ (right)._"
# ---------------------------------------------------------------- Throughput chart
echo "### Comparative chart โ€” throughput per operation (ops/s, higher is better)"
echo ""
echo "_First bar series = OpenLDAP, second bar series = OpenDJ._"
echo "$CAPTION"
echo ""
echo '```mermaid'
echo "$PALETTE"
echo "xychart-beta"
echo "    title \"Throughput per operation (ops/s) โ€” OpenLDAP vs OpenDJ\""
echo "    x-axis [${XAXIS}]"
echo "    y-axis \"ops/s\""
echo "    bar [$(series m "$OL_JSON" throughput)]"
echo "    bar [$(series m "$DJ_JSON" throughput)]"
echo "    bar [$(series_ol m "$OL_JSON" throughput)]"
echo "    bar [$(series_dj m "$DJ_JSON" throughput)]"
echo '```'
echo ""
# ---------------------------------------------------------------- Latency chart
echo "### Comparative chart โ€” mean latency per operation (ms, lower is better)"
echo ""
echo "_First bar series = OpenLDAP, second bar series = OpenDJ._"
echo "$CAPTION"
echo ""
echo '```mermaid'
echo "$PALETTE"
echo "xychart-beta"
echo "    title \"Mean latency per operation (ms) โ€” OpenLDAP vs OpenDJ\""
echo "    x-axis [${XAXIS}]"
echo "    y-axis \"ms\""
echo "    bar [$(series m "$OL_JSON" meanResTime)]"
echo "    bar [$(series m "$DJ_JSON" meanResTime)]"
echo "    bar [$(series_ol m "$OL_JSON" meanResTime)]"
echo "    bar [$(series_dj m "$DJ_JSON" meanResTime)]"
echo '```'
echo ""
@@ -139,6 +151,7 @@
echo "- \`BIND\` is the measured **user authentication** (\`test=sbind\`, single bind/unbind on its"
echo "  own connection) as \`cn=user_<n>,ou=People\` with the password set by \`MODIFY\`. The admin"
echo "  connection bind (\`ADMIN_CONNECT\`) is cached once per thread and excluded from these results."
echo "- Default password storage scheme differs by server (OpenDJ PBKDF2/Salted-SHA vs OpenLDAP"
echo "  SSHA); this is a legitimate part of authentication cost."
echo "- MODIFY sends the password in cleartext; each server hashes it on write with the **same"
echo "  scheme (SSHA-256)** โ€” OpenLDAP via the pw-sha2 module + ppolicy hash-cleartext, OpenDJ via"
echo "  its Salted SHA-256 default scheme โ€” so BIND authentication is compared on equal footing."
echo "- Full interactive JMeter HTML dashboards are attached as the \`jmeter-reports\` artifact."
.github/workflows/benchmark.yml
@@ -78,7 +78,7 @@
      - uses: actions/checkout@v6
      - name: Cache JMeter
        uses: actions/cache@v4
        uses: actions/cache@v5
        with:
          path: ~/jmeter
          key: jmeter-${{ env.JMETER }}
@@ -103,15 +103,32 @@
            -e LDAP_TLS=false \
            "$OPENLDAP_IMAGE"
      - name: Wait for OpenLDAP + seed ou=People
      - name: Configure OpenLDAP (SSHA-256 hash-on-write, seed)
        run: |
          for i in $(seq 1 90); do
            if ldapsearch -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \
                 -b "$BASEDN" -s base dn >/dev/null 2>&1; then
              echo "OpenLDAP is up"; break
            fi
            sleep 2
          done
          wait_ldap() {
            for i in $(seq 1 90); do
              ldapsearch -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \
                -b "$BASEDN" -s base dn >/dev/null 2>&1 && { echo "OpenLDAP is up"; return 0; }
              sleep 2
            done
          }
          le() { docker exec -i openldap "$@" -Y EXTERNAL -H ldapi:/// >/dev/null 2>&1 || true; }
          wait_ldap
          # Load pw-sha2 (provides {SSHA256}) and ppolicy (overlay) modules; osixia ships both
          # in /usr/lib/ldap. Create the module list entry or append the values, then restart so
          # the modules are active in the running slapd.
          printf 'dn: cn=module{0},cn=config\nobjectClass: olcModuleList\nolcModuleLoad: pw-sha2\nolcModuleLoad: ppolicy\n' | le ldapadd
          printf 'dn: cn=module{0},cn=config\nchangetype: modify\nadd: olcModuleLoad\nolcModuleLoad: pw-sha2\n' | le ldapmodify
          printf 'dn: cn=module{0},cn=config\nchangetype: modify\nadd: olcModuleLoad\nolcModuleLoad: ppolicy\n' | le ldapmodify
          docker restart openldap
          wait_ldap
          # Make slapd hash cleartext userPassword with {SSHA256} on a plain modify: set the global
          # password-hash and enable the ppolicy overlay's hash_cleartext on the mdb database.
          printf 'dn: olcDatabase={-1}frontend,cn=config\nchangetype: modify\nreplace: olcPasswordHash\nolcPasswordHash: {SSHA256}\n' | le ldapmodify
          DBDN="$(docker exec openldap ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config '(olcDatabase=*mdb)' dn 2>/dev/null | sed -n 's/^dn: //p' | head -1)"
          [ -n "$DBDN" ] || DBDN="olcDatabase={1}mdb,cn=config"
          printf 'dn: olcOverlay=ppolicy,%s\nobjectClass: olcOverlayConfig\nobjectClass: olcPPolicyConfig\nolcOverlay: ppolicy\nolcPPolicyHashCleartext: TRUE\n' "$DBDN" | le ldapadd
          # Seed ou=People (after the restart so it survives any re-init).
          ldapadd -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \
            -f .github/benchmark/people.ldif || true
@@ -141,6 +158,11 @@
            -Jjmeter.reportgenerator.sample_filter="$SAMPLE_FILTER" \
            -l openldap.jtl -e -o openldap
      - name: Stop OpenLDAP
        run: |
          docker logs openldap 2>&1 | tail -n 100 || true
          docker rm -f openldap || true
      - name: Start OpenDJ
        run: |
          docker run -d --name opendj -p 1389:1389 \
@@ -161,6 +183,16 @@
          ldapadd -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \
            -f .github/benchmark/people.ldif || true
      - name: Configure OpenDJ password policy (SSHA-256 hash-on-write)
        run: |
          # Hash cleartext userPassword with Salted SHA-256 on write, matching OpenLDAP.
          docker exec opendj /opt/opendj/bin/dsconfig set-password-policy-prop \
            --policy-name "Default Password Policy" \
            --set default-password-storage-scheme:"Salted SHA-256" \
            --hostname localhost --port 4444 \
            --bindDN "cn=Directory Manager" --bindPassword password \
            --trustAll --no-prompt
      - name: Capture OpenDJ version
        run: |
          # `|| true` guards the bind so a transient ldapsearch failure can't trip pipefail;
@@ -187,6 +219,11 @@
            -Jjmeter.reportgenerator.sample_filter="$SAMPLE_FILTER" \
            -l opendj.jtl -e -o opendj
      - name: Stop OpenDJ
        run: |
          docker logs opendj 2>&1 | tail -n 100 || true
          docker rm -f opendj || true
      # ---------------------------------------------------------------- Report
      - name: Build job summary
        run: |
@@ -197,7 +234,7 @@
      - name: Upload JMeter reports
        if: always()
        uses: actions/upload-artifact@v4
        uses: actions/upload-artifact@v7
        with:
          name: jmeter-reports
          path: |