small refactor*
This commit is contained in:
parent
b6b541e050
commit
24a4d30275
232 changed files with 2164 additions and 1906 deletions
plugins/qbittorrent
273
plugins/qbittorrent/client.go
Normal file
273
plugins/qbittorrent/client.go
Normal file
|
@ -0,0 +1,273 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/plugins/qbittorrent/pkg/qbittorrent"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"github.com/royalcat/btrgo/btrsync"
|
||||
"github.com/viccon/sturdyc"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
var meter = otel.Meter("git.kmsign.ru/royalcat/tstor/plugins/qbittorrent")
|
||||
|
||||
type cacheClient struct {
|
||||
qb qbittorrent.Client
|
||||
|
||||
propertiesCache *expirable.LRU[string, qbittorrent.TorrentProperties]
|
||||
|
||||
pieceCache btrsync.MapOf[pieceKey, int]
|
||||
|
||||
infoClient *sturdyc.Client[*qbittorrent.TorrentInfo]
|
||||
}
|
||||
|
||||
type pieceKey struct {
|
||||
hash string
|
||||
index int
|
||||
}
|
||||
|
||||
func wrapClient(qb qbittorrent.Client) *cacheClient {
|
||||
const (
|
||||
cacheSize = 5000
|
||||
cacheTTL = time.Minute
|
||||
)
|
||||
|
||||
return &cacheClient{
|
||||
qb: qb,
|
||||
propertiesCache: expirable.NewLRU[string, qbittorrent.TorrentProperties](cacheSize, nil, cacheTTL),
|
||||
|
||||
infoClient: sturdyc.New[*qbittorrent.TorrentInfo](cacheSize, 1, cacheTTL, 10,
|
||||
sturdyc.WithEarlyRefreshes(time.Minute, time.Minute*5, time.Second*10, time.Second*10),
|
||||
sturdyc.WithRefreshCoalescing(100, time.Second/4),
|
||||
sturdyc.WithMetrics(newSturdycMetrics()),
|
||||
),
|
||||
|
||||
pieceCache: btrsync.MapOf[pieceKey, int]{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *cacheClient) getInfo(ctx context.Context, hash string) (*qbittorrent.TorrentInfo, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
out := make(map[string]*qbittorrent.TorrentInfo)
|
||||
for _, info := range infos {
|
||||
out[info.Hash] = info
|
||||
}
|
||||
|
||||
return out, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if out[hash] == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return out[hash], nil
|
||||
}
|
||||
|
||||
func (f *cacheClient) getProperties(ctx context.Context, hash string) (*qbittorrent.TorrentProperties, error) {
|
||||
if v, ok := f.propertiesCache.Get(hash); ok {
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
info, err := f.qb.Torrent().GetProperties(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.propertiesCache.Add(hash, *info)
|
||||
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
|
||||
}
|
||||
|
||||
contentI := slices.IndexFunc(contents, func(c *qbittorrent.TorrentContent) bool {
|
||||
return c.Index == contentIndex
|
||||
})
|
||||
if contentI == -1 {
|
||||
return nil, fmt.Errorf("content not found")
|
||||
}
|
||||
|
||||
return contents[contentI], nil
|
||||
}
|
||||
|
||||
func (f *cacheClient) isPieceComplete(ctx context.Context, hash string, pieceIndex int) (bool, error) {
|
||||
cachedPieceState, ok := f.pieceCache.Load(pieceKey{hash: hash, index: pieceIndex})
|
||||
if ok && cachedPieceState == 2 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
completion, err := f.qb.Torrent().GetPiecesStates(ctx, hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for i, v := range completion {
|
||||
f.pieceCache.Store(pieceKey{hash: hash, index: i}, v)
|
||||
}
|
||||
|
||||
if completion[pieceIndex] == 2 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// AsynchronousRefresh implements sturdyc.MetricsRecorder.
|
||||
func (s *sturdycMetrics) AsynchronousRefresh() {
|
||||
return
|
||||
}
|
||||
|
||||
// SynchronousRefresh implements sturdyc.MetricsRecorder.
|
||||
func (s *sturdycMetrics) SynchronousRefresh() {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) ShardIndex(int) {
|
||||
return
|
||||
}
|
||||
|
||||
func must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue