tstor/daemons/qbittorrent/client.go

264 lines
6.2 KiB
Go
Raw Normal View History

package qbittorrent
import (
"context"
"fmt"
"slices"
"time"
2024-08-31 23:00:13 +00:00
"git.kmsign.ru/royalcat/tstor/pkg/qbittorrent"
2024-12-09 20:46:53 +00:00
"github.com/creativecreature/sturdyc"
2024-09-24 13:26:15 +00:00
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/royalcat/btrgo/btrsync"
2024-12-09 20:46:53 +00:00
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
)
2024-12-09 20:46:53 +00:00
var meter = otel.Meter("git.kmsign.ru/royalcat/tstor/daemons/qbittorrent")
2024-08-31 23:00:13 +00:00
type cacheClient struct {
qb qbittorrent.Client
2024-09-24 13:26:15 +00:00
propertiesCache *expirable.LRU[string, qbittorrent.TorrentProperties]
pieceCache btrsync.MapOf[pieceKey, int]
2024-12-09 20:46:53 +00:00
infoClient *sturdyc.Client[*qbittorrent.TorrentInfo]
2024-09-24 13:26:15 +00:00
}
type pieceKey struct {
hash string
index int
}
2024-08-31 23:00:13 +00:00
func wrapClient(qb qbittorrent.Client) *cacheClient {
2024-09-24 13:26:15 +00:00
const (
cacheSize = 5000
cacheTTL = time.Minute
)
return &cacheClient{
qb: qb,
propertiesCache: expirable.NewLRU[string, qbittorrent.TorrentProperties](cacheSize, nil, cacheTTL),
2024-12-09 20:46:53 +00:00
infoClient: sturdyc.New[*qbittorrent.TorrentInfo](cacheSize, 1, cacheTTL, 10,
sturdyc.WithEarlyRefreshes(time.Minute, time.Minute*5, time.Second*10),
sturdyc.WithRefreshCoalescing(100, time.Second/4),
sturdyc.WithMetrics(newSturdycMetrics()),
),
pieceCache: btrsync.MapOf[pieceKey, int]{},
2024-09-24 13:26:15 +00:00
}
}
2024-09-24 13:26:15 +00:00
func (f *cacheClient) getInfo(ctx context.Context, hash string) (*qbittorrent.TorrentInfo, error) {
2024-12-09 20:46:53 +00:00
out, err := f.infoClient.GetOrFetchBatch(ctx, []string{hash},
f.infoClient.BatchKeyFn(""),
func(ctx context.Context, ids []string) (map[string]*qbittorrent.TorrentInfo, error) {
infos, err := f.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{
Hashes: ids,
})
if err != nil {
return nil, fmt.Errorf("error to get torrents: %w", err)
}
2024-09-24 13:26:15 +00:00
2024-12-09 20:46:53 +00:00
out := make(map[string]*qbittorrent.TorrentInfo)
for _, info := range infos {
out[info.Hash] = info
}
return out, nil
},
)
2024-09-24 13:26:15 +00:00
if err != nil {
2024-12-09 20:46:53 +00:00
return nil, err
2024-09-24 13:26:15 +00:00
}
2024-12-09 20:46:53 +00:00
if out[hash] == nil {
2024-09-24 13:26:15 +00:00
return nil, nil
}
2024-12-09 20:46:53 +00:00
return out[hash], nil
2024-09-24 13:26:15 +00:00
}
2024-08-31 23:00:13 +00:00
func (f *cacheClient) getProperties(ctx context.Context, hash string) (*qbittorrent.TorrentProperties, error) {
2024-09-24 13:26:15 +00:00
if v, ok := f.propertiesCache.Get(hash); ok {
return &v, nil
}
2024-08-31 23:00:13 +00:00
info, err := f.qb.Torrent().GetProperties(ctx, hash)
if err != nil {
return nil, err
}
2024-09-24 13:26:15 +00:00
f.propertiesCache.Add(hash, *info)
2024-08-31 23:00:13 +00:00
return info, nil
}
func (f *cacheClient) listContent(ctx context.Context, hash string) ([]*qbittorrent.TorrentContent, error) {
contents, err := f.qb.Torrent().GetContents(ctx, hash)
if err != nil {
return nil, err
}
return contents, nil
}
func (f *cacheClient) getContent(ctx context.Context, hash string, contentIndex int) (*qbittorrent.TorrentContent, error) {
contents, err := f.qb.Torrent().GetContents(ctx, hash, contentIndex)
if err != nil {
return nil, err
}
2024-08-31 23:00:13 +00:00
contentI := slices.IndexFunc(contents, func(c *qbittorrent.TorrentContent) bool {
return c.Index == contentIndex
})
2024-08-31 23:00:13 +00:00
if contentI == -1 {
return nil, fmt.Errorf("content not found")
}
2024-08-31 23:00:13 +00:00
return contents[contentI], nil
}
2024-08-31 23:00:13 +00:00
func (f *cacheClient) isPieceComplete(ctx context.Context, hash string, pieceIndex int) (bool, error) {
2024-09-24 13:26:15 +00:00
cachedPieceState, ok := f.pieceCache.Load(pieceKey{hash: hash, index: pieceIndex})
if ok && cachedPieceState == 2 {
return true, nil
}
2024-08-31 23:00:13 +00:00
completion, err := f.qb.Torrent().GetPiecesStates(ctx, hash)
if err != nil {
return false, err
}
2024-09-24 13:26:15 +00:00
for i, v := range completion {
f.pieceCache.Store(pieceKey{hash: hash, index: i}, v)
}
if completion[pieceIndex] == 2 {
return true, nil
}
return false, nil
}
2024-08-31 23:00:13 +00:00
func (f *cacheClient) waitPieceToComplete(ctx context.Context, hash string, pieceIndex int) error {
const checkingInterval = 1 * time.Second
ok, err := f.isPieceComplete(ctx, hash, pieceIndex)
if err != nil {
return err
}
if ok {
return nil
}
if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < checkingInterval {
return context.DeadlineExceeded
}
ticker := time.NewTicker(checkingInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
ok, err := f.isPieceComplete(ctx, hash, pieceIndex)
if err != nil {
return err
}
if ok {
return nil
}
if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < checkingInterval {
return context.DeadlineExceeded
}
}
}
}
2024-12-09 20:46:53 +00:00
type sturdycMetrics struct {
ctx context.Context
cacheHit metric.Int64Counter
cacheMiss metric.Int64Counter
refresh metric.Int64Counter
missing metric.Int64Counter
forcedEviction metric.Int64Counter
entryEviction metric.Int64Counter
batchSize metric.Int64Histogram
observeCacheSize func() int
}
var _ sturdyc.MetricsRecorder = (*sturdycMetrics)(nil)
func newSturdycMetrics() *sturdycMetrics {
m := &sturdycMetrics{
ctx: context.Background(),
cacheHit: must(meter.Int64Counter("sturdyc_cache_hit")),
cacheMiss: must(meter.Int64Counter("sturdyc_cache_miss")),
refresh: must(meter.Int64Counter("sturdyc_cache_refresh")),
missing: must(meter.Int64Counter("sturdyc_cache_missing")),
forcedEviction: must(meter.Int64Counter("sturdyc_cache_forced_eviction")),
entryEviction: must(meter.Int64Counter("sturdyc_cache_entry_eviction")),
batchSize: must(meter.Int64Histogram("sturdyc_cache_batch_size")),
}
must(meter.Int64ObservableGauge("sturdyc_cache_size",
metric.WithInt64Callback(func(ctx context.Context, io metric.Int64Observer) error {
if m.observeCacheSize == nil {
return nil
}
io.Observe(int64(m.observeCacheSize()))
return nil
})))
return m
}
func (s *sturdycMetrics) CacheHit() {
s.cacheHit.Add(s.ctx, 1)
}
func (s *sturdycMetrics) CacheMiss() {
s.cacheMiss.Add(s.ctx, 1)
}
func (s *sturdycMetrics) Refresh() {
s.refresh.Add(s.ctx, 1)
}
func (s *sturdycMetrics) MissingRecord() {
s.missing.Add(s.ctx, 1)
}
func (s *sturdycMetrics) ForcedEviction() {
s.forcedEviction.Add(s.ctx, 1)
}
func (s *sturdycMetrics) CacheBatchRefreshSize(size int) {
s.batchSize.Record(s.ctx, int64(size))
}
func (s *sturdycMetrics) ObserveCacheSize(callback func() int) {
s.observeCacheSize = callback
}
func (s *sturdycMetrics) EntriesEvicted(evictd int) {
s.entryEviction.Add(s.ctx, int64(evictd))
}
func (s *sturdycMetrics) ShardIndex(int) {
return
}
func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}