Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

256 lines
6.7 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package nosql
import (
"crypto/tls"
"net/url"
"path"
"runtime/pprof"
"strconv"
"strings"
"github.com/hanzoai/git/modules/log"
"github.com/hanzokv/go/v9"
)
var replacer = strings.NewReplacer("_", "", "-", "")
// CloseKVClient closes a KV client
func (m *Manager) CloseKVClient(connection string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
client, ok := m.KVConnections[connection]
if !ok {
connection = ToKVURI(connection).String()
client, ok = m.KVConnections[connection]
}
if !ok {
return nil
}
client.count--
if client.count > 0 {
return nil
}
for _, name := range client.name {
delete(m.KVConnections, name)
}
return client.UniversalClient.Close()
}
// GetKVClient gets a KV client for a particular connection
func (m *Manager) GetKVClient(connection string) (client kv.UniversalClient) {
// Because we want associate any goroutines created by this call to the main nosqldb context we need to
// wrap this in a goroutine labelled with the nosqldb context
done := make(chan struct{})
var recovered any
go func() {
defer func() {
recovered = recover()
if recovered != nil {
log.Error("PANIC during GetKVClient: %v\nStacktrace: %s", recovered, log.Stack(2))
}
close(done)
}()
pprof.SetGoroutineLabels(m.ctx)
client = m.getKVClient(connection)
}()
<-done
if recovered != nil {
panic(recovered)
}
return client
}
func (m *Manager) getKVClient(connection string) kv.UniversalClient {
m.mutex.Lock()
defer m.mutex.Unlock()
client, ok := m.KVConnections[connection]
if ok {
client.count++
return client
}
uri := ToKVURI(connection)
client, ok = m.KVConnections[uri.String()]
if ok {
client.count++
return client
}
client = &kvClientHolder{
name: []string{connection, uri.String()},
}
opts := getKVOptions(uri)
tlsConfig := getKVTLSOptions(uri)
clientName := uri.Query().Get("clientname")
if len(clientName) > 0 {
client.name = append(client.name, clientName)
}
// the trailing "s" on "kv" is the one and only way to ask for TLS: kvs, kvs+sentinel, kvs+cluster
switch uri.Scheme {
case "kvs+sentinel":
opts.TLSConfig = tlsConfig
fallthrough
case "kv+sentinel":
client.UniversalClient = kv.NewFailoverClient(opts.Failover())
case "kvs+cluster":
opts.TLSConfig = tlsConfig
fallthrough
case "kv+cluster":
client.UniversalClient = kv.NewClusterClient(opts.Cluster())
case "kv+socket":
simpleOpts := opts.Simple()
simpleOpts.Network = "unix"
simpleOpts.Addr = path.Join(uri.Host, uri.Path)
client.UniversalClient = kv.NewClient(simpleOpts)
case "kvs":
opts.TLSConfig = tlsConfig
fallthrough
case "kv":
client.UniversalClient = kv.NewClient(opts.Simple())
default:
return nil
}
for _, name := range client.name {
m.KVConnections[name] = client
}
client.count++
return client
}
// getKVOptions pulls various configuration options based on the KV URI format and converts them to the KV client's
// UniversalOptions fields. This function explicitly excludes fields related to TLS configuration, which is
// conditionally attached to this options struct before being converted to the specific type for the KV scheme being
// used, and only in scenarios where TLS is applicable (e.g. kvs://, kvs+cluster://).
func getKVOptions(uri *url.URL) *kv.UniversalOptions {
opts := &kv.UniversalOptions{}
// Handle username/password
if password, ok := uri.User.Password(); ok {
opts.Password = password
// Username does not appear to be handled by kv.Options
opts.Username = uri.User.Username()
} else if uri.User.Username() != "" {
// assume this is the password
opts.Password = uri.User.Username()
}
// Now handle the uri query sets
for k, v := range uri.Query() {
switch replacer.Replace(strings.ToLower(k)) {
case "addr":
opts.Addrs = append(opts.Addrs, v...)
case "addrs":
opts.Addrs = append(opts.Addrs, strings.Split(v[0], ",")...)
case "username":
opts.Username = v[0]
case "password":
opts.Password = v[0]
case "database":
fallthrough
case "db":
opts.DB, _ = strconv.Atoi(v[0])
case "maxretries":
opts.MaxRetries, _ = strconv.Atoi(v[0])
case "minretrybackoff":
opts.MinRetryBackoff = valToTimeDuration(v)
case "maxretrybackoff":
opts.MaxRetryBackoff = valToTimeDuration(v)
case "timeout":
timeout := valToTimeDuration(v)
if timeout != 0 {
if opts.DialTimeout == 0 {
opts.DialTimeout = timeout
}
if opts.ReadTimeout == 0 {
opts.ReadTimeout = timeout
}
}
case "dialtimeout":
opts.DialTimeout = valToTimeDuration(v)
case "readtimeout":
opts.ReadTimeout = valToTimeDuration(v)
case "writetimeout":
opts.WriteTimeout = valToTimeDuration(v)
case "poolsize":
opts.PoolSize, _ = strconv.Atoi(v[0])
case "minidleconns":
opts.MinIdleConns, _ = strconv.Atoi(v[0])
case "pooltimeout":
opts.PoolTimeout = valToTimeDuration(v)
case "maxredirects":
opts.MaxRedirects, _ = strconv.Atoi(v[0])
case "readonly":
opts.ReadOnly, _ = strconv.ParseBool(v[0])
case "routebylatency":
opts.RouteByLatency, _ = strconv.ParseBool(v[0])
case "routerandomly":
opts.RouteRandomly, _ = strconv.ParseBool(v[0])
case "sentinelmasterid":
fallthrough
case "mastername":
opts.MasterName = v[0]
case "sentinelusername":
opts.SentinelUsername = v[0]
case "sentinelpassword":
opts.SentinelPassword = v[0]
}
}
if uri.Host != "" {
opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
}
// A KV connection string uses the path section of the URI in two different ways. In a TCP-based connection, the
// path will be a database index to automatically have the client SELECT. In a Unix socket connection, it will be the
// file path. We only want to try to coerce this to the database index when we're not expecting a file path so that
// the error log stays clean.
if uri.Path != "" && uri.Scheme != "kv+socket" {
if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
opts.DB = db
} else {
log.Error("Provided database identifier '%s' is not a valid integer. Hanzo Git will ignore this option.", uri.Path)
}
}
return opts
}
// getKVTLSOptions parses KV URI TLS configuration parameters and converts them to the go TLS configuration
// equivalent fields.
func getKVTLSOptions(uri *url.URL) *tls.Config {
tlsConfig := &tls.Config{}
skipverify := uri.Query().Get("skipverify")
if len(skipverify) > 0 {
skipverify, err := strconv.ParseBool(skipverify)
if err == nil {
tlsConfig.InsecureSkipVerify = skipverify
}
}
insecureskipverify := uri.Query().Get("insecureskipverify")
if len(insecureskipverify) > 0 {
insecureskipverify, err := strconv.ParseBool(insecureskipverify)
if err == nil {
tlsConfig.InsecureSkipVerify = insecureskipverify
}
}
return tlsConfig
}