feat: [OCISDEV-1031] add opt-in LDAP connection pool#658
Conversation
Add GetLDAPClientWithPool, a bounded pool of authenticated LDAP connections, as a drop-in alternative to the existing single, long-lived reconnecting connection used by the auth/user/group LDAP managers. Connections are dialed and bound lazily on checkout, unhealthy connections are discarded and lazily re-dialed rather than eagerly reconnected, and checkout blocks with a configurable timeout once the pool is exhausted. Disabled by default; enable per backend via pool_enabled (plus pool_size and pool_checkout_timeout) on the LDAP connection config. Signed-off-by: Lukas Hirt <info@hirt.cz>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
|
@kobergj I copied over the stubbed methods from |
|
ai review |
Retry once on a network error in ConnPool.do, mirroring ConnWithReconnect's existing retry behavior: idle pooled connections aren't health-checked before use, so a connection that went stale while idle would otherwise fail the caller's first operation instead of being transparently recovered. Also: stop leaking the checkout() timer when the semaphore wins the select, simplify GetLastError to not check out a connection just to read a value that's meaningless for a pool, and wire a real logger into GetLDAPClientWithPool so pool dial/checkout debug logging isn't silently discarded. Signed-off-by: Lukas Hirt <info@hirt.cz>
| // lazily on checkout and reused across requests; connections that fail with a network error are | ||
| // discarded and lazily re-dialed on a later checkout, instead of the pool eagerly reconnecting them. | ||
| type ConnPool struct { | ||
| config Config |
There was a problem hiding this comment.
The Config type definition needs to be moved to its own file. It's a pain to scroll in the file and not find the definition, and it's a bigger pain to search the definition in all the package files.
| size := config.PoolSize | ||
| if size <= 0 { | ||
| size = defaultPoolSize | ||
| } | ||
| timeout := config.PoolCheckoutTimeout | ||
| if timeout <= 0 { | ||
| timeout = defaultPoolCheckoutTimeout | ||
| } |
There was a problem hiding this comment.
These default values need to be documented, specially when 0 or negative values could mean "unlimited"
| } | ||
| } | ||
|
|
||
| func dialLDAP(config Config) (ldap.Client, error) { |
There was a problem hiding this comment.
This looks weird. Why not create a func (p *CoonPool) getLdapClient(config Config) (ldap.Client, error) ? What benefits creating and using a private callback has, when you won't be able to replace it from outside?
If it's for the tests, to mock the LDAP clients, you can use a LDAP client factory.
In addition, this needs documentation, specially if what it does is "connect + bind", not just connect as the name suggests.
| // SetLogger sets the logger for the current instance | ||
| func (p *ConnPool) SetLogger(logger *zerolog.Logger) { | ||
| p.logger = logger | ||
| } |
There was a problem hiding this comment.
Any benefit? Why the logger isn't enforced when the pool is created? Do we want to allow changing the logger at any time?
| p.mu.Lock() | ||
| closed := p.closed | ||
| p.mu.Unlock() |
There was a problem hiding this comment.
Why not an atomic variable to skip the locks?
| sem chan struct{} | ||
| idle chan ldap.Client |
There was a problem hiding this comment.
These 2 vars need explanation. What is their purpose? What is the expected usage?
| if manager.c.LDAPConn.PoolEnabled { | ||
| manager.ldapClient, err = utils.GetLDAPClientWithPool(&manager.c.LDAPConn) | ||
| } else { | ||
| manager.ldapClient, err = utils.GetLDAPClientWithReconnect(&manager.c.LDAPConn) | ||
| } |
There was a problem hiding this comment.
It seems better a utils.GetLDAPClientFromConfig(manager.c.LDAPConn) that returns the appropriate client.
func GetLDAPClientFromConfig(cfg Config) (*ldap.Client, error) {
if cfg.PoolEnabled {
return GetLDAPClientWithPool(cfg)
} else {
return GetLDAPClientWithReconnect(cfg)
}
}
then you can call that method from here
manager.ldapClient, err = utils.GetLDAPClientFromConfig(&manager.c.LDAPConn)
|
Some additional things: Why are we trying to use a LDAP connection pool as a regular LDAP client?. I'm pretty sure this will end up causing problems. There are several unimplemented methods in the pool that will return errors. This is fine, but the problem is that the client that will be used (either the "real" client or the pool) is set in one place and used in another. Whoever will use the client won't know if he's using the "real" client or the pool, and as such won't know what methods aren't implemented. Why not use the LDAP pool as a pool? Either use a regular LDAP client, or use the LDAP pool, but don't try to use one as another because the behavior is naturally different. You can use a LDAP pool with only one slot if you want (assuming everything works fine) There is a lack of The last thing is that blocking calls aren't common, so they should be explicitly documented to prevent possible issues in the future. It's true that those call are private, but they could be used internally in other places if we need to add new functionality. It would also help to document the expected behavior. |
| p.mu.Unlock() | ||
|
|
||
| if closed || (opErr != nil && ldap.IsErrorWithCode(opErr, ldap.ErrorNetwork)) { | ||
| conn.Close() |
There was a problem hiding this comment.
The Close() func in a ldap.Client interface returns err.
It will be good to log the error.
| for { | ||
| select { | ||
| case conn := <-p.idle: | ||
| conn.Close() |
There was a problem hiding this comment.
The Close() func in a ldap.Client interface returns err.
It will be good to log the error.
| p.mu.Lock() | ||
| closed := p.closed | ||
| p.mu.Unlock() |
There was a problem hiding this comment.
The ConnPool already has IsClosing() function
We can call if IsClosing() instead
|
|
||
| // IsClosing implements the ldap.Client interface | ||
| func (p *ConnPool) IsClosing() bool { | ||
| p.mu.Lock() |
There was a problem hiding this comment.
better to use p.mu.RLock() p.mu.RUnlock()
Add GetLDAPClientWithPool, a bounded pool of authenticated LDAP connections, as a drop-in alternative to the existing single, long-lived reconnecting connection used by the auth/user/group LDAP managers. Connections are dialed and bound lazily on checkout, unhealthy connections are discarded and lazily re-dialed rather than eagerly reconnected, and checkout blocks with a configurable timeout once the pool is exhausted.
Disabled by default; enable per backend via pool_enabled (plus pool_size and pool_checkout_timeout) on the LDAP connection config.