Files
forge/modules/nosql/manager_kv.go
zeekayandHanzo Dev 9a07b4d089 legal: Hanzo AI, Inc. copyright above the upstream notices
2978 files. The fork is 2026-07-15 (rebrand: gitea.dev -> github.com/hanzoai/git),
so 2026 is the year, and "Copyright 2026 Hanzo AI, Inc." was already the form
used by the handful of files that had ours.

Shape is now:

	// Copyright 2026 Hanzo AI, Inc. All rights reserved.
	// Copyright 2015 The Gogs Authors. All rights reserved.
	// Copyright 2019 The Gitea Authors. All rights reserved.
	// SPDX-License-Identifier: MIT

Ours on top, theirs retained verbatim. Retained because MIT requires it, and
separately because they are true — those authors did write that code.

Not renamed to "The Git Authors": Git is a different project under a different
license, and putting its name here would attribute this code to people who have
nothing to do with it. Adding our line is the fix; overwriting theirs would have
been both a licence violation and a false statement.

gofmt still reports import-order drift in some of these files. It predates this
change (verified against a stash) and is left alone rather than burying a
one-line-per-file legal change under 3000 files of unrelated reordering.

Build clean.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 13:49:52 -07:00

257 lines
6.8 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// Copyright 2020 The Gitea Authors. 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
}