@@ -128,6 +128,9 @@ The benchmark command:
|
|||||||
- Run the benchmark on the target environment and database profile before deciding defaults:
|
- Run the benchmark on the target environment and database profile before deciding defaults:
|
||||||
- `vctp -settings /path/to/vctp.yml -benchmark-aggregations -benchmark-runs 3`
|
- `vctp -settings /path/to/vctp.yml -benchmark-aggregations -benchmark-runs 3`
|
||||||
- Current local comparison snapshot (2026-04-20) is recorded in `phase-metrics-2026-04-20.md`.
|
- Current local comparison snapshot (2026-04-20) is recorded in `phase-metrics-2026-04-20.md`.
|
||||||
|
- Latest tuned Postgres snapshot (2026-04-21, `runs=3`) showed:
|
||||||
|
- Daily window (`2026-04-21` to `2026-04-22` UTC): Go avg `2.261369712s` vs SQL avg `1m31.738727387s` (Go ~`40.57x` faster).
|
||||||
|
- Monthly window (`2026-04-01` to `2026-05-01` UTC): Go avg `3.705308832s` vs SQL avg `3.065612298s` (SQL ~`1.21x` faster).
|
||||||
- Default-path decision remains `settings.scheduled_aggregation_engine: go`.
|
- Default-path decision remains `settings.scheduled_aggregation_engine: go`.
|
||||||
- Promote SQL only when representative production-scale **Postgres** runs show clear, repeatable wins.
|
- Promote SQL only when representative production-scale **Postgres** runs show clear, repeatable wins.
|
||||||
|
|
||||||
|
|||||||
+99
-21
@@ -107,13 +107,24 @@ func (a *LDAPAuthenticator) AuthenticateAndFetchGroups(ctx context.Context, user
|
|||||||
Username: username,
|
Username: username,
|
||||||
UserDN: username,
|
UserDN: username,
|
||||||
}
|
}
|
||||||
|
if whoami, err := conn.WhoAmI(nil); err != nil {
|
||||||
|
identity.Diagnostics = append(identity.Diagnostics, fmt.Sprintf("whoami_failed:%v", err))
|
||||||
|
} else if boundDN := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(whoami.AuthzID), "dn:")); boundDN != "" {
|
||||||
|
identity.UserDN = boundDN
|
||||||
|
identity.Diagnostics = append(identity.Diagnostics, "whoami_dn_resolved")
|
||||||
|
} else {
|
||||||
|
identity.Diagnostics = append(identity.Diagnostics, "whoami_dn_empty")
|
||||||
|
}
|
||||||
|
|
||||||
entry, err := a.lookupUserEntry(conn, username)
|
entry, lookupStrategy, err := a.lookupUserEntry(conn, username, identity.UserDN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return LDAPIdentity{}, err
|
return LDAPIdentity{}, err
|
||||||
}
|
}
|
||||||
if entry != nil {
|
if entry != nil {
|
||||||
identity.Diagnostics = append(identity.Diagnostics, "user_entry_found")
|
if lookupStrategy == "" {
|
||||||
|
lookupStrategy = "unknown"
|
||||||
|
}
|
||||||
|
identity.Diagnostics = append(identity.Diagnostics, "user_entry_found:"+lookupStrategy)
|
||||||
if strings.TrimSpace(entry.DN) != "" {
|
if strings.TrimSpace(entry.DN) != "" {
|
||||||
identity.UserDN = entry.DN
|
identity.UserDN = entry.DN
|
||||||
}
|
}
|
||||||
@@ -140,6 +151,7 @@ func (a *LDAPAuthenticator) AuthenticateAndFetchGroups(ctx context.Context, user
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupFilter := buildGroupMembershipFilter(identity.UserDN, principalCandidates(username))
|
||||||
groupEntries, err := conn.Search(ldap.NewSearchRequest(
|
groupEntries, err := conn.Search(ldap.NewSearchRequest(
|
||||||
a.baseDN,
|
a.baseDN,
|
||||||
ldap.ScopeWholeSubtree,
|
ldap.ScopeWholeSubtree,
|
||||||
@@ -147,11 +159,7 @@ func (a *LDAPAuthenticator) AuthenticateAndFetchGroups(ctx context.Context, user
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
fmt.Sprintf("(|(member=%s)(uniqueMember=%s)(memberUid=%s))",
|
groupFilter,
|
||||||
ldap.EscapeFilter(identity.UserDN),
|
|
||||||
ldap.EscapeFilter(identity.UserDN),
|
|
||||||
ldap.EscapeFilter(username),
|
|
||||||
),
|
|
||||||
[]string{"dn"},
|
[]string{"dn"},
|
||||||
nil,
|
nil,
|
||||||
))
|
))
|
||||||
@@ -272,10 +280,24 @@ func (a *LDAPAuthenticator) buildTLSConfig() (*tls.Config, error) {
|
|||||||
return tlsConfig, nil
|
return tlsConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *LDAPAuthenticator) lookupUserEntry(conn *ldap.Conn, username string) (*ldap.Entry, error) {
|
func (a *LDAPAuthenticator) lookupUserEntry(conn *ldap.Conn, username string, userDNHint string) (*ldap.Entry, string, error) {
|
||||||
|
dnCandidates := compactTrimmedStrings([]string{userDNHint})
|
||||||
if looksLikeDN(username) {
|
if looksLikeDN(username) {
|
||||||
|
dnCandidates = append(dnCandidates, strings.TrimSpace(username))
|
||||||
|
}
|
||||||
|
seenDN := make(map[string]struct{}, len(dnCandidates))
|
||||||
|
for _, dn := range dnCandidates {
|
||||||
|
key := normalizeDN(dn)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenDN[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenDN[key] = struct{}{}
|
||||||
|
|
||||||
searchRes, err := conn.Search(ldap.NewSearchRequest(
|
searchRes, err := conn.Search(ldap.NewSearchRequest(
|
||||||
username,
|
dn,
|
||||||
ldap.ScopeBaseObject,
|
ldap.ScopeBaseObject,
|
||||||
ldap.NeverDerefAliases,
|
ldap.NeverDerefAliases,
|
||||||
1,
|
1,
|
||||||
@@ -286,14 +308,17 @@ func (a *LDAPAuthenticator) lookupUserEntry(conn *ldap.Conn, username string) (*
|
|||||||
nil,
|
nil,
|
||||||
))
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: unable to load user entry: %v", ErrLDAPOperationFailed, err)
|
if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if len(searchRes.Entries) == 0 {
|
return nil, "", fmt.Errorf("%w: unable to load user entry by dn: %v", ErrLDAPOperationFailed, err)
|
||||||
return nil, nil
|
}
|
||||||
|
if len(searchRes.Entries) > 0 {
|
||||||
|
return searchRes.Entries[0], "dn", nil
|
||||||
}
|
}
|
||||||
return searchRes.Entries[0], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, principal := range principalCandidates(username) {
|
||||||
searchRes, err := conn.Search(ldap.NewSearchRequest(
|
searchRes, err := conn.Search(ldap.NewSearchRequest(
|
||||||
a.baseDN,
|
a.baseDN,
|
||||||
ldap.ScopeWholeSubtree,
|
ldap.ScopeWholeSubtree,
|
||||||
@@ -302,21 +327,22 @@ func (a *LDAPAuthenticator) lookupUserEntry(conn *ldap.Conn, username string) (*
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
fmt.Sprintf("(|(uid=%s)(cn=%s)(sAMAccountName=%s)(userPrincipalName=%s))",
|
fmt.Sprintf("(|(uid=%s)(cn=%s)(sAMAccountName=%s)(userPrincipalName=%s))",
|
||||||
ldap.EscapeFilter(username),
|
ldap.EscapeFilter(principal),
|
||||||
ldap.EscapeFilter(username),
|
ldap.EscapeFilter(principal),
|
||||||
ldap.EscapeFilter(username),
|
ldap.EscapeFilter(principal),
|
||||||
ldap.EscapeFilter(username),
|
ldap.EscapeFilter(principal),
|
||||||
),
|
),
|
||||||
[]string{"uid", "sAMAccountName", "userPrincipalName", "cn", "memberOf"},
|
[]string{"uid", "sAMAccountName", "userPrincipalName", "cn", "memberOf"},
|
||||||
nil,
|
nil,
|
||||||
))
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: user lookup failed: %v", ErrLDAPOperationFailed, err)
|
return nil, "", fmt.Errorf("%w: user lookup failed: %v", ErrLDAPOperationFailed, err)
|
||||||
}
|
}
|
||||||
if len(searchRes.Entries) == 0 {
|
if len(searchRes.Entries) > 0 {
|
||||||
return nil, nil
|
return searchRes.Entries[0], "principal", nil
|
||||||
}
|
}
|
||||||
return searchRes.Entries[0], nil
|
}
|
||||||
|
return nil, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeDN(value string) string {
|
func normalizeDN(value string) string {
|
||||||
@@ -352,6 +378,58 @@ func looksLikeDN(value string) bool {
|
|||||||
return strings.Contains(value, "=") && strings.Contains(value, ",")
|
return strings.Contains(value, "=") && strings.Contains(value, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func principalCandidates(username string) []string {
|
||||||
|
username = strings.TrimSpace(username)
|
||||||
|
if username == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, 4)
|
||||||
|
candidates := make([]string, 0, 4)
|
||||||
|
add := func(value string) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := strings.ToLower(value)
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
candidates = append(candidates, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(username)
|
||||||
|
if idx := strings.LastIndex(username, `\`); idx >= 0 && idx < len(username)-1 {
|
||||||
|
add(username[idx+1:])
|
||||||
|
}
|
||||||
|
if idx := strings.Index(username, "@"); idx > 0 {
|
||||||
|
add(username[:idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGroupMembershipFilter(userDN string, principals []string) string {
|
||||||
|
clauses := make([]string, 0, 2+len(principals))
|
||||||
|
userDN = strings.TrimSpace(userDN)
|
||||||
|
if userDN != "" {
|
||||||
|
escapedDN := ldap.EscapeFilter(userDN)
|
||||||
|
clauses = append(clauses, "(member="+escapedDN+")", "(uniqueMember="+escapedDN+")")
|
||||||
|
}
|
||||||
|
for _, principal := range principals {
|
||||||
|
principal = strings.TrimSpace(principal)
|
||||||
|
if principal == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clauses = append(clauses, "(memberUid="+ldap.EscapeFilter(principal)+")")
|
||||||
|
}
|
||||||
|
if len(clauses) == 0 {
|
||||||
|
return "(objectClass=group)"
|
||||||
|
}
|
||||||
|
return "(|" + strings.Join(clauses, "") + ")"
|
||||||
|
}
|
||||||
|
|
||||||
func ctxErr(ctx context.Context) error {
|
func ctxErr(ctx context.Context) error {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -37,3 +37,52 @@ func TestHasAnyGroup(t *testing.T) {
|
|||||||
t.Fatal("expected empty required groups to allow")
|
t.Fatal("expected empty required groups to allow")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrincipalCandidates(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
username string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "upn adds local part",
|
||||||
|
username: "L075239@corpau.wbcau.westpac.com.au",
|
||||||
|
want: []string{"L075239@corpau.wbcau.westpac.com.au", "L075239"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "domain slash user adds sam",
|
||||||
|
username: `CORPAU\L075239`,
|
||||||
|
want: []string{`CORPAU\L075239`, "L075239"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plain username unchanged",
|
||||||
|
username: "L075239",
|
||||||
|
want: []string{"L075239"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := principalCandidates(tc.username)
|
||||||
|
if len(got) != len(tc.want) {
|
||||||
|
t.Fatalf("unexpected candidate count: got=%d want=%d (%#v)", len(got), len(tc.want), got)
|
||||||
|
}
|
||||||
|
for i := range tc.want {
|
||||||
|
if got[i] != tc.want[i] {
|
||||||
|
t.Fatalf("unexpected candidate at %d: got=%q want=%q", i, got[i], tc.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildGroupMembershipFilter(t *testing.T) {
|
||||||
|
filter := buildGroupMembershipFilter(
|
||||||
|
"CN=User,OU=Users,DC=corpau,DC=wbcau,DC=westpac,DC=com,DC=au",
|
||||||
|
[]string{"L075239@corpau.wbcau.westpac.com.au", "L075239"},
|
||||||
|
)
|
||||||
|
expected := "(|(member=CN=User,OU=Users,DC=corpau,DC=wbcau,DC=westpac,DC=com,DC=au)(uniqueMember=CN=User,OU=Users,DC=corpau,DC=wbcau,DC=westpac,DC=com,DC=au)(memberUid=L075239@corpau.wbcau.westpac.com.au)(memberUid=L075239))"
|
||||||
|
if filter != expected {
|
||||||
|
t.Fatalf("unexpected group filter:\n got: %s\nwant: %s", filter, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -305,10 +305,15 @@ The target architecture is:
|
|||||||
- [x] Validate/add canonical `vm_hourly_stats` indexes for snapshot time, vCenter+time, VM identity+time, and trace lookup.
|
- [x] Validate/add canonical `vm_hourly_stats` indexes for snapshot time, vCenter+time, VM identity+time, and trace lookup.
|
||||||
- [x] Add PostgreSQL monthly partitioning for `vm_hourly_stats` behind migration controls.
|
- [x] Add PostgreSQL monthly partitioning for `vm_hourly_stats` behind migration controls.
|
||||||
- [x] Benchmark Go vs SQL on canonical Postgres tables using representative production-scale data.
|
- [x] Benchmark Go vs SQL on canonical Postgres tables using representative production-scale data.
|
||||||
- Production-scale Postgres run completed on 2026-04-21 via one-shot canonical benchmark (`-benchmark-aggregations` with `runs_per_mode=1`, `driver=postgres`).
|
- Production-scale Postgres benchmark runs completed on 2026-04-21 via one-shot canonical benchmark (`-benchmark-aggregations`, `driver=postgres`, with `runs_per_mode=1` and `runs_per_mode=3`).
|
||||||
- Daily window `2026-04-20T00:00:00Z` to `2026-04-21T00:00:00Z`: Go `4.000602432s` (`14881` rows) vs SQL `1h17m19.039092561s` (`14920` rows), with Go ~`1159.59x` faster on this run.
|
- Run A (pre-tuning), daily window `2026-04-20T00:00:00Z` to `2026-04-21T00:00:00Z`: Go `4.000602432s` (`14881` rows) vs SQL `1h17m19.039092561s` (`14920` rows), with Go ~`1159.59x` faster.
|
||||||
- Monthly window `2026-04-01T00:00:00Z` to `2026-05-01T00:00:00Z`: Go `3.529410947s` (`15871` rows) vs SQL `3.313037973s` (`15873` rows), near parity with SQL slightly faster (~`0.216s`, `6.1%`).
|
- Run A (pre-tuning), monthly window `2026-04-01T00:00:00Z` to `2026-05-01T00:00:00Z`: Go `3.529410947s` (`15871` rows) vs SQL `3.313037973s` (`15873` rows), near parity with SQL slightly faster (~`0.216s`, `6.1%`).
|
||||||
- Decision remains unchanged: keep Go as scheduled default and treat SQL as fallback/backfill until SQL shows a clear, repeatable runtime win across canonical workloads.
|
- Run B (after PostgreSQL tuning), daily window `2026-04-21T00:00:00Z` to `2026-04-22T00:00:00Z`: Go `2.277889486s` (`14831` rows) vs SQL `1m31.273491543s` (`14839` rows), with Go still ~`40.07x` faster.
|
||||||
|
- Run B (after PostgreSQL tuning), monthly window `2026-04-01T00:00:00Z` to `2026-05-01T00:00:00Z`: Go `3.947474215s` (`15871` rows) vs SQL `2.758716002s` (`15873` rows), with SQL ~`1.43x` faster.
|
||||||
|
- Run C (after PostgreSQL tuning, `runs=3`), daily window `2026-04-21T00:00:00Z` to `2026-04-22T00:00:00Z`: Go avg `2.261369712s` (min `2.169537168s`, median `2.191474445s`, max `2.423097524s`, rows `14831`) vs SQL avg `1m31.738727387s` (min `1m29.960115863s`, median `1m32.068576507s`, max `1m33.187489791s`, rows `14839`), with Go ~`40.57x` faster by average.
|
||||||
|
- Run C (after PostgreSQL tuning, `runs=3`), monthly window `2026-04-01T00:00:00Z` to `2026-05-01T00:00:00Z`: Go avg `3.705308832s` (min `3.696553751s`, median `3.70776704s`, max `3.711605706s`, rows `15871`) vs SQL avg `3.065612298s` (min `2.873749798s`, median `3.022090149s`, max `3.300996948s`, rows `15873`), with SQL ~`1.21x` faster by average (~`17.26%` faster than Go).
|
||||||
|
- Tuning impact between Run A and Run B: daily SQL improved ~`50.83x`, daily Go improved ~`1.76x`, monthly SQL improved ~`1.20x`, and monthly Go regressed (~`0.89x` of prior speed).
|
||||||
|
- Decision remains unchanged: keep Go as scheduled default and treat SQL as fallback/backfill until SQL shows a clear, repeatable runtime win across canonical workloads, especially on daily windows (where Go remains consistently dominant across runs).
|
||||||
- [x] Keep Go as scheduled default unless SQL shows clear and repeatable runtime wins.
|
- [x] Keep Go as scheduled default unless SQL shows clear and repeatable runtime wins.
|
||||||
- [x] If SQL wins, roll out behind a controlled flag before any default switch.
|
- [x] If SQL wins, roll out behind a controlled flag before any default switch.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user