From 055b46acfb792ebeaa8cfce4f140c8744eb5968d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Wed, 6 Aug 2025 15:08:32 +0800 Subject: [PATCH] fead: update add cassandra --- .gitignore | 6 + etc/blockchain.yaml | 18 +- go.mod | 66 +++- go.sum | 152 +++++++-- internal/lib/cassandra/batch.go | 4 +- internal/lib/cassandra/batch_test.go | 32 +- .../lib/cassandra/{cassandra.go => client.go} | 67 ++-- .../{cassandra_test.go => client_test.go} | 71 ++--- internal/lib/cassandra/const.go | 5 +- internal/lib/cassandra/crud.go | 144 ++++----- internal/lib/cassandra/crud_test.go | 288 +++--------------- internal/lib/cassandra/errors.go | 5 - internal/lib/cassandra/ez_transaction.go | 9 +- internal/lib/cassandra/ez_transaction_test.go | 138 +++------ internal/lib/cassandra/go.mod | 70 ----- internal/lib/cassandra/go.sum | 224 -------------- .../init_cassandra_container_test.go | 19 +- internal/lib/cassandra/lock.go | 7 +- internal/lib/cassandra/lock_test.go | 67 ++++ internal/lib/cassandra/metadata.go | 3 +- internal/lib/cassandra/metadata_test.go | 3 +- internal/lib/cassandra/option.go | 33 +- internal/lib/cassandra/option_test.go | 35 +-- internal/lib/cassandra/table.go | 94 +++++- internal/lib/cassandra/table_test.go | 201 +++++++++++- internal/lib/cassandra/utils.go | 38 ++- internal/lib/cassandra/utils_test.go | 117 +++++-- internal/repository/data_source_binance.go | 12 +- internal/svc/cassandra.go | 6 +- internal/svc/service_context.go | 11 +- 30 files changed, 986 insertions(+), 959 deletions(-) create mode 100644 .gitignore rename internal/lib/cassandra/{cassandra.go => client.go} (75%) rename internal/lib/cassandra/{cassandra_test.go => client_test.go} (97%) delete mode 100644 internal/lib/cassandra/errors.go delete mode 100644 internal/lib/cassandra/go.mod delete mode 100644 internal/lib/cassandra/go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6fcd21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +.DS_Store +.env +*.db +*.db-shm +*.db-wal \ No newline at end of file diff --git a/etc/blockchain.yaml b/etc/blockchain.yaml index 0bb8e20..abde5db 100644 --- a/etc/blockchain.yaml +++ b/etc/blockchain.yaml @@ -12,4 +12,20 @@ Binance: RedisCluster: Host: 127.0.0.1:6379 - Type: node \ No newline at end of file + Type: node + +Cassandra: + Hosts: + - 127.0.0.1 + Port: 9042 + Keyspace: sccflex + UseAuth: true + Username: cassandra + Password: cassandra + ConnectTimeoutSec: 10 + NumConns: 2 + MaxRetries: 3 + RetryMin: 500ms + RetryMax: 5s + ReconnectInitial: 500ms + ReconnectMax: 5s \ No newline at end of file diff --git a/go.mod b/go.mod index 2f27521..ccf241a 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,65 @@ module blockchain -go 1.24.4 +go 1.24.5 require ( github.com/alicebob/miniredis/v2 v2.35.0 + github.com/gocql/gocql v1.5.0 github.com/panjf2000/ants/v2 v2.11.3 + github.com/scylladb/gocqlx/v3 v3.0.1 github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.38.0 github.com/zeromicro/go-zero v1.8.5 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.6 ) require ( - github.com/bitly/go-simplejson v0.5.0 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/bitly/go-simplejson v0.5.1 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.2.2+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/jpillora/backoff v1.0.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/scylladb/go-reflectx v1.0.1 // indirect + github.com/shirou/gopsutil/v4 v4.25.5 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + golang.org/x/crypto v0.40.0 // indirect golang.org/x/sync v0.16.0 // indirect ) @@ -51,9 +93,9 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jszwec/csvutil v1.10.0 - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -70,29 +112,29 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.10.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6d3fe0f..e2e3e4f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU= code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/adshao/go-binance/v2 v2.8.3 h1:jwPRcX2u7FIO1pPoXgocyXpXhBI81A41kcmSDzS6uzo= github.com/adshao/go-binance/v2 v2.8.3/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= @@ -8,8 +16,10 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -20,26 +30,52 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -49,6 +85,8 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gocql/gocql v1.5.0 h1:Lth5ZH2Wzf6lQ8UC/ddOQS0BZ/YtMuUtIsbyt9oCBTM= +github.com/gocql/gocql v1.5.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -56,8 +94,12 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -78,6 +120,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -88,8 +132,9 @@ github.com/jszwec/csvutil v1.10.0 h1:upMDUxhQKqZ5ZDCs/wy+8Kib8rZR8I8lOR34yJkdqhI github.com/jszwec/csvutil v1.10.0/go.mod h1:/E4ONrmGkwmWsk9ae9jpXnv9QT8pLHEPcCirMFhxG9I= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -99,24 +144,49 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= @@ -127,6 +197,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= @@ -141,8 +213,16 @@ github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUA github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= +github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= +github.com/scylladb/gocqlx/v3 v3.0.1 h1:JBvOUBz62LQ2lbIgJqQbwVMiDftbtrJSi63KVxvRYOQ= +github.com/scylladb/gocqlx/v3 v3.0.1/go.mod h1:EjbSZM0VR2a57ZUxCRQ3v3CSoWIkH1WTMwxeDbFQorY= +github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= +github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -153,6 +233,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -160,11 +241,19 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= +github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeromicro/go-zero v1.8.5 h1:YkdQhYllE+BPOrxcni0oCewebs7qHfXvjN9glnpcmJQ= github.com/zeromicro/go-zero v1.8.5/go.mod h1:P0DKW1vJx+2J3TReptbeg0H9tRSvehymr0HX4SCfZ6g= go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= @@ -175,8 +264,10 @@ go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4 go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= @@ -189,14 +280,14 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDO go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= @@ -212,6 +303,8 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -220,8 +313,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -232,21 +325,26 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -254,16 +352,16 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= @@ -281,6 +379,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= diff --git a/internal/lib/cassandra/batch.go b/internal/lib/cassandra/batch.go index afdc6ef..ad139b4 100644 --- a/internal/lib/cassandra/batch.go +++ b/internal/lib/cassandra/batch.go @@ -17,7 +17,7 @@ import ( // 2. 研究 自己做 TX_ID 以及 STATUS 的方案 // 這個是已知問題,一定要解決 -func (db *DB) NewBatch(ctx context.Context, keyspace string) *Batch { +func (db *CassandraDB) NewBatch(ctx context.Context, keyspace string) *Batch { session := db.GetSession() return &Batch{ ctx: ctx, @@ -32,7 +32,7 @@ func (db *DB) NewBatch(ctx context.Context, keyspace string) *Batch { type Batch struct { ctx context.Context keyspace string - db *DB + db *CassandraDB batch gocqlx.Batch } diff --git a/internal/lib/cassandra/batch_test.go b/internal/lib/cassandra/batch_test.go index 83b3cd8..d03d7a6 100644 --- a/internal/lib/cassandra/batch_test.go +++ b/internal/lib/cassandra/batch_test.go @@ -1,6 +1,7 @@ package cassandra import ( + "fmt" "testing" "time" @@ -8,18 +9,18 @@ import ( "github.com/stretchr/testify/assert" ) -func TestBatchTx_AllSuccess(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - defer cassandraContainer.Terminate(ctx) +func setupForTest(t *testing.T) (Container, *CassandraDB) { + // 啟動 Cassandra container + dbContainer, err := initCassandraContainer("5.0.4") + assert.NoError(t, err) db, err := NewCassandraDB( - []string{host}, - WithPort(port), + []string{dbContainer.Host}, + WithPort(dbContainer.Port), WithConsistency(gocql.One), WithNumConns(2), ) assert.NoError(t, err) - // 建立 keyspace 和 table err = db.EnsureTable(` CREATE KEYSPACE IF NOT EXISTS my_keyspace @@ -28,7 +29,6 @@ WITH replication = { 'replication_factor': 1 };`) assert.NoError(t, err) - err = db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( id UUID, @@ -39,12 +39,22 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( );`) assert.NoError(t, err) + return dbContainer, db +} + +func TestBatchTx_AllSuccess(t *testing.T) { + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() + now := time.Now() id1 := gocql.TimeUUID() id2 := gocql.TimeUUID() - tx := db.NewBatch(ctx, "my_keyspace") - err = tx.Insert(&MonkeyEntity{ID: id1, Name: "Alice", UpdateAt: now, CreateAt: now}) + tx := db.NewBatch(container.Ctx, "my_keyspace") + err := tx.Insert(&MonkeyEntity{ID: id1, Name: "Alice", UpdateAt: now, CreateAt: now}) assert.NoError(t, err) err = tx.Insert(&MonkeyEntity{ID: id2, Name: "Bob", UpdateAt: now, CreateAt: now}) assert.NoError(t, err) @@ -59,11 +69,11 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( // Alice 應該還在,且被更新 var alice MonkeyEntity alice.ID, alice.Name = id1, "Alice" - err = db.Get(ctx, &alice, "my_keyspace") + err = db.Get(container.Ctx, &alice, "my_keyspace") assert.NoError(t, err) assert.WithinDuration(t, now.Add(5*time.Minute), alice.UpdateAt, time.Second) // Bob 應該被刪除 - err = db.Get(ctx, &MonkeyEntity{ID: id2, Name: "Bob"}, "my_keyspace") + err = db.Get(container.Ctx, &MonkeyEntity{ID: id2, Name: "Bob"}, "my_keyspace") assert.Error(t, err) } diff --git a/internal/lib/cassandra/cassandra.go b/internal/lib/cassandra/client.go similarity index 75% rename from internal/lib/cassandra/cassandra.go rename to internal/lib/cassandra/client.go index c1966dc..2efffd0 100644 --- a/internal/lib/cassandra/cassandra.go +++ b/internal/lib/cassandra/client.go @@ -2,16 +2,17 @@ package cassandra import ( "fmt" - "github.com/gocql/gocql" - "github.com/scylladb/gocqlx/v3" - "github.com/zeromicro/go-zero/core/logx" + "log" "strconv" "strings" "time" + + "github.com/gocql/gocql" + "github.com/scylladb/gocqlx/v3" ) -// conf 是初始化 CassandraDB 所需的內部設定(私有) -type conf struct { +// cassandraConf 是初始化 CassandraDB 所需的內部設定(私有) +type cassandraConf struct { Hosts []string // Cassandra 主機列表 Port int // 連線埠 Keyspace string // 預設使用的 Keyspace @@ -19,7 +20,7 @@ type conf struct { Password string // 認證密碼 Consistency gocql.Consistency // 一致性級別 ConnectTimeoutSec int // 連線逾時秒數 - NumConnect int // 每個節點連線數 + NumConns int // 每個節點連線數 MaxRetries int // 重試次數 UseAuth bool // 是否使用帳號密碼驗證 RetryMin time.Duration // 重試間隔最小值 @@ -29,21 +30,21 @@ type conf struct { CQLVersion string // 執行連線的CQL 版本號 } -// DB 是封裝了 Cassandra 資料庫 session 的結構 -type DB struct { +// CassandraDB 是封裝了 Cassandra 資料庫 session 的結構 +type CassandraDB struct { session gocqlx.Session SaiSupported bool // 是否支援 sai Version string // 資料庫版本 } -// NewDB 初始化並建立 Cassandra 資料庫連線,使用預設設定並可透過Option修改 -func NewDB(hosts []string, opts ...Option) (*DB, error) { - config := &conf{ +// NewCassandraDB 初始化並建立 Cassandra 資料庫連線,使用預設設定並可透過Option修改 +func NewCassandraDB(hosts []string, opts ...Option) (*CassandraDB, error) { + config := &cassandraConf{ Hosts: hosts, Port: defaultPort, Consistency: defaultConsistency, ConnectTimeoutSec: defaultTimeoutSec, - NumConnect: defaultNumConnections, + NumConns: defaultNumConns, MaxRetries: defaultMaxRetries, RetryMin: defaultRetryMin, RetryMax: defaultRetryMax, @@ -62,7 +63,7 @@ func NewDB(hosts []string, opts ...Option) (*DB, error) { cluster.Port = config.Port cluster.Consistency = config.Consistency cluster.Timeout = time.Duration(config.ConnectTimeoutSec) * time.Second - cluster.NumConns = config.NumConnect + cluster.NumConns = config.NumConns cluster.RetryPolicy = &gocql.ExponentialBackoffRetryPolicy{ NumRetries: config.MaxRetries, Min: config.RetryMin, @@ -86,7 +87,7 @@ func NewDB(hosts []string, opts ...Option) (*DB, error) { } waitInterval := cluster.ReconnectionPolicy.GetInterval(i) - logx.Errorf("[CassandraDB] Retry attempt #%d, waiting %s...", i, waitInterval) + log.Printf("[CassandraDB] Retry attempt #%d, waiting %s...", i, waitInterval) time.Sleep(waitInterval) } @@ -106,37 +107,15 @@ func NewDB(hosts []string, opts ...Option) (*DB, error) { Password: config.Password, } } + log.Printf("[CassandraDB] try to connect to Cassandra cluster %v, port: %d", config.Hosts, config.Port) - logx.Infof("[CassandraDB] try to connect to Cassandra cluster %v, port: %d", config.Hosts, config.Port) // 建立 Session s, err := gocqlx.WrapSession(session, nil) if err != nil { return nil, fmt.Errorf("failed to connect to Cassandra cluster: %s", err) } - logx.Infof("[CassandraDB] success init Cassandra cluster") - - db := &DB{ - session: s, - } - version, err := db.getReleaseVersion() - if err != nil { - return nil, fmt.Errorf("failed to get DB version: %s", err) - } - - db.Version = version - db.SaiSupported = isSAISupported(version) - - return db, nil -} - -// NewDBFromSession 用現成的 gocql.Session 封裝 -func NewDBFromSession(session *gocql.Session) (*DB, error) { - s, err := gocqlx.WrapSession(session, nil) - if err != nil { - return nil, fmt.Errorf("failed to wrap gocql session: %w", err) - } - - db := &DB{ + log.Printf("[CassandraDB] success init Cassandra cluster") + db := &CassandraDB{ session: s, } version, err := db.getReleaseVersion() @@ -151,21 +130,21 @@ func NewDBFromSession(session *gocql.Session) (*DB, error) { } // Close 關閉 Cassandra 資料庫連線 -func (db *DB) Close() { +func (db *CassandraDB) Close() { db.session.Close() } // GetSession 返回目前使用的 Cassandra Session -func (db *DB) GetSession() gocqlx.Session { +func (db *CassandraDB) GetSession() gocqlx.Session { return db.session } // EnsureTable 確認並建立資料表 -func (db *DB) EnsureTable(schema string) error { +func (db *CassandraDB) EnsureTable(schema string) error { return db.session.ExecStmt(schema) } -func (db *DB) InitVersionSupport() error { +func (db *CassandraDB) InitVersionSupport() error { version, err := db.getReleaseVersion() if err != nil { return err @@ -175,7 +154,7 @@ func (db *DB) InitVersionSupport() error { return nil } -func (db *DB) getReleaseVersion() (string, error) { +func (db *CassandraDB) getReleaseVersion() (string, error) { var version string stmt := "SELECT release_version FROM system.local" err := db.GetSession().Query(stmt, []string{"release_version"}).Consistency(gocql.One).Scan(&version) diff --git a/internal/lib/cassandra/cassandra_test.go b/internal/lib/cassandra/client_test.go similarity index 97% rename from internal/lib/cassandra/cassandra_test.go rename to internal/lib/cassandra/client_test.go index 2e156fc..0f0bf9d 100644 --- a/internal/lib/cassandra/cassandra_test.go +++ b/internal/lib/cassandra/client_test.go @@ -2,11 +2,42 @@ package cassandra import ( "fmt" + "testing" + "github.com/gocql/gocql" "github.com/stretchr/testify/assert" - "testing" ) +func TestIsSAISupported(t *testing.T) { + tests := []struct { + version string + expected bool + }{ + {"5.0.0", true}, // 5.x 支援 + {"5.1.2", true}, // 5.x 支援 + {"6.0.0", true}, // 6.x 理論上也支援 + {"4.0.8", false}, // 4.0.8 不支援 + {"4.0.9", true}, // 4.0.9 支援 + {"4.1.0", true}, // 4.1.0 支援 + {"4.2.2", true}, // 4.2.2 支援 + {"3.11.10", false}, // 3.x 不支援 + {"3.0.0", false}, + {"", false}, // 空字串,不支援 + {"unknown", false}, // 無效格式 + {"4", false}, // 缺 patch,不支援 + {"4.0", false}, // 缺 patch,不支援 + {"5", false}, // 缺 minor + {"5.0", true}, // 5.0 預設支援 + } + + for _, tt := range tests { + t.Run(tt.version, func(t *testing.T) { + result := isSAISupported(tt.version) + assert.Equal(t, tt.expected, result, "version: %s", tt.version) + }) + } +} + // TestCassandraDB_Integration_TableDriven 使用 table-driven 方式整合測試 func TestCassandraDB_Integration_TableDriven(t *testing.T) { // 啟動 Cassandra container @@ -18,11 +49,11 @@ func TestCassandraDB_Integration_TableDriven(t *testing.T) { // 建立 CassandraDB 連線 hosts := []string{dbContainer.Host} - db, err := NewDB( + db, err := NewCassandraDB( hosts, WithPort(dbContainer.Port), WithConsistency(gocql.One), - WithNumConnects(2), + WithNumConns(2), ) assert.NoError(t, err, "should success create CassandraDB") assert.NotNil(t, db, "db should not be nil") @@ -87,36 +118,6 @@ func TestCassandraDB_Integration_TableDriven(t *testing.T) { } } -func TestIsSAISupported(t *testing.T) { - tests := []struct { - version string - expected bool - }{ - {"5.0.0", true}, // 5.x 支援 - {"5.1.2", true}, // 5.x 支援 - {"6.0.0", true}, // 6.x 理論上也支援 - {"4.0.8", false}, // 4.0.8 不支援 - {"4.0.9", true}, // 4.0.9 支援 - {"4.1.0", true}, // 4.1.0 支援 - {"4.2.2", true}, // 4.2.2 支援 - {"3.11.10", false}, // 3.x 不支援 - {"3.0.0", false}, - {"", false}, // 空字串,不支援 - {"unknown", false}, // 無效格式 - {"4", false}, // 缺 patch,不支援 - {"4.0", false}, // 缺 patch,不支援 - {"5", false}, // 缺 minor - {"5.0", true}, // 5.0 預設支援 - } - - for _, tt := range tests { - t.Run(tt.version, func(t *testing.T) { - result := isSAISupported(tt.version) - assert.Equal(t, tt.expected, result, "version: %s", tt.version) - }) - } -} - func TestCassandraDB_getReleaseVersion(t *testing.T) { type fields struct { Version string @@ -154,11 +155,11 @@ func TestCassandraDB_getReleaseVersion(t *testing.T) { assert.NoError(t, err) // 建立 CassandraDB 連線 hosts := []string{container.Host} - db, err := NewDB( + db, err := NewCassandraDB( hosts, WithPort(container.Port), WithConsistency(gocql.One), - WithNumConnects(2), + WithNumConns(2), ) assert.NoError(t, err) version, err := db.getReleaseVersion() diff --git a/internal/lib/cassandra/const.go b/internal/lib/cassandra/const.go index 6a8eeb3..3f2fcc7 100644 --- a/internal/lib/cassandra/const.go +++ b/internal/lib/cassandra/const.go @@ -8,8 +8,7 @@ import ( // 預設設定常數 const ( - defaultNumConnections = 10 // 預設每個節點的連線數量 - defaultCqlVersion = "3.0.0" + defaultNumConns = 10 // 預設每個節點的連線數量 defaultTimeoutSec = 10 // 預設連線逾時秒數 defaultMaxRetries = 3 // 預設重試次數 defaultPort = 9042 @@ -18,5 +17,5 @@ const ( defaultRetryMax = 30 * time.Second defaultReconnectInitial = 1 * time.Second defaultReconnectMax = 60 * time.Second - spanName = "cassandra" + defaultCqlVersion = "3.0.0" ) diff --git a/internal/lib/cassandra/crud.go b/internal/lib/cassandra/crud.go index 4398bac..e8e0364 100644 --- a/internal/lib/cassandra/crud.go +++ b/internal/lib/cassandra/crud.go @@ -4,15 +4,18 @@ import ( "context" "errors" "fmt" + "reflect" + "time" + "github.com/gocql/gocql" "github.com/scylladb/gocqlx/v3/qb" "github.com/scylladb/gocqlx/v3/table" - "reflect" - "time" ) -// Deprecated: is deprecate please use new func Query.InsertOne() -func (db *DB) Insert(ctx context.Context, document any, keyspace string) error { +var ErrNotFound = fmt.Errorf("not found") + +// Insert 依據 document 自動產生 INSERT 語句並執行 +func (db *CassandraDB) Insert(ctx context.Context, document any, keyspace string) error { metadata, err := GenerateTableMetadata(document, keyspace) if err != nil { return err @@ -32,8 +35,8 @@ func (db *DB) Insert(ctx context.Context, document any, keyspace string) error { // - 如果僅提供 Partition Key,會查到分區內的多筆資料,但由於 .Get() 預設加 LIMIT 1,僅會取得其中一筆(排序第一) // - 若想查詢特定欄位(如 name)但該欄位不是 Primary Key 組成部分,則無法使用 .Get() 查詢,也無法用該欄位直接篩選資料(會報錯) // - 解法是:1. 改變 table 結構使欲查欄位成為 PK,或 2. 建立額外 table 以該欄位為 Partition Key,或 3. 使用 ALLOW FILTERING(不建議) -// Deprecated: is deprecate please use new func Query.Get() -func (db *DB) Get(ctx context.Context, dest any, keyspace string) error { + +func (db *CassandraDB) Get(ctx context.Context, dest any, keyspace string) error { metadata, err := GenerateTableMetadata(dest, keyspace) if err != nil { return err @@ -52,8 +55,7 @@ func (db *DB) Get(ctx context.Context, dest any, keyspace string) error { } // Delete 依據 document 的主鍵產生 DELETE 語句並執行 -// Deprecated: is deprecate please use new func Query.Delete() -func (db *DB) Delete(ctx context.Context, filter any, keyspace string) error { +func (db *CassandraDB) Delete(ctx context.Context, filter any, keyspace string) error { metadata, err := GenerateTableMetadata(filter, keyspace) if err != nil { return err @@ -65,62 +67,11 @@ func (db *DB) Delete(ctx context.Context, filter any, keyspace string) error { return q.ExecRelease() } -// TODO: Cassandra 不支援 OFFSET 方式的分頁(例如查詢第 N 頁) -// 原因:Cassandra 是分散式資料庫,設計上不允許像傳統 SQL 那樣用 OFFSET 跳頁,會導致效能極差 -// ✅ 正確方式為使用 PagingState 做游標式(Cursor-based)分頁,一頁一頁往後翻 -// ✅ 如果需要快取第 N 頁位置,應在應用層儲存每一頁的 PagingState 以供跳轉 -// ❌ Cassandra 不適合直接實作全站排行榜或全表分頁查詢,除非搭配 ElasticSearch 或針對 Partition Key 分頁設計 -// 若未來有特定分區(如 user_id)條件,可考慮實作分區內的分頁邏輯以提高效能 - -// GetAll 取得指定 struct 類型在 Cassandra 中的所有資料 -// - structInstance:用來推斷 table 結構的範例物件(可為指標) -// - result:要寫入的 slice 指標,如 *[]MyStruct -// Deprecated: is deprecate please use new func Query.GetAll() -func (db *DB) GetAll(ctx context.Context, filter any, result any, keyspace string) error { - metadata, err := GenerateTableMetadata(filter, keyspace) - if err != nil { - return err - } - t := table.New(metadata) - - stmt, names := qb.Select(t.Name()).Columns(metadata.Columns...).ToCql() - q := db.GetSession().Query(stmt, names).WithContext(ctx).WithTimestamp(time.Now().UnixNano() / 1e3) - - return q.SelectRelease(result) -} - -// QueryBuilder executes a query with optional conditions on Cassandra table -func (db *DB) QueryBuilder( - ctx context.Context, - tableStruct any, - result any, - keyspace string, - opts ...QueryOption, -) error { - metadata, err := GenerateTableMetadata(tableStruct, keyspace) - if err != nil { - return err - } - tbl := table.New(metadata) - - builder := qb.Select(tbl.Name()).Columns(metadata.Columns...) - bindMap := qb.M{} - for _, opt := range opts { - opt(builder, bindMap) - } - - stmt, names := builder.ToCql() - query := db.GetSession().Query(stmt, names).WithContext(ctx).BindMap(bindMap).WithTimestamp(time.Now().UnixNano() / 1e3) - - return query.SelectRelease(result) -} - // Update 根據 document 欄位產生 UPDATE 語句並執行 // - 只會更新非零值或非 nil 的欄位(零值欄位會被排除) // - 主鍵欄位一定會保留,作為 WHERE 條件使用 // Update 根據 document 產生 UPDATE 語句並執行(只更新非零值欄位,保留主鍵) -// Deprecated: is deprecate please use new func Query.Update() -func (db *DB) Update(ctx context.Context, document any, keyspace string) error { +func (db *CassandraDB) Update(ctx context.Context, document any, keyspace string) error { metadata, err := GenerateTableMetadata(document, keyspace) if err != nil { return err @@ -164,30 +115,6 @@ func (db *DB) Update(ctx context.Context, document any, keyspace string) error { setVals = append(setVals, val.Interface()) } - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - tag := field.Tag.Get("db") - if tag == "" || tag == "-" { - continue - } - val := v.Field(i) - if !val.IsValid() { - continue - } - - if contains(metadata.PartKey, tag) || contains(metadata.SortKey, tag) { - whereCols = append(whereCols, tag) - whereVals = append(whereVals, val.Interface()) - continue - } - - // 只要 pointer 不為 nil,就 update(不管是不是 zero value) - if field.Type.Kind() == reflect.Ptr && !val.IsNil() { - setCols = append(setCols, tag) - setVals = append(setVals, val.Elem().Interface()) - } - } - if len(setCols) == 0 { return fmt.Errorf("no non-zero update fields provided") } @@ -204,3 +131,52 @@ func (db *DB) Update(ctx context.Context, document any, keyspace string) error { return q.ExecRelease() } + +// TODO: Cassandra 不支援 OFFSET 方式的分頁(例如查詢第 N 頁) +// 原因:Cassandra 是分散式資料庫,設計上不允許像傳統 SQL 那樣用 OFFSET 跳頁,會導致效能極差 +// ✅ 正確方式為使用 PagingState 做游標式(Cursor-based)分頁,一頁一頁往後翻 +// ✅ 如果需要快取第 N 頁位置,應在應用層儲存每一頁的 PagingState 以供跳轉 +// ❌ Cassandra 不適合直接實作全站排行榜或全表分頁查詢,除非搭配 ElasticSearch 或針對 Partition Key 分頁設計 +// 若未來有特定分區(如 user_id)條件,可考慮實作分區內的分頁邏輯以提高效能 + +// GetAll 取得指定 struct 類型在 Cassandra 中的所有資料 +// - structInstance:用來推斷 table 結構的範例物件(可為指標) +// - result:要寫入的 slice 指標,如 *[]MyStruct +func (db *CassandraDB) GetAll(ctx context.Context, filter any, result any, keyspace string) error { + metadata, err := GenerateTableMetadata(filter, keyspace) + if err != nil { + return err + } + t := table.New(metadata) + + stmt, names := qb.Select(t.Name()).Columns(metadata.Columns...).ToCql() + q := db.GetSession().Query(stmt, names).WithContext(ctx).WithTimestamp(time.Now().UnixNano() / 1e3) + + return q.SelectRelease(result) +} + +// QueryBuilder executes a query with optional conditions on Cassandra table +func (db *CassandraDB) QueryBuilder( + ctx context.Context, + tableStruct any, + result any, + keyspace string, + opts ...QueryOption, +) error { + metadata, err := GenerateTableMetadata(tableStruct, keyspace) + if err != nil { + return err + } + tbl := table.New(metadata) + + builder := qb.Select(tbl.Name()).Columns(metadata.Columns...) + bindMap := qb.M{} + for _, opt := range opts { + opt(builder, bindMap) + } + + stmt, names := builder.ToCql() + query := db.GetSession().Query(stmt, names).WithContext(ctx).BindMap(bindMap).WithTimestamp(time.Now().UnixNano() / 1e3) + + return query.SelectRelease(result) +} diff --git a/internal/lib/cassandra/crud_test.go b/internal/lib/cassandra/crud_test.go index f781a4b..6ba43b7 100644 --- a/internal/lib/cassandra/crud_test.go +++ b/internal/lib/cassandra/crud_test.go @@ -2,59 +2,21 @@ package cassandra import ( "context" + "fmt" "testing" "time" "github.com/gocql/gocql" "github.com/scylladb/gocqlx/v3/qb" "github.com/stretchr/testify/assert" - "github.com/testcontainers/testcontainers-go" ) -type Consistency struct { - ID gocql.UUID `db:"id" partition:"true"` - ConsistencyName string `db:"consistency_name" sai:"true"` // can editor - ConsistencyType string `db:"consistency_type"` - LastTaskID string `db:"last_task_id"` // ConsistencyTask ID - Target string `db:"target"` // file name can editor - Status string `db:"status" sai:"true"` - ConsistencyMap string `db:"consistency_map"` // JSON string - CreateAT int64 `db:"create_at"` - UpdateAT int64 `db:"update_at"` -} - -func (c *Consistency) TableName() string { - return "consistency" -} - func TestInsert(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - defer cassandraContainer.Terminate(ctx) - - // 連線 - hosts := []string{host} - db, err := NewCassandraDB( - hosts, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - assert.NotNil(t, db) - - // 建立 keyspace + table - err = db.EnsureTable("CREATE KEYSPACE my_keyspace\nWITH replication = {\n 'class': 'SimpleStrategy',\n 'replication_factor': 1\n};\n") - assert.NoError(t, err, "should success ensure table") - - err = db.EnsureTable(` -CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( - id UUID, - name TEXT, - update_at TIMESTAMP, - create_at TIMESTAMP, - PRIMARY KEY ((id), name) -);`) - assert.NoError(t, err) + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() now := time.Now() // 測試案例(可擴充) @@ -94,7 +56,7 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( // 執行測試 for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - err := db.Insert(ctx, &tc.input, "my_keyspace") + err := db.Insert(container.Ctx, &tc.input, "my_keyspace") assert.NoError(t, err) // 驗證寫入 @@ -108,35 +70,11 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( } func TestGet(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - defer cassandraContainer.Terminate(ctx) - - db, err := NewCassandraDB( - []string{host}, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - - err = db.EnsureTable(` -CREATE KEYSPACE IF NOT EXISTS my_keyspace -WITH replication = { - 'class': 'SimpleStrategy', - 'replication_factor': 1 -};`) - assert.NoError(t, err) - - err = db.EnsureTable(` -CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( - id UUID, - name TEXT, - update_at TIMESTAMP, - create_at TIMESTAMP, - PRIMARY KEY ((id), name) -);`) - assert.NoError(t, err) - + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() now := time.Now() monkey := MonkeyEntity{ ID: gocql.TimeUUID(), @@ -146,7 +84,7 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( } // 插入一筆資料 - err = db.Insert(ctx, &monkey, "my_keyspace") + err := db.Insert(container.Ctx, &monkey, "my_keyspace") assert.NoError(t, err) tests := []struct { @@ -169,7 +107,7 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := tc.filter // 預設填入主鍵 - err := db.Get(ctx, &result, "my_keyspace") + err := db.Get(container.Ctx, &result, "my_keyspace") if tc.expect == "" { assert.Error(t, err, "expected error for missing record") @@ -182,36 +120,11 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( } func TestDelete(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - defer cassandraContainer.Terminate(ctx) - - db, err := NewCassandraDB( - []string{host}, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - - // 建立 keyspace & table - err = db.EnsureTable(` -CREATE KEYSPACE IF NOT EXISTS my_keyspace -WITH replication = { - 'class': 'SimpleStrategy', - 'replication_factor': 1 -};`) - assert.NoError(t, err) - - err = db.EnsureTable(` -CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( - id UUID, - name TEXT, - update_at TIMESTAMP, - create_at TIMESTAMP, - PRIMARY KEY ((id), name) -);`) - assert.NoError(t, err) - + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() now := time.Now() monkey := MonkeyEntity{ ID: gocql.TimeUUID(), @@ -221,55 +134,31 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( } // 插入資料 - err = db.Insert(ctx, &monkey, "my_keyspace") + err := db.Insert(container.Ctx, &monkey, "my_keyspace") assert.NoError(t, err) // 先確認有插入成功 verify := MonkeyEntity{ID: monkey.ID, Name: monkey.Name} - err = db.Get(ctx, &verify, "my_keyspace") + err = db.Get(container.Ctx, &verify, "my_keyspace") assert.NoError(t, err) assert.Equal(t, "DeleteMe", verify.Name) // 執行刪除 - err = db.Delete(ctx, &monkey, "my_keyspace") + err = db.Delete(container.Ctx, &monkey, "my_keyspace") assert.NoError(t, err) // 再查,應該查不到 result := MonkeyEntity{ID: monkey.ID, Name: monkey.Name} - err = db.Get(ctx, &result, "my_keyspace") + err = db.Get(container.Ctx, &result, "my_keyspace") assert.Error(t, err, "expected error because record should be deleted") } func TestUpdate(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - defer cassandraContainer.Terminate(ctx) - - db, err := NewCassandraDB( - []string{host}, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - - // 建立 keyspace & table - err = db.EnsureTable(` -CREATE KEYSPACE IF NOT EXISTS my_keyspace -WITH replication = { - 'class': 'SimpleStrategy', - 'replication_factor': 1 -};`) - assert.NoError(t, err) - - err = db.EnsureTable(` -CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( - id UUID, - name TEXT, - update_at TIMESTAMP, - create_at TIMESTAMP, - PRIMARY KEY ((id), name) -);`) - assert.NoError(t, err) + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() now := time.Now() id := gocql.TimeUUID() @@ -281,7 +170,7 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( UpdateAt: now, CreateAt: now, } - err = db.Insert(ctx, &monkey, "my_keyspace") + err := db.Insert(container.Ctx, &monkey, "my_keyspace") assert.NoError(t, err) // Step 2: 更新 UpdateAt 欄位(模擬只更新一欄) @@ -292,7 +181,7 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( UpdateAt: updatedTime, // CreateAt 是零值,不會被更新 } - err = db.Update(ctx, &updateDoc, "my_keyspace") + err = db.Update(container.Ctx, &updateDoc, "my_keyspace") assert.NoError(t, err) // Step 3: 查詢回來驗證更新 @@ -300,31 +189,15 @@ CREATE TABLE IF NOT EXISTS my_keyspace.monkey_entity ( ID: id, Name: "OldName", } - err = db.Get(ctx, &result, "my_keyspace") + err = db.Get(container.Ctx, &result, "my_keyspace") assert.NoError(t, err) assert.WithinDuration(t, updatedTime, result.UpdateAt, time.Second) assert.WithinDuration(t, now, result.CreateAt, time.Second) // 未被更新 } -func setupTestQueryBuilder(t *testing.T) (*CassandraDB, testcontainers.Container, context.Context) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) +func insertSampleConsistency(t *testing.T, db *CassandraDB, ctx context.Context, keyspace string) *Consistency { - // 連線 - hosts := []string{host} - db, err := NewCassandraDB( - hosts, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - assert.NotNil(t, db) - - // 建立 keyspace + table - err = db.EnsureTable("CREATE KEYSPACE my_keyspace\nWITH replication = {\n 'class': 'SimpleStrategy',\n 'replication_factor': 1\n};\n") - assert.NoError(t, err, "should success ensure table") - - err = db.EnsureTable(` + err := db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.consistency ( id UUID, consistency_name TEXT, @@ -339,10 +212,6 @@ func setupTestQueryBuilder(t *testing.T) (*CassandraDB, testcontainers.Container );`) assert.NoError(t, err) - return db, cassandraContainer, ctx -} - -func insertSampleConsistency(t *testing.T, db *CassandraDB, ctx context.Context, keyspace string) *Consistency { c := &Consistency{ ID: gocql.TimeUUID(), ConsistencyName: "query-test", @@ -355,23 +224,26 @@ func insertSampleConsistency(t *testing.T, db *CassandraDB, ctx context.Context, UpdateAT: time.Now().UnixNano(), } - err := db.Insert(ctx, c, keyspace) + err = db.Insert(ctx, c, keyspace) assert.NoError(t, err) return c } func TestQueryBuilder_WithWhere(t *testing.T) { - db, def, ctx := setupTestQueryBuilder(t) - defer def.Terminate(ctx) + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() - saved := insertSampleConsistency(t, db, ctx, "my_keyspace") + saved := insertSampleConsistency(t, db, container.Ctx, "my_keyspace") t.Run("query by id", func(t *testing.T) { var results []*Consistency e := &Consistency{} field := GetCqlTag(e, &e.ID) err := db.QueryBuilder( - ctx, + container.Ctx, &Consistency{}, &results, "my_keyspace", @@ -400,7 +272,7 @@ func TestQueryBuilder_WithWhere(t *testing.T) { e := &Consistency{} field := GetCqlTag(e, &e.ID) err := db.QueryBuilder( - ctx, + container.Ctx, &Consistency{}, &results, "my_keyspace", @@ -419,7 +291,7 @@ func TestQueryBuilder_WithWhere(t *testing.T) { e := &Consistency{} field := GetCqlTag(e, &e.ID) err := db.QueryBuilder( - ctx, + container.Ctx, &Consistency{}, &results, "my_keyspace", @@ -447,7 +319,7 @@ func TestQueryBuilder_WithWhere(t *testing.T) { e := &Consistency{} field := GetCqlTag(e, &e.ID) err := db.QueryBuilder( - ctx, + container.Ctx, &Consistency{}, &results, "my_keyspace", @@ -474,7 +346,7 @@ func TestQueryBuilder_WithWhere(t *testing.T) { var results []*Consistency e := &Consistency{} err := db.QueryBuilder( - ctx, + container.Ctx, e, &results, "my_keyspace", @@ -494,73 +366,3 @@ func TestQueryBuilder_WithWhere(t *testing.T) { }) } - -// ====================================================================================================================== -func TestSearchBySAIFields(t *testing.T) { - container, err := initCassandraContainer("5.0.4") - - ctx := context.Background() - defer container.Container.Terminate(ctx) - - // 連線 - hosts := []string{container.Host} - db, err := NewCassandraDB( - hosts, - WithPort(container.Port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - assert.NotNil(t, db) - - // 建立 keyspace + table - err = db.EnsureTable("CREATE KEYSPACE my_keyspace\nWITH replication = {\n 'class': 'SimpleStrategy',\n 'replication_factor': 1\n};\n") - assert.NoError(t, err, "should success ensure table") - - err = db.EnsureTable(` - CREATE TABLE IF NOT EXISTS my_keyspace.consistency ( - id UUID, - consistency_name TEXT, - last_task_id TEXT, - target TEXT, - status TEXT, - consistency_type TEXT, - consistency_map TEXT, - create_at BIGINT, - update_at BIGINT, - PRIMARY KEY ((id)) - );`) - assert.NoError(t, err) - _ = db.AutoCreateSAIIndexes(&Consistency{}, "my_keyspace") - - c := &Consistency{ - ID: gocql.TimeUUID(), - ConsistencyName: "query-test", - LastTaskID: "task-1", - Target: "test.csv", - Status: "Running", - ConsistencyType: "simple", - ConsistencyMap: `{"example": "value"}`, - CreateAT: time.Now().UnixNano(), - UpdateAT: time.Now().UnixNano(), - } - - err = db.Insert(ctx, c, "my_keyspace") - assert.NoError(t, err) - - results := []Consistency{} - err = db.SearchBySAIFields(ctx, &Consistency{}, &results, "my_keyspace", - []qb.Cmp{qb.Eq("consistency_name")}, - map[string]any{"consistency_name": "query-test"}, - ) - assert.NoError(t, err) - assert.Len(t, results, 1) - - results2 := []Consistency{} - err = db.SearchBySAIFields(ctx, &Consistency{}, &results, "my_keyspace", - []qb.Cmp{qb.Eq("consistency_name")}, - map[string]any{"consistency_name": "vvvvvvv"}, - ) - assert.NoError(t, err) - assert.Len(t, results2, 0) -} diff --git a/internal/lib/cassandra/errors.go b/internal/lib/cassandra/errors.go deleted file mode 100644 index adfd95c..0000000 --- a/internal/lib/cassandra/errors.go +++ /dev/null @@ -1,5 +0,0 @@ -package cassandra - -import "fmt" - -var ErrNotFound = fmt.Errorf("not found") diff --git a/internal/lib/cassandra/ez_transaction.go b/internal/lib/cassandra/ez_transaction.go index 81ab185..84d72bd 100644 --- a/internal/lib/cassandra/ez_transaction.go +++ b/internal/lib/cassandra/ez_transaction.go @@ -3,13 +3,14 @@ package cassandra import ( "context" "fmt" + "reflect" + "time" + "github.com/gocql/gocql" "github.com/scylladb/gocqlx/v3" "github.com/scylladb/gocqlx/v3/qb" "github.com/scylladb/gocqlx/v3/table" "github.com/testcontainers/testcontainers-go/log" - "reflect" - "time" ) /* @@ -68,11 +69,11 @@ type Transaction interface { type transaction struct { ctx context.Context keyspace string - db *DB + db *CassandraDB Steps []OperationLog // 用來記錄所有操作步驟的日誌 } -func NewEZTransaction(ctx context.Context, keyspace string, db *DB) Transaction { +func NewEZTransaction(ctx context.Context, keyspace string, db *CassandraDB) Transaction { return &transaction{ ctx: ctx, keyspace: keyspace, diff --git a/internal/lib/cassandra/ez_transaction_test.go b/internal/lib/cassandra/ez_transaction_test.go index 69c483b..297727b 100644 --- a/internal/lib/cassandra/ez_transaction_test.go +++ b/internal/lib/cassandra/ez_transaction_test.go @@ -1,14 +1,16 @@ package cassandra import ( + "fmt" + "testing" + "github.com/gocql/gocql" "github.com/stretchr/testify/assert" - "testing" ) type TE struct { - ID gocql.UUID `cql:"id" partition:"true"` - Name string `cql:"name"` + ID gocql.UUID `db:"id" partition_key:"true"` + Name string `db:"name"` } func (m *TE) TableName() string { @@ -16,25 +18,13 @@ func (m *TE) TableName() string { } func TestNewEZTransactionInsert(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - defer cassandraContainer.Terminate(ctx) + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() - // 連線 - hosts := []string{host} - db, err := NewCassandraDB( - hosts, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - assert.NotNil(t, db) - - // 建立 keyspace + table - err = db.EnsureTable("CREATE KEYSPACE my_keyspace\nWITH replication = {\n 'class': 'SimpleStrategy',\n 'replication_factor': 1\n};\n") - assert.NoError(t, err, "should success ensure table") - - err = db.EnsureTable(` + err := db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( id UUID PRIMARY KEY, name TEXT @@ -72,10 +62,10 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 每個子案例都使用新的 transaction - tx := NewEZTransaction(ctx, "my_keyspace", db) + tx := NewEZTransaction(container.Ctx, "my_keyspace", db) // 1. 呼叫 Insert - err := tx.Insert(ctx, &tt.doc) + err := tx.Insert(container.Ctx, &tt.doc) assert.NoError(t, err, "Insert() 應該不會錯誤") // 2. 呼叫 Commit,真正寫入 Cassandra @@ -86,7 +76,7 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( var got TE got.ID = tt.doc.ID - err = db.Get(ctx, &got, "my_keyspace") + err = db.Get(container.Ctx, &got, "my_keyspace") assert.NoError(t, err) // 驗證欄位值符合 assert.Equal(t, tt.doc.ID, got.ID, "ID 應一致") @@ -96,25 +86,13 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( } func TestNewEZTransactionDelete(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - defer cassandraContainer.Terminate(ctx) + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() - // 連線 - hosts := []string{host} - db, err := NewCassandraDB( - hosts, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - assert.NotNil(t, db) - - // 建立 keyspace + table - err = db.EnsureTable("CREATE KEYSPACE my_keyspace\nWITH replication = {\n 'class': 'SimpleStrategy',\n 'replication_factor': 1\n};\n") - assert.NoError(t, err, "should success ensure table") - - err = db.EnsureTable(` + err := db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( id UUID PRIMARY KEY, name TEXT @@ -138,14 +116,14 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 每個子案例都使用新的 transaction - tx := NewEZTransaction(ctx, "my_keyspace", db) + tx := NewEZTransaction(container.Ctx, "my_keyspace", db) // 1. 呼叫 Delete - err := tx.Insert(ctx, &tt.doc) + err := tx.Insert(container.Ctx, &tt.doc) assert.NoError(t, err, "Insert() 應該不會錯誤") // 2. 呼叫 Delete - err = tx.Delete(ctx, &tt.doc) + err = tx.Delete(container.Ctx, &tt.doc) assert.NoError(t, err, "Delete() 應該不會錯誤") // 3. 呼叫 Commit,真正寫入 Cassandra @@ -156,41 +134,30 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( var got TE got.ID = tt.doc.ID - err = db.Get(ctx, &got, "my_keyspace") + err = db.Get(container.Ctx, &got, "my_keyspace") assert.Equal(t, err, gocql.ErrNotFound) }) } } func TestNewEZTransactionUpdate(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - t.Cleanup(func() { cassandraContainer.Terminate(ctx) }) + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() - // 1. 連線並建立 keyspace + table - db, err := NewCassandraDB( - []string{host}, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - assert.NotNil(t, db) - - assert.NoError(t, db.EnsureTable(` -CREATE KEYSPACE IF NOT EXISTS my_keyspace -WITH replication = {'class':'SimpleStrategy','replication_factor':1}; -`)) assert.NoError(t, db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( - id UUID PRIMARY KEY, - name TEXT + id UUID PRIMARY KEY, + name TEXT ); `)) // 2. 插入初始資料 id := gocql.TimeUUID() before := TE{ID: id, Name: "Before"} - assert.NoError(t, db.Insert(ctx, &before, "my_keyspace")) + assert.NoError(t, db.Insert(container.Ctx, &before, "my_keyspace")) // 定義多組更新案例 tests := []struct { @@ -207,12 +174,12 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( t.Run(tt.name, func(t *testing.T) { // 為每個案例都重置為 Before // 重新 insert 一次(覆蓋舊值) - assert.NoError(t, db.Insert(ctx, &before, "my_keyspace")) + assert.NoError(t, db.Insert(container.Ctx, &before, "my_keyspace")) // 3. 建立 transaction 並呼叫 Update - tx := NewEZTransaction(ctx, "my_keyspace", db) + tx := NewEZTransaction(container.Ctx, "my_keyspace", db) updateDoc := TE{ID: id, Name: tt.newName} - err := tx.Update(ctx, &updateDoc) + err := tx.Update(container.Ctx, &updateDoc) if tt.wantErr { assert.Error(t, err, "Update() 應該會出錯") return @@ -227,7 +194,7 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( // 5. 查詢並驗證 var got TE got.ID = id - err = db.Get(ctx, &got, "my_keyspace") + err = db.Get(container.Ctx, &got, "my_keyspace") assert.NoError(t, err, "db.Get() 應成功") assert.Equal(t, id, got.ID, "ID 應一致") @@ -237,42 +204,31 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( } func Test_Rollback(t *testing.T) { - ctx, cassandraContainer, host, port := setupCassandraContainer(t) - t.Cleanup(func() { cassandraContainer.Terminate(ctx) }) + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() - // 1. 連線並建立 keyspace + table - db, err := NewCassandraDB( - []string{host}, - WithPort(port), - WithConsistency(gocql.One), - WithNumConns(2), - ) - assert.NoError(t, err) - assert.NotNil(t, db) - - assert.NoError(t, db.EnsureTable(` -CREATE KEYSPACE IF NOT EXISTS my_keyspace -WITH replication = {'class':'SimpleStrategy','replication_factor':1}; -`)) assert.NoError(t, db.EnsureTable(` CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( - id UUID PRIMARY KEY, - name TEXT + id UUID PRIMARY KEY, + name TEXT ); `)) // 3. 用 Transaction 插入一筆資料,並 Commit id := gocql.TimeUUID() doc := TE{ID: id, Name: "Alice"} - tx := NewEZTransaction(ctx, "my_keyspace", db) - err = tx.Insert(ctx, &doc) + tx := NewEZTransaction(container.Ctx, "my_keyspace", db) + err := tx.Insert(container.Ctx, &doc) assert.NoError(t, err) err = tx.Commit() assert.NoError(t, err) // 4. Query 確認資料已存在 var got TE got.ID = id - err = db.Get(ctx, &got, "my_keyspace") + err = db.Get(container.Ctx, &got, "my_keyspace") assert.NoError(t, err) assert.Equal(t, got.Name, doc.Name) @@ -282,7 +238,7 @@ CREATE TABLE IF NOT EXISTS my_keyspace.test_entity ( var afterGot TE afterGot.ID = id - err = db.Get(ctx, &afterGot, "my_keyspace") + err = db.Get(container.Ctx, &afterGot, "my_keyspace") assert.Error(t, err) // Output: diff --git a/internal/lib/cassandra/go.mod b/internal/lib/cassandra/go.mod deleted file mode 100644 index f572fa3..0000000 --- a/internal/lib/cassandra/go.mod +++ /dev/null @@ -1,70 +0,0 @@ -module gitlab.supermicro.com/storage/cassandra - -go 1.24.2 - -require ( - github.com/gocql/gocql v1.7.0 - github.com/scylladb/gocqlx/v3 v3.0.1 - github.com/stretchr/testify v1.10.0 - github.com/testcontainers/testcontainers-go v0.37.0 - github.com/zeromicro/go-zero v1.8.3 -) - -require ( - dario.cat/mergo v1.0.1 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.0.1+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.8.2 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect - github.com/moby/sys/sequential v0.5.0 // indirect - github.com/moby/sys/user v0.1.0 // indirect - github.com/moby/sys/userns v0.1.0 // indirect - github.com/moby/term v0.5.0 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/scylladb/go-reflectx v1.0.1 // indirect - github.com/shirou/gopsutil/v4 v4.25.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/sys v0.32.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/internal/lib/cassandra/go.sum b/internal/lib/cassandra/go.sum deleted file mode 100644 index 15828b0..0000000 --- a/internal/lib/cassandra/go.sum +++ /dev/null @@ -1,224 +0,0 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= -github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= -github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= -github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= -github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= -github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= -github.com/scylladb/gocqlx/v3 v3.0.1 h1:JBvOUBz62LQ2lbIgJqQbwVMiDftbtrJSi63KVxvRYOQ= -github.com/scylladb/gocqlx/v3 v3.0.1/go.mod h1:EjbSZM0VR2a57ZUxCRQ3v3CSoWIkH1WTMwxeDbFQorY= -github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= -github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= -github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeromicro/go-zero v1.8.3 h1:AwpBJQLAsZAt4OOnK0eR8UU1Ja2RFBIXfKkHdnXQKfc= -github.com/zeromicro/go-zero v1.8.3/go.mod h1:EnuEA3XdIQvAvc4WWTskRTO0jM2/aQi7OXv1gKWRNJ0= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= -google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/internal/lib/cassandra/init_cassandra_container_test.go b/internal/lib/cassandra/init_cassandra_container_test.go index 3be2784..7522815 100644 --- a/internal/lib/cassandra/init_cassandra_container_test.go +++ b/internal/lib/cassandra/init_cassandra_container_test.go @@ -3,10 +3,11 @@ package cassandra import ( "context" "fmt" + "time" + "github.com/gocql/gocql" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" - "time" ) type Container struct { @@ -91,3 +92,19 @@ type CatEntity struct { func (m *CatEntity) TableName() string { return "cat_entity" } + +type Consistency struct { + ID gocql.UUID `db:"id" partition_key:"true"` + ConsistencyName string `db:"consistency_name"` // can editor + ConsistencyType string `db:"consistency_type"` + LastTaskID string `db:"last_task_id"` // ConsistencyTask ID + Target string `db:"target"` // file name can editor + Status string `db:"status"` + ConsistencyMap string `db:"consistency_map"` // JSON string + CreateAT int64 `db:"create_at"` + UpdateAT int64 `db:"update_at"` +} + +func (c *Consistency) TableName() string { + return "consistency" +} diff --git a/internal/lib/cassandra/lock.go b/internal/lib/cassandra/lock.go index 327771e..a07d7bf 100644 --- a/internal/lib/cassandra/lock.go +++ b/internal/lib/cassandra/lock.go @@ -3,8 +3,9 @@ package cassandra import ( "context" "fmt" - "github.com/scylladb/gocqlx/v3/qb" "time" + + "github.com/scylladb/gocqlx/v3/qb" ) const ( @@ -35,7 +36,7 @@ func WithNoLockExpire() LockOption { // TryLock 嘗試在表上插入一筆唯一鍵(IF NOT EXISTS)作為鎖 // 預設 30 秒 TTL,可透過 option 調整或取消 TTL -func (db *DB) TryLock( +func (db *CassandraDB) TryLock( ctx context.Context, document any, keyspace string, @@ -80,7 +81,7 @@ func (db *DB) TryLock( } // UnLock 砍掉鎖,其實就是 Delete -func (db *DB) UnLock(ctx context.Context, filter any, keyspace string) error { +func (db *CassandraDB) UnLock(ctx context.Context, filter any, keyspace string) error { if filter == nil { return fmt.Errorf("unlock failed: nil filter") } diff --git a/internal/lib/cassandra/lock_test.go b/internal/lib/cassandra/lock_test.go index 5bf5f89..c422898 100644 --- a/internal/lib/cassandra/lock_test.go +++ b/internal/lib/cassandra/lock_test.go @@ -1 +1,68 @@ package cassandra + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type LockTest struct { + ID string `db:"id" partition_key:"true"` + Holder string `db:"holder"` +} + +func (l *LockTest) TableName() string { return "lock_test" } + +func TestTryLockAndUnLock(t *testing.T) { + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + fmt.Println("[TEST] Container terminated") + }() + + assert.NoError(t, db.EnsureTable(` +CREATE TABLE IF NOT EXISTS my_keyspace.lock_test ( + id TEXT PRIMARY KEY, + holder TEXT +); +`)) + + lockID := "lock-123" + holder := "daniel" + + lockDoc := &LockTest{ID: lockID, Holder: holder} + + t.Run("acquire lock - success", func(t *testing.T) { + err := db.TryLock(container.Ctx, lockDoc, "my_keyspace") + assert.NoError(t, err, "TryLock 應該成功") + }) + + t.Run("acquire lock again - fail", func(t *testing.T) { + err := db.TryLock(container.Ctx, lockDoc, "my_keyspace") + assert.Error(t, err, "重複上鎖應該失敗") + }) + + t.Run("unlock", func(t *testing.T) { + err := db.UnLock(container.Ctx, lockDoc, "my_keyspace") + assert.NoError(t, err, "UnLock 應該成功") + }) + + t.Run("lock with TTL", func(t *testing.T) { + lockWithTTL := &LockTest{ID: "lock-ttl", Holder: "jack"} + err := db.TryLock(container.Ctx, lockWithTTL, "my_keyspace", WithLockTTL(2*time.Second)) + assert.NoError(t, err) + // 兩秒後嘗試再次上鎖應該成功(TTL 過期) + time.Sleep(3 * time.Second) + err = db.TryLock(container.Ctx, lockWithTTL, "my_keyspace") + assert.NoError(t, err) + _ = db.UnLock(container.Ctx, lockWithTTL, "my_keyspace") + }) + + t.Run("unlock not exist", func(t *testing.T) { + nonExist := &LockTest{ID: "not-exist", Holder: "nobody"} + err := db.UnLock(container.Ctx, nonExist, "my_keyspace") + assert.Error(t, err, "unlock 不存在的鎖應該失敗") + }) +} diff --git a/internal/lib/cassandra/metadata.go b/internal/lib/cassandra/metadata.go index 2e0046f..a847a84 100644 --- a/internal/lib/cassandra/metadata.go +++ b/internal/lib/cassandra/metadata.go @@ -2,8 +2,9 @@ package cassandra import ( "fmt" - "github.com/scylladb/gocqlx/v3/table" "reflect" + + "github.com/scylladb/gocqlx/v3/table" ) // GenerateTableMetadata 根據傳入的 struct 產生 table.Metadata diff --git a/internal/lib/cassandra/metadata_test.go b/internal/lib/cassandra/metadata_test.go index 7621308..d9a2c18 100644 --- a/internal/lib/cassandra/metadata_test.go +++ b/internal/lib/cassandra/metadata_test.go @@ -1,9 +1,10 @@ package cassandra import ( + "testing" + "github.com/scylladb/gocqlx/v3/table" "github.com/stretchr/testify/assert" - "testing" ) // TestGenerateTableMetadata 測試 GenerateTableMetadata 函式 diff --git a/internal/lib/cassandra/option.go b/internal/lib/cassandra/option.go index e6781e6..4135765 100644 --- a/internal/lib/cassandra/option.go +++ b/internal/lib/cassandra/option.go @@ -1,29 +1,30 @@ package cassandra import ( - "github.com/scylladb/gocqlx/v3/qb" "time" + "github.com/scylladb/gocqlx/v3/qb" + "github.com/gocql/gocql" ) // Option 是設定選項的函數型別 -type Option func(*conf) +type Option func(*cassandraConf) func WithPort(port int) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.Port = port } } func WithKeyspace(keyspace string) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.Keyspace = keyspace } } func WithAuth(username, password string) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.Username = username c.Password = password c.UseAuth = true @@ -31,55 +32,55 @@ func WithAuth(username, password string) Option { } func WithConsistency(consistency gocql.Consistency) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.Consistency = consistency } } func WithConnectTimeoutSec(timeout int) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.ConnectTimeoutSec = timeout } } -func WithNumConnects(numConnects int) Option { - return func(c *conf) { - c.NumConnect = numConnects +func WithNumConns(numConns int) Option { + return func(c *cassandraConf) { + c.NumConns = numConns } } func WithMaxRetries(maxRetries int) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.MaxRetries = maxRetries } } func WithRetryMin(duration time.Duration) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.RetryMin = duration } } func WithRetryMax(duration time.Duration) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.RetryMax = duration } } func WithReconnectInitial(duration time.Duration) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.ReconnectInitial = duration } } func WithReconnectMax(duration time.Duration) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.ReconnectMax = duration } } func WithCQLVersion(version string) Option { - return func(c *conf) { + return func(c *cassandraConf) { c.CQLVersion = version } } diff --git a/internal/lib/cassandra/option_test.go b/internal/lib/cassandra/option_test.go index 91f1124..3a31aef 100644 --- a/internal/lib/cassandra/option_test.go +++ b/internal/lib/cassandra/option_test.go @@ -1,36 +1,37 @@ package cassandra import ( - "github.com/gocql/gocql" - "github.com/stretchr/testify/assert" "testing" "time" + + "github.com/gocql/gocql" + "github.com/stretchr/testify/assert" ) func TestOptions(t *testing.T) { tests := []struct { name string option Option - check func(conf *conf) + check func(conf *cassandraConf) }{ { name: "WithPort", option: WithPort(1234), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, 1234, conf.Port, "Port 設定錯誤") }, }, { name: "WithKeyspace", option: WithKeyspace("my_keyspace"), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, "my_keyspace", conf.Keyspace, "Keyspace 設定錯誤") }, }, { name: "WithAuth", option: WithAuth("user", "pass"), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, "user", conf.Username, "Username 設定錯誤") assert.Equal(t, "pass", conf.Password, "Password 設定錯誤") assert.True(t, conf.UseAuth, "UseAuth 應該為 true") @@ -39,56 +40,56 @@ func TestOptions(t *testing.T) { { name: "WithConsistency", option: WithConsistency(gocql.Quorum), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, gocql.Quorum, conf.Consistency, "Consistency 設定錯誤") }, }, { name: "WithConnectTimeoutSec", option: WithConnectTimeoutSec(45), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, 45, conf.ConnectTimeoutSec, "ConnectTimeoutSec 設定錯誤") }, }, { name: "WithNumConns", - option: WithNumConnects(10), - check: func(conf *conf) { - assert.Equal(t, 10, conf.NumConnect, "NumConns 設定錯誤") + option: WithNumConns(10), + check: func(conf *cassandraConf) { + assert.Equal(t, 10, conf.NumConns, "NumConns 設定錯誤") }, }, { name: "WithMaxRetries", option: WithMaxRetries(5), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, 5, conf.MaxRetries, "MaxRetries 設定錯誤") }, }, { name: "WithRetryMin", option: WithRetryMin(2 * time.Second), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, 2*time.Second, conf.RetryMin, "RetryMin 設定錯誤") }, }, { name: "WithRetryMax", option: WithRetryMax(10 * time.Second), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, 10*time.Second, conf.RetryMax, "RetryMax 設定錯誤") }, }, { name: "WithReconnectInitial", option: WithReconnectInitial(1 * time.Second), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, 1*time.Second, conf.ReconnectInitial, "ReconnectInitial 設定錯誤") }, }, { name: "WithReconnectMax", option: WithReconnectMax(5 * time.Second), - check: func(conf *conf) { + check: func(conf *cassandraConf) { assert.Equal(t, 5*time.Second, conf.ReconnectMax, "ReconnectMax 設定錯誤") }, }, @@ -98,7 +99,7 @@ func TestOptions(t *testing.T) { tc := tc // 避免 closure 捕捉迴圈變數 t.Run(tc.name, func(t *testing.T) { // 為每個測試案例產生一個新的 cassandraConf 實例 - conf := &conf{} + conf := &cassandraConf{} // 套用 Option tc.option(conf) // 執行檢查 diff --git a/internal/lib/cassandra/table.go b/internal/lib/cassandra/table.go index caf34fd..9de6a61 100644 --- a/internal/lib/cassandra/table.go +++ b/internal/lib/cassandra/table.go @@ -2,14 +2,16 @@ package cassandra import ( "context" + "errors" "fmt" - "github.com/scylladb/gocqlx/v3/qb" - "github.com/scylladb/gocqlx/v3/table" "reflect" "time" + + "github.com/scylladb/gocqlx/v3/qb" + "github.com/scylladb/gocqlx/v3/table" ) -func (db *DB) AutoCreateSAIIndexes(doc any, keyspace string) error { +func (db *CassandraDB) AutoCreateSAIIndexes(doc any, keyspace string) error { metadata, err := GenerateTableMetadata(doc, keyspace) if err != nil { return err @@ -21,7 +23,7 @@ func (db *DB) AutoCreateSAIIndexes(doc any, keyspace string) error { for i := 0; i < t.NumField(); i++ { f := t.Field(i) if f.Tag.Get("sai") == "true" { - col := f.Tag.Get("cql") + col := f.Tag.Get("db") if col == "" { col = toSnakeCase(f.Name) } @@ -35,7 +37,7 @@ func (db *DB) AutoCreateSAIIndexes(doc any, keyspace string) error { } type Query struct { - db *DB + db *CassandraDB ctx context.Context table string keyspace string @@ -46,6 +48,7 @@ type Query struct { limit uint document any sets []setField // 欲更新欄位及其值 + errs []error } type orderBy struct { @@ -58,7 +61,7 @@ type setField struct { Val any } -func (db *DB) Model(ctx context.Context, document any, keyspace string) *Query { +func (db *CassandraDB) Model(ctx context.Context, document any, keyspace string) *Query { metadata, _ := GenerateTableMetadata(document, keyspace) return &Query{ @@ -83,7 +86,7 @@ func (q *Query) Where(cmp qb.Cmp, args map[string]any) *Query { isPartition := contains(metadata.PartKey, k) isSAI := IsSAIField(q.document, k) if !isPartition && !isSAI { - panic(fmt.Sprintf("field %s must be partition key or SAI index", k)) + q.errs = append(q.errs, fmt.Errorf("field %s must be partition key or SAI index", k)) } q.bindMap[k] = args[k] } @@ -97,11 +100,13 @@ func (q *Query) Where(cmp qb.Cmp, args map[string]any) *Query { func (q *Query) Select(cols ...string) *Query { q.columns = append(q.columns, cols...) + return q } func (q *Query) OrderBy(column string, order qb.Order) *Query { q.orders = append(q.orders, orderBy{Column: column, Order: order}) + return q } @@ -113,10 +118,15 @@ func (q *Query) Limit(limit uint) *Query { func (q *Query) Set(col string, val any) *Query { q.sets = append(q.sets, setField{Col: col, Val: val}) + return q } func (q *Query) Scan(dest any) error { + if len(q.errs) > 0 { + return errors.Join(q.errs...) + } + builder := qb.Select(q.table) if len(q.columns) > 0 { builder = builder.Columns(q.columns...) @@ -140,15 +150,31 @@ func (q *Query) Scan(dest any) error { } query = query.BindMap(q.bindMap) - return query.SelectRelease(dest) + // 型態判斷自動選用單筆/多筆查詢 + destType := reflect.TypeOf(dest) + if destType.Kind() != reflect.Ptr { + return fmt.Errorf("dest must be a pointer") + } + elemType := destType.Elem() + if elemType.Kind() == reflect.Slice { + return query.SelectRelease(dest) // 多筆 + } else if elemType.Kind() == reflect.Struct { + return query.GetRelease(dest) // 單筆 + } else { + return fmt.Errorf("dest must be pointer to struct or slice") + } } func (q *Query) Take(dest any) error { q.limit = 1 + return q.Scan(dest) } func (q *Query) Delete() error { + if len(q.errs) > 0 { + return errors.Join(q.errs...) + } // 拿 partition key 清單 metadata, err := GenerateTableMetadata(q.document, q.keyspace) if err != nil { @@ -181,6 +207,10 @@ func (q *Query) Delete() error { } func (q *Query) Update() error { + if len(q.errs) > 0 { + return errors.Join(q.errs...) + } + if q.document == nil { return fmt.Errorf("update requires modelType to check partition keys") } @@ -190,15 +220,35 @@ func (q *Query) Update() error { return fmt.Errorf("update: failed to get table metadata: %w", err) } - // 檢查 partition key 是否都在 bindMap - missingKeys := make([]string, 0) + // 先收集所有可被當作主查詢條件的欄位 + allowed := make(map[string]struct{}) + + // 收集 partition_key for _, pk := range metadata.PartKey { - if _, ok := q.bindMap[pk]; !ok { - missingKeys = append(missingKeys, pk) + allowed[pk] = struct{}{} + } + + // 收集所有 sai 欄位 + for _, f := range reflect.VisibleFields(reflect.TypeOf(q.document)) { + if f.Tag.Get("sai") == "true" { + col := f.Tag.Get("db") + if col == "" { + col = toSnakeCase(f.Name) + } + allowed[col] = struct{}{} } } - if len(missingKeys) > 0 { - return fmt.Errorf("update operation requires all partition keys in WHERE: missing %v", missingKeys) + + // 檢查 bindMap 有沒有 hit 到 + hasCondition := false + for k := range q.bindMap { + if _, ok := allowed[k]; ok { + hasCondition = true + break + } + } + if !hasCondition { + return fmt.Errorf("update/delete requires at least one partition_key or sai indexed field in WHERE") } // 至少要有一個 set 欄位 @@ -239,6 +289,10 @@ func (q *Query) Update() error { } func (q *Query) InsertOne(data any) error { + if len(q.errs) > 0 { + return errors.Join(q.errs...) + } + metadata, err := GenerateTableMetadata(q.document, q.keyspace) if err != nil { return err @@ -256,6 +310,10 @@ func (q *Query) InsertOne(data any) error { } func (q *Query) InsertMany(documents any) error { + if len(q.errs) > 0 { + return errors.Join(q.errs...) + } + v := reflect.ValueOf(documents) if v.Kind() != reflect.Slice { return fmt.Errorf("InsertMany: input must be a slice") @@ -274,6 +332,10 @@ func (q *Query) InsertMany(documents any) error { } func (q *Query) GetAll(dest any) error { + if len(q.errs) > 0 { + return errors.Join(q.errs...) + } + metadata, err := GenerateTableMetadata(q.document, q.keyspace) if err != nil { return err @@ -287,6 +349,10 @@ func (q *Query) GetAll(dest any) error { } func (q *Query) Count() (int64, error) { + if len(q.errs) > 0 { + return 0, errors.Join(q.errs...) + } + metadata, err := GenerateTableMetadata(q.document, q.keyspace) if err != nil { return 0, err diff --git a/internal/lib/cassandra/table_test.go b/internal/lib/cassandra/table_test.go index aed2c9e..55eda71 100644 --- a/internal/lib/cassandra/table_test.go +++ b/internal/lib/cassandra/table_test.go @@ -3,14 +3,17 @@ package cassandra import ( "context" "testing" + "time" + + "github.com/gocql/gocql" "github.com/scylladb/gocqlx/v3/qb" "github.com/stretchr/testify/assert" ) -func TestQueryBuilder_TableDriven(t *testing.T) { +func TestQueryBuilder(t *testing.T) { ctx := context.Background() - db := &DB{} // 可以用 mock DB + db := &CassandraDB{} // 可以用 mock DB type args struct { cmp qb.Cmp @@ -92,14 +95,16 @@ func TestQueryBuilder_TableDriven(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - run := func() { - q := db.Model(ctx, &MonkeyEntity{}, "my_keyspace"). - Where(tc.args.cmp, tc.args.whereArg). - Select(tc.args.selects...). - OrderBy(tc.args.orderCol, tc.args.order). - Limit(tc.args.limit). - Set(tc.args.setCol, tc.args.setVal) + q := db.Model(ctx, &MonkeyEntity{}, "my_keyspace"). + Where(tc.args.cmp, tc.args.whereArg). + Select(tc.args.selects...). + OrderBy(tc.args.orderCol, tc.args.order). + Limit(tc.args.limit). + Set(tc.args.setCol, tc.args.setVal) + if tc.wantPanic { + assert.Error(t, q.Update()) + } else { assert.Equal(t, tc.wantColumns, q.columns) if len(q.orders) > 0 { assert.Equal(t, tc.wantOrderCol, q.orders[0].Column) @@ -111,17 +116,11 @@ func TestQueryBuilder_TableDriven(t *testing.T) { assert.Equal(t, tc.wantSetVal, q.sets[0].Val) } } - - if tc.wantPanic { - assert.Panics(t, run) - } else { - assert.NotPanics(t, run) - } }) } } -func TestQuery_Select_TableDriven(t *testing.T) { +func TestQuery_Select(t *testing.T) { tests := []struct { name string selectCalls [][]string @@ -159,3 +158,173 @@ func TestQuery_Select_TableDriven(t *testing.T) { }) } } + +func TestQuery_Count(t *testing.T) { + // 準備測試用資料 + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + }() + + db.AutoCreateSAIIndexes(&MonkeyEntity{}, "my_keyspace") + now := time.Now().UTC() + // 批量插入資料 + docs := []MonkeyEntity{ + {ID: gocql.TimeUUID(), Name: "Alice", CreateAt: now, UpdateAt: now}, + {ID: gocql.TimeUUID(), Name: "Bob", CreateAt: now, UpdateAt: now}, + {ID: gocql.TimeUUID(), Name: "Alice", CreateAt: now, UpdateAt: now}, + } + for _, doc := range docs { + assert.NoError(t, db.Insert(container.Ctx, &doc, "my_keyspace")) + } + + tests := []struct { + name string + filterName string + wantCount int64 + }{ + {"CountAll", "", 3}, + {"CountAlice", "Alice", 2}, + {"CountBob", "Bob", 1}, + {"CountNobody", "Charlie", 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := db.Model(container.Ctx, &MonkeyEntity{}, "my_keyspace") + if tt.filterName != "" { + q = q.Where(qb.Eq("name"), qb.M{"name": tt.filterName}) + } + count, err := q.Count() + assert.NoError(t, err) + assert.Equal(t, tt.wantCount, count) + }) + } +} + +type TestUser struct { + ID gocql.UUID `db:"id" partition_key:"true"` + Name string `db:"name" sai:"true"` + Age int64 `db:"age"` +} + +func (TestUser) TableName() string { return "test_user" } + +func TestQueryBasicFlow(t *testing.T) { + // 啟動 Cassandra container + container, db := setupForTest(t) + defer func() { + _ = container.Container.Terminate(container.Ctx) + }() + + ctx := context.Background() + keyspace := "my_keyspace" + err := db.EnsureTable(` +CREATE TABLE IF NOT EXISTS my_keyspace.test_user ( + id UUID, + name TEXT, + age BIGINT, + PRIMARY KEY (id) +);`) + assert.NoError(t, err) + err = db.AutoCreateSAIIndexes(&TestUser{}, keyspace) + assert.NoError(t, err) + // 測試資料 + u1 := TestUser{ID: gocql.TimeUUID(), Name: "Alice", Age: 20} + u2 := TestUser{ID: gocql.TimeUUID(), Name: "Bob", Age: 22} + u3 := TestUser{ID: gocql.TimeUUID(), Name: "Carol", Age: 23} + + // InsertOne/InsertMany + t.Run("InsertOne", func(t *testing.T) { + q := db.Model(ctx, TestUser{}, keyspace) + assert.NoError(t, q.InsertOne(u1)) + }) + + t.Run("InsertMany", func(t *testing.T) { + q := db.Model(ctx, TestUser{}, keyspace) + assert.NoError(t, q.InsertMany([]TestUser{u2, u3})) + }) + + // GetAll + t.Run("GetAll", func(t *testing.T) { + var got []TestUser + q := db.Model(ctx, TestUser{}, keyspace) + assert.NoError(t, q.GetAll(&got)) + assert.GreaterOrEqual(t, len(got), 3) + }) + + // Count + t.Run("Count All", func(t *testing.T) { + q := db.Model(ctx, TestUser{}, keyspace) + count, err := q.Count() + assert.NoError(t, err) + assert.GreaterOrEqual(t, count, int64(3)) + }) + + // Delete + t.Run("Delete Carol", func(t *testing.T) { + q2 := db.Model(ctx, TestUser{}, keyspace) + q2.Where(qb.Eq("id"), map[string]any{"id": u3.ID}) + assert.NoError(t, q2.Delete()) + // 驗證已刪除 + var user TestUser + err := db.Model(ctx, TestUser{}, keyspace). + Where(qb.Eq("id"), map[string]any{"id": u3.ID}).Scan(&user) + assert.Error(t, err) + + q3 := db.Model(ctx, TestUser{}, keyspace) + count, err := q3.Count() + assert.NoError(t, err) + assert.GreaterOrEqual(t, count, int64(2)) + }) + + // Scan + t.Run("Scan Find Alice", func(t *testing.T) { + var user []TestUser + err := db.Model(ctx, TestUser{}, keyspace). + Where(qb.Eq("name"), map[string]any{"name": "Alice"}).Scan(&user) + + assert.NoError(t, err) + assert.Equal(t, u1.Name, user[0].Name) + }) + // + // Take (僅取一筆) + t.Run("Take Get Bob", func(t *testing.T) { + var user TestUser + q2 := db.Model(ctx, TestUser{}, keyspace). + Where(qb.Eq("name"), map[string]any{"name": "Bob"}) + assert.NoError(t, q2.Take(&user)) + assert.Equal(t, u2.Name, user.Name) + }) + // Update + t.Run("Update Age of Alice", func(t *testing.T) { + q := db.Model(ctx, TestUser{}, keyspace) + assert.NoError(t, q.InsertMany([]TestUser{u1, u2, u3})) + + err = db.Model(ctx, + TestUser{}, keyspace). + Where(qb.Eq("id"), map[string]any{"id": u1.ID}). + Set("age", 30). + Update() + + assert.NoError(t, err) + // 驗證 + var user TestUser + assert.NoError(t, db.Model(ctx, TestUser{}, keyspace). + Where(qb.Eq("id"), map[string]any{"id": u1.ID}).Take(&user)) + assert.Equal(t, int64(30), user.Age) + }) + + // In 這個 case 不通過,原因是 sai key 也不一定可以確認 cassandra 分區 + t.Run("In", func(t *testing.T) { + q := db.Model(ctx, TestUser{}, keyspace) + assert.NoError(t, q.InsertMany([]TestUser{u1, u2, u3})) + + var user []TestUser + err = db.Model(ctx, + TestUser{}, keyspace). + Where(qb.In("name"), map[string]any{"name": []string{u1.Name, u2.Name}}). + Scan(&user) + assert.Error(t, err) + }) +} diff --git a/internal/lib/cassandra/utils.go b/internal/lib/cassandra/utils.go index fe6bd85..11541ac 100644 --- a/internal/lib/cassandra/utils.go +++ b/internal/lib/cassandra/utils.go @@ -1,18 +1,34 @@ package cassandra import ( + "log" "reflect" "unicode" ) -// 判斷字串是否存在於 slice 中 -func contains(list []string, target string) bool { - for _, item := range list { - if item == target { - return true +// GetCqlTag 取得指定欄位的 cql tag +// model 必須為 struct 指標,fieldPtr 為該 struct 欄位的指標 +func GetCqlTag(model interface{}, fieldPtr interface{}) string { + v := reflect.ValueOf(model) + // 確保 model 為 struct 指標 + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + log.Printf("should be a pointer to struct") + + return "" + } + + s := v.Elem() + // 遍歷所有欄位,找出地址與傳入 fieldPtr 相符的欄位 + for i := 0; i < s.NumField(); i++ { + field := s.Type().Field(i) + fieldVal := s.Field(i) + // 如果能取地址且地址與 fieldPtr 相等,則取得 tag + if fieldVal.CanAddr() && fieldVal.Addr().Interface() == fieldPtr { + return field.Tag.Get("db") } } - return false + + return "" } // toSnakeCase 將 CamelCase 字串轉換為 snake_case @@ -40,3 +56,13 @@ func isZero(v reflect.Value) bool { return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) } } + +// 判斷字串是否存在於 slice 中 +func contains(list []string, target string) bool { + for _, item := range list { + if item == target { + return true + } + } + return false +} diff --git a/internal/lib/cassandra/utils_test.go b/internal/lib/cassandra/utils_test.go index 2d88a4a..5556da9 100644 --- a/internal/lib/cassandra/utils_test.go +++ b/internal/lib/cassandra/utils_test.go @@ -1,37 +1,92 @@ package cassandra import ( - "github.com/stretchr/testify/assert" "reflect" "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/gocql/gocql" ) -func TestContains(t *testing.T) { - type testCase struct { - name string - list []string - target string - expected bool +func TestGetCqlTag(t *testing.T) { + monkey := &MonkeyEntity{ + // 為了測試用,欄位內容可以不給值 + ID: gocql.TimeUUID(), + Name: "TestMonkey", + UpdateAt: time.Now(), + CreateAt: time.Now(), } - tests := []testCase{ - {"contains first", []string{"a", "b", "c"}, "a", true}, - {"contains middle", []string{"a", "b", "c"}, "b", true}, - {"contains last", []string{"a", "b", "c"}, "c", true}, - {"not contains", []string{"a", "b", "c"}, "d", false}, - {"empty list", []string{}, "a", false}, + tests := []struct { + name string + model interface{} + fieldPtr interface{} + expected string + expectPanic bool + }{ + { + name: "取得 Name 的 cql tag", + model: monkey, + fieldPtr: &monkey.Name, + expected: "name", + }, + { + name: "取得 ID 的 cql tag", + model: monkey, + fieldPtr: &monkey.ID, + expected: "id", + }, + { + name: "取得 UpdateAt 的 cql tag", + model: monkey, + fieldPtr: &monkey.UpdateAt, + expected: "update_at", + }, + { + name: "取得 CreateAt 的 cql tag", + model: monkey, + fieldPtr: &monkey.CreateAt, + expected: "create_at", + }, + { + name: "找不到對應欄位,回傳空字串", + model: monkey, + fieldPtr: new(int), // 傳入與 MonkeyEntity 無關的欄位指標 + expected: "", + }, + { + name: "非指向 struct 的 model,應該 panic", + model: MonkeyEntity{}, // 非指針 + fieldPtr: &monkey.Name, + expected: "", + }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actual := contains(tc.list, tc.target) - if actual != tc.expected { - t.Errorf("contains(%v, %q) = %v; want %v", tc.list, tc.target, actual, tc.expected) + for _, tt := range tests { + tt := tt // 捕捉迴圈變數 + t.Run(tt.name, func(t *testing.T) { + // 如果預期會 panic,則用 recover 進行驗證 + if tt.expectPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("預期測試案例 %q 發生 panic,但實際並未 panic", tt.name) + } + }() + _ = GetCqlTag(tt.model, tt.fieldPtr) + } else { + result := GetCqlTag(tt.model, tt.fieldPtr) + if result != tt.expected { + t.Errorf("測試案例 %q: 預期 %q, 但得到 %q", tt.name, tt.expected, result) + } } }) } } +// -------------------- 測試函式 -------------------- + // TestToSnakeCase 測試 toSnakeCase 函式 func TestToSnakeCase(t *testing.T) { testCases := []struct { @@ -83,3 +138,29 @@ func TestIsZero(t *testing.T) { }) } } + +func TestContains(t *testing.T) { + type testCase struct { + name string + list []string + target string + expected bool + } + + tests := []testCase{ + {"contains first", []string{"a", "b", "c"}, "a", true}, + {"contains middle", []string{"a", "b", "c"}, "b", true}, + {"contains last", []string{"a", "b", "c"}, "c", true}, + {"not contains", []string{"a", "b", "c"}, "d", false}, + {"empty list", []string{}, "a", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := contains(tc.list, tc.target) + if actual != tc.expected { + t.Errorf("contains(%v, %q) = %v; want %v", tc.list, tc.target, actual, tc.expected) + } + }) + } +} diff --git a/internal/repository/data_source_binance.go b/internal/repository/data_source_binance.go index 1926c10..2326e27 100644 --- a/internal/repository/data_source_binance.go +++ b/internal/repository/data_source_binance.go @@ -32,12 +32,12 @@ import ( type BinanceRepositoryParam struct { Conf *config.Binance Redis *redis.Redis - DB *cassandra.DB + DB *cassandra.CassandraDB } type BinanceRepository struct { Client *binance.Client - db *cassandra.DB + db *cassandra.CassandraDB rds *redis.Redis barrier syncx.SingleFlight workers *ants.Pool @@ -173,8 +173,14 @@ func (repo *BinanceRepository) FetchHistoryKline(ctx context.Context, param repo } func (repo *BinanceRepository) SaveHistoryKline(ctx context.Context, data []*entity.Kline) error { + for _, item := range data { + err := repo.db.Insert(ctx, item, repo.KeySpace) + if err != nil { + logx.Errorf("failed to insert data: %v", item) + } + } - //repo.db.Insert(ctx, repo.KeySpace) + return nil } // ============= diff --git a/internal/svc/cassandra.go b/internal/svc/cassandra.go index bd060c5..e3b2d57 100644 --- a/internal/svc/cassandra.go +++ b/internal/svc/cassandra.go @@ -6,7 +6,7 @@ import ( ) // NewDB 傳入 config 返回 CassandraDB -func NewDB(cfg config.Config) (*cassandra.DB, error) { +func NewDB(cfg config.Config) (*cassandra.CassandraDB, error) { var opts []cassandra.Option // 必填欄位 @@ -18,7 +18,7 @@ func NewDB(cfg config.Config) (*cassandra.DB, error) { opts = append(opts, cassandra.WithConnectTimeoutSec(cfg.Cassandra.ConnectTimeoutSec)) } if cfg.Cassandra.NumConns > 0 { - opts = append(opts, cassandra.WithNumConnects(cfg.Cassandra.NumConns)) + opts = append(opts, cassandra.WithNumConns(cfg.Cassandra.NumConns)) } if cfg.Cassandra.MaxRetries > 0 { opts = append(opts, cassandra.WithMaxRetries(cfg.Cassandra.MaxRetries)) @@ -41,5 +41,5 @@ func NewDB(cfg config.Config) (*cassandra.DB, error) { opts = append(opts, cassandra.WithAuth(cfg.Cassandra.Username, cfg.Cassandra.Password)) } - return cassandra.NewDB(cfg.Cassandra.Hosts, opts...) + return cassandra.NewCassandraDB(cfg.Cassandra.Hosts, opts...) } diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 460de33..0d8b39a 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -4,7 +4,6 @@ import ( "blockchain/internal/config" "blockchain/internal/domain/repository" "blockchain/internal/domain/usecase" - "blockchain/internal/lib/cassandra" repo "blockchain/internal/repository" uc "blockchain/internal/usecase" @@ -22,16 +21,18 @@ func NewServiceContext(c config.Config) *ServiceContext { if err != nil { panic(err) } - binanceRepo := repo.MustBinanceRepository(repo.BinanceRepositoryParam{ - Conf: &c.Binance, - Redis: newRedis, - }) cassandra, err := NewDB(c) if err != nil { return nil } + binanceRepo := repo.MustBinanceRepository(repo.BinanceRepositoryParam{ + Conf: &c.Binance, + Redis: newRedis, + DB: cassandra, + }) + return &ServiceContext{ Config: c, BinanceRepo: binanceRepo,