forked from mirrors/gotosocial
Compare commits
No commits in common. "120510ccbfd32f2c2981f28e8d08ed318b371f30" and "2be83fdca5c440d45b8cd92bda9315757463d6c7" have entirely different histories.
120510ccbf
...
2be83fdca5
73 changed files with 2887 additions and 3130 deletions
|
@ -224,7 +224,6 @@ The following open source libraries, frameworks, and tools are used by GoToSocia
|
|||
- [buckket/go-blurhash](https://github.com/buckket/go-blurhash); used for generating image blurhashes. [GPL-3.0 License](https://spdx.org/licenses/GPL-3.0-only.html).
|
||||
- [coreos/go-oidc](https://github.com/coreos/go-oidc); OIDC client library. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html).
|
||||
- [disintegration/imaging](https://github.com/disintegration/imaging); image resizing. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- [DmitriyVTitov/size](https://github.com/DmitriyVTitov/size); runtime model memory size calculations. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- Gin:
|
||||
- [gin-contrib/cors](https://github.com/gin-contrib/cors); Gin CORS middleware. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- [gin-contrib/gzip](https://github.com/gin-contrib/gzip); Gin gzip middleware. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
|
|
|
@ -75,14 +75,14 @@ func setupPrune(ctx context.Context) (*prune, error) {
|
|||
}
|
||||
|
||||
func (p *prune) shutdown(ctx context.Context) error {
|
||||
errs := gtserror.NewMultiError(2)
|
||||
var errs gtserror.MultiError
|
||||
|
||||
if err := p.storage.Close(); err != nil {
|
||||
errs.Appendf("error closing storage backend: %w", err)
|
||||
errs.Appendf("error closing storage backend: %v", err)
|
||||
}
|
||||
|
||||
if err := p.dbService.Stop(ctx); err != nil {
|
||||
errs.Appendf("error stopping database: %w", err)
|
||||
errs.Appendf("error stopping database: %v", err)
|
||||
}
|
||||
|
||||
p.state.Workers.Stop()
|
||||
|
|
|
@ -25,9 +25,7 @@ import (
|
|||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-sched"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/cmd/gotosocial/action"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
|
@ -119,13 +117,6 @@ var Start action.GTSAction = func(ctx context.Context) error {
|
|||
state.Workers.Start()
|
||||
defer state.Workers.Stop()
|
||||
|
||||
// Add a task to the scheduler to sweep caches.
|
||||
// Frequency = 1 * minute
|
||||
// Threshold = 80% capacity
|
||||
sweep := func(time.Time) { state.Caches.Sweep(80) }
|
||||
job := sched.NewJob(sweep).Every(time.Minute)
|
||||
_ = state.Workers.Scheduler.Schedule(job)
|
||||
|
||||
// Build handlers used in later initializations.
|
||||
mediaManager := media.NewManager(&state)
|
||||
oauthServer := oauth.New(ctx, dbService)
|
||||
|
|
|
@ -231,13 +231,111 @@ db-sqlite-cache-size: "8MiB"
|
|||
db-sqlite-busy-timeout: "30m"
|
||||
|
||||
cache:
|
||||
# cache.memory-target sets a target limit that
|
||||
# the application will try to keep it's caches
|
||||
# within. This is based on estimated sizes of
|
||||
# in-memory objects, and so NOT AT ALL EXACT.
|
||||
# Examples: ["100MiB", "200MiB", "500MiB", "1GiB"]
|
||||
# Default: "200MiB"
|
||||
memory-target: "200MiB"
|
||||
# Cache configuration options:
|
||||
#
|
||||
# max-size = maximum cached objects count
|
||||
# ttl = cached object lifetime
|
||||
# sweep-freq = frequency to look for stale cache objects
|
||||
# (zero will disable cache sweeping)
|
||||
|
||||
#############################
|
||||
#### VISIBILITY CACHES ######
|
||||
#############################
|
||||
#
|
||||
# Configure Status and account
|
||||
# visibility cache.
|
||||
|
||||
visibility-max-size: 2000
|
||||
visibility-ttl: "30m"
|
||||
visibility-sweep-freq: "1m"
|
||||
|
||||
gts:
|
||||
###########################
|
||||
#### DATABASE CACHES ######
|
||||
###########################
|
||||
#
|
||||
# Configure GTS database
|
||||
# model caches.
|
||||
|
||||
account-max-size: 2000
|
||||
account-ttl: "30m"
|
||||
account-sweep-freq: "1m"
|
||||
|
||||
block-max-size: 1000
|
||||
block-ttl: "30m"
|
||||
block-sweep-freq: "1m"
|
||||
|
||||
domain-block-max-size: 2000
|
||||
domain-block-ttl: "24h"
|
||||
domain-block-sweep-freq: "1m"
|
||||
|
||||
emoji-max-size: 2000
|
||||
emoji-ttl: "30m"
|
||||
emoji-sweep-freq: "1m"
|
||||
|
||||
emoji-category-max-size: 100
|
||||
emoji-category-ttl: "30m"
|
||||
emoji-category-sweep-freq: "1m"
|
||||
|
||||
follow-max-size: 2000
|
||||
follow-ttl: "30m"
|
||||
follow-sweep-freq: "1m"
|
||||
|
||||
follow-request-max-size: 2000
|
||||
follow-request-ttl: "30m"
|
||||
follow-request-sweep-freq: "1m"
|
||||
|
||||
instance-max-size: 2000
|
||||
instance-ttl: "30m"
|
||||
instance-sweep-freq: "1m"
|
||||
|
||||
list-max-size: 2000
|
||||
list-ttl: "30m"
|
||||
list-sweep-freq: "1m"
|
||||
|
||||
list-entry-max-size: 2000
|
||||
list-entry-ttl: "30m"
|
||||
list-entry-sweep-freq: "1m"
|
||||
|
||||
media-max-size: 1000
|
||||
media-ttl: "30m"
|
||||
media-sweep-freq: "1m"
|
||||
|
||||
mention-max-size: 2000
|
||||
mention-ttl: "30m"
|
||||
mention-sweep-freq: "1m"
|
||||
|
||||
notification-max-size: 1000
|
||||
notification-ttl: "30m"
|
||||
notification-sweep-freq: "1m"
|
||||
|
||||
report-max-size: 100
|
||||
report-ttl: "30m"
|
||||
report-sweep-freq: "1m"
|
||||
|
||||
status-max-size: 2000
|
||||
status-ttl: "30m"
|
||||
status-sweep-freq: "1m"
|
||||
|
||||
status-fave-max-size: 2000
|
||||
status-fave-ttl: "30m"
|
||||
status-fave-sweep-freq: "1m"
|
||||
|
||||
tag-max-size: 2000
|
||||
tag-ttl: "30m"
|
||||
tag-sweep-freq: "1m"
|
||||
|
||||
tombstone-max-size: 500
|
||||
tombstone-ttl: "30m"
|
||||
tombstone-sweep-freq: "1m"
|
||||
|
||||
user-max-size: 500
|
||||
user-ttl: "30m"
|
||||
user-sweep-freq: "1m"
|
||||
|
||||
webfinger-max-size: 250
|
||||
webfinger-ttl: "24h"
|
||||
webfinger-sweep-freq: "15m"
|
||||
|
||||
######################
|
||||
##### WEB CONFIG #####
|
||||
|
|
3
go.mod
3
go.mod
|
@ -5,7 +5,7 @@ go 1.20
|
|||
require (
|
||||
codeberg.org/gruf/go-bytesize v1.0.2
|
||||
codeberg.org/gruf/go-byteutil v1.1.2
|
||||
codeberg.org/gruf/go-cache/v3 v3.5.5
|
||||
codeberg.org/gruf/go-cache/v3 v3.4.4
|
||||
codeberg.org/gruf/go-debug v1.3.0
|
||||
codeberg.org/gruf/go-errors/v2 v2.2.0
|
||||
codeberg.org/gruf/go-fastcopy v1.1.2
|
||||
|
@ -16,7 +16,6 @@ require (
|
|||
codeberg.org/gruf/go-runners v1.6.1
|
||||
codeberg.org/gruf/go-sched v1.2.3
|
||||
codeberg.org/gruf/go-store/v2 v2.2.2
|
||||
github.com/DmitriyVTitov/size v1.5.0
|
||||
github.com/KimMachineGun/automemlimit v0.2.6
|
||||
github.com/abema/go-mp4 v0.11.0
|
||||
github.com/buckket/go-blurhash v1.1.0
|
||||
|
|
8
go.sum
8
go.sum
|
@ -48,8 +48,8 @@ codeberg.org/gruf/go-bytesize v1.0.2/go.mod h1:n/GU8HzL9f3UNp/mUKyr1qVmTlj7+xacp
|
|||
codeberg.org/gruf/go-byteutil v1.0.0/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU=
|
||||
codeberg.org/gruf/go-byteutil v1.1.2 h1:TQLZtTxTNca9xEfDIndmo7nBYxeS94nrv/9DS3Nk5Tw=
|
||||
codeberg.org/gruf/go-byteutil v1.1.2/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU=
|
||||
codeberg.org/gruf/go-cache/v3 v3.5.5 h1:Ce7odyvr8oF6h49LSjPL7AZs2QGyKMN9BPkgKcfR0BA=
|
||||
codeberg.org/gruf/go-cache/v3 v3.5.5/go.mod h1:NbsGQUgEdNFd631WSasvCHIVAaY9ovuiSeoBwtsIeDc=
|
||||
codeberg.org/gruf/go-cache/v3 v3.4.4 h1:V0A3EzjhzhULOydD16pwa2DRDwF67OuuP4ORnm//7p8=
|
||||
codeberg.org/gruf/go-cache/v3 v3.4.4/go.mod h1:pTeVPEb9DshXUkd8Dg76UcsLpU6EC/tXQ2qb+JrmxEc=
|
||||
codeberg.org/gruf/go-debug v1.3.0 h1:PIRxQiWUFKtGOGZFdZ3Y0pqyfI0Xr87j224IYe2snZs=
|
||||
codeberg.org/gruf/go-debug v1.3.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg=
|
||||
codeberg.org/gruf/go-errors/v2 v2.0.0/go.mod h1:ZRhbdhvgoUA3Yw6e56kd9Ox984RrvbEFC2pOXyHDJP4=
|
||||
|
@ -87,8 +87,6 @@ codeberg.org/gruf/go-store/v2 v2.2.2/go.mod h1:QRM3LUAfYyoGMWLTqA1WzohxQgYqPFiVv
|
|||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g=
|
||||
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
|
||||
github.com/KimMachineGun/automemlimit v0.2.6 h1:tQFriVTcIteUkV5EgU9iz03eDY36T8JU5RAjP2r6Kt0=
|
||||
github.com/KimMachineGun/automemlimit v0.2.6/go.mod h1:pJhTW/nWJMj6SnWSU2TEKSlCaM+1N5Mej+IfS/5/Ol0=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
|
@ -258,8 +256,6 @@ github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
|
|||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -104,16 +105,16 @@ func (suite *InboxPostTestSuite) inboxPost(
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// Check expected code + body.
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// If we got an expected body, return early.
|
||||
if expectedBody != "" && string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
|
|
|
@ -90,16 +90,16 @@ func (suite *AccountUpdateTestSuite) updateAccount(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// Check expected code + body.
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// If we got an expected body, return early.
|
||||
if expectedBody != "" && string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
|
|
|
@ -19,6 +19,7 @@ package accounts_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -62,16 +63,16 @@ func (suite *ListsTestSuite) getLists(targetAccountID string, expectedHTTPStatus
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// Check expected code + body.
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// If we got an expected body, return early.
|
||||
if expectedBody != "" && string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
|
|
|
@ -19,6 +19,7 @@ package accounts_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -98,16 +99,16 @@ func (suite *AccountSearchTestSuite) getSearch(
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// Check expected code + body.
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// If we got an expected body, return early.
|
||||
if expectedBody != "" && string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
|
|
|
@ -19,6 +19,7 @@ package admin_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -83,16 +84,16 @@ func (suite *ReportResolveTestSuite) resolveReport(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package admin_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -100,16 +101,16 @@ func (suite *ReportsGetTestSuite) getReports(
|
|||
return nil, "", err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
return nil, "", errs.Combine()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package lists_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -102,17 +103,17 @@ func (suite *ListAccountsTestSuite) getListAccounts(
|
|||
return nil, "", err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
return nil, "", errs.Combine()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package reports_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -76,17 +77,17 @@ func (suite *ReportCreateTestSuite) createReport(expectedHTTPStatus int, expecte
|
|||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package reports_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -63,17 +64,17 @@ func (suite *ReportGetTestSuite) getReport(expectedHTTPStatus int, expectedBody
|
|||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
|
|
@ -19,9 +19,8 @@ package search_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -31,7 +30,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/search"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
|
@ -121,16 +119,16 @@ func (suite *SearchGetTestSuite) getSearch(
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// Check expected code + body.
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d: %v", expectedHTTPStatus, resultCode, ctx.Errors.JSON()))
|
||||
}
|
||||
|
||||
// If we got an expected body, return early.
|
||||
if expectedBody != "" && string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
|
@ -1003,7 +1001,7 @@ func (suite *SearchGetTestSuite) TestSearchAAccounts() {
|
|||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
func (suite *SearchGetTestSuite) TestSearchAccountsLimit1() {
|
||||
func (suite *SearchGetTestSuite) TestSearchAAccountsLimit1() {
|
||||
var (
|
||||
requestingAccount = suite.testAccounts["local_account_1"]
|
||||
token = suite.testTokens["local_account_1"]
|
||||
|
@ -1080,14 +1078,12 @@ func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountByURI() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Should be able to get instance
|
||||
// account by exact URI.
|
||||
suite.Len(searchResult.Accounts, 1)
|
||||
suite.Len(searchResult.Accounts, 0)
|
||||
suite.Len(searchResult.Statuses, 0)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountFull() {
|
||||
func (suite *SearchGetTestSuite) TestSearchInstanceAccountFull() {
|
||||
// Namestring excludes ':' in usernames, so we
|
||||
// need to fiddle with the instance account a
|
||||
// bit to get it to look like a different domain.
|
||||
|
@ -1129,14 +1125,12 @@ func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountFull() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Should be able to get instance
|
||||
// account by full namestring.
|
||||
suite.Len(searchResult.Accounts, 1)
|
||||
suite.Len(searchResult.Accounts, 0)
|
||||
suite.Len(searchResult.Statuses, 0)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountPartial() {
|
||||
func (suite *SearchGetTestSuite) TestSearchInstanceAccountPartial() {
|
||||
// Namestring excludes ':' in usernames, so we
|
||||
// need to fiddle with the instance account a
|
||||
// bit to get it to look like a different domain.
|
||||
|
@ -1178,131 +1172,6 @@ func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountPartial() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Query was a partial namestring from our
|
||||
// instance, so will return the instance account.
|
||||
suite.Len(searchResult.Accounts, 1)
|
||||
suite.Len(searchResult.Statuses, 0)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountEvenMorePartial() {
|
||||
// Namestring excludes ':' in usernames, so we
|
||||
// need to fiddle with the instance account a
|
||||
// bit to get it to look like a different domain.
|
||||
newDomain := "example.org"
|
||||
suite.bodgeLocalInstance(newDomain)
|
||||
|
||||
var (
|
||||
requestingAccount = suite.testAccounts["local_account_1"]
|
||||
token = suite.testTokens["local_account_1"]
|
||||
user = suite.testUsers["local_account_1"]
|
||||
maxID *string = nil
|
||||
minID *string = nil
|
||||
limit *int = nil
|
||||
offset *int = nil
|
||||
resolve *bool = nil
|
||||
query = newDomain
|
||||
queryType *string = nil
|
||||
following *bool = nil
|
||||
expectedHTTPStatus = http.StatusOK
|
||||
expectedBody = ""
|
||||
)
|
||||
|
||||
searchResult, err := suite.getSearch(
|
||||
requestingAccount,
|
||||
token,
|
||||
apiutil.APIv2,
|
||||
user,
|
||||
maxID,
|
||||
minID,
|
||||
limit,
|
||||
offset,
|
||||
query,
|
||||
queryType,
|
||||
resolve,
|
||||
following,
|
||||
expectedHTTPStatus,
|
||||
expectedBody)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Query was just 'example.org' which doesn't
|
||||
// look like a namestring, so search should
|
||||
// fall back to text search and therefore give
|
||||
// 0 results back.
|
||||
suite.Len(searchResult.Accounts, 0)
|
||||
suite.Len(searchResult.Statuses, 0)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
func (suite *SearchGetTestSuite) TestSearchRemoteInstanceAccountPartial() {
|
||||
// Insert an instance account that's not
|
||||
// from our instance, and try to search
|
||||
// for it with a partial namestring.
|
||||
theirDomain := "example.org"
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
if err := suite.db.PutAccount(context.Background(), >smodel.Account{
|
||||
ID: "01H6RWPG8T6DNW6VNXPBCJBH5S",
|
||||
Username: theirDomain,
|
||||
Domain: theirDomain,
|
||||
URI: "http://" + theirDomain + "/users/" + theirDomain,
|
||||
URL: "http://" + theirDomain + "/@" + theirDomain,
|
||||
PublicKeyURI: "http://" + theirDomain + "/users/" + theirDomain + "#main-key",
|
||||
InboxURI: "http://" + theirDomain + "/users/" + theirDomain + "/inbox",
|
||||
OutboxURI: "http://" + theirDomain + "/users/" + theirDomain + "/outbox",
|
||||
FollowersURI: "http://" + theirDomain + "/users/" + theirDomain + "/followers",
|
||||
FollowingURI: "http://" + theirDomain + "/users/" + theirDomain + "/following",
|
||||
FeaturedCollectionURI: "http://" + theirDomain + "/users/" + theirDomain + "/collections/featured",
|
||||
ActorType: ap.ActorPerson,
|
||||
PrivateKey: key,
|
||||
PublicKey: &key.PublicKey,
|
||||
}); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
var (
|
||||
requestingAccount = suite.testAccounts["local_account_1"]
|
||||
token = suite.testTokens["local_account_1"]
|
||||
user = suite.testUsers["local_account_1"]
|
||||
maxID *string = nil
|
||||
minID *string = nil
|
||||
limit *int = nil
|
||||
offset *int = nil
|
||||
resolve *bool = nil
|
||||
query = "@" + theirDomain
|
||||
queryType *string = nil
|
||||
following *bool = nil
|
||||
expectedHTTPStatus = http.StatusOK
|
||||
expectedBody = ""
|
||||
)
|
||||
|
||||
searchResult, err := suite.getSearch(
|
||||
requestingAccount,
|
||||
token,
|
||||
apiutil.APIv2,
|
||||
user,
|
||||
maxID,
|
||||
minID,
|
||||
limit,
|
||||
offset,
|
||||
query,
|
||||
queryType,
|
||||
resolve,
|
||||
following,
|
||||
expectedHTTPStatus,
|
||||
expectedBody)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Search for instance account from
|
||||
// another domain should return 0 results.
|
||||
suite.Len(searchResult.Accounts, 0)
|
||||
suite.Len(searchResult.Statuses, 0)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
|
|
|
@ -20,6 +20,7 @@ package statuses_test
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -73,20 +74,20 @@ func (suite *StatusPinTestSuite) createPin(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// Check expected code + body.
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// If we got an expected body, return early.
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" && string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
suite.FailNow("", "%v (body %s)", err, string(b))
|
||||
if len(errs) > 0 {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.Status{}
|
||||
|
|
|
@ -19,6 +19,7 @@ package statuses_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -67,20 +68,20 @@ func (suite *StatusUnpinTestSuite) createUnpin(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
errs := gtserror.MultiError{}
|
||||
|
||||
// Check expected code + body.
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
|
||||
}
|
||||
|
||||
// If we got an expected body, return early.
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" && string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
suite.FailNow("", "%v (body %s)", err, string(b))
|
||||
if len(errs) > 0 {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.Status{}
|
||||
|
|
48
internal/cache/cache.go
vendored
48
internal/cache/cache.go
vendored
|
@ -196,21 +196,6 @@ func (c *Caches) setuphooks() {
|
|||
// c.GTS.Media().Invalidate("StatusID") will not work.
|
||||
c.GTS.Media().Invalidate("ID", id)
|
||||
}
|
||||
|
||||
if status.BoostOfID != "" {
|
||||
// Invalidate boost ID list of the original status.
|
||||
c.GTS.BoostOfIDs().Invalidate(status.BoostOfID)
|
||||
}
|
||||
|
||||
if status.InReplyToID != "" {
|
||||
// Invalidate in reply to ID list of original status.
|
||||
c.GTS.InReplyToIDs().Invalidate(status.InReplyToID)
|
||||
}
|
||||
})
|
||||
|
||||
c.GTS.StatusFave().SetInvalidateCallback(func(fave *gtsmodel.StatusFave) {
|
||||
// Invalidate status fave ID list for this status.
|
||||
c.GTS.StatusFaveIDs().Invalidate(fave.StatusID)
|
||||
})
|
||||
|
||||
c.GTS.User().SetInvalidateCallback(func(user *gtsmodel.User) {
|
||||
|
@ -219,36 +204,3 @@ func (c *Caches) setuphooks() {
|
|||
c.Visibility.Invalidate("RequesterID", user.AccountID)
|
||||
})
|
||||
}
|
||||
|
||||
// Sweep will sweep all the available caches to ensure none
|
||||
// are above threshold percent full to their total capacity.
|
||||
//
|
||||
// This helps with cache performance, as a full cache will
|
||||
// require an eviction on every single write, which adds
|
||||
// significant overhead to all cache writes.
|
||||
func (c *Caches) Sweep(threshold float64) {
|
||||
c.GTS.Account().Trim(threshold)
|
||||
c.GTS.AccountNote().Trim(threshold)
|
||||
c.GTS.Block().Trim(threshold)
|
||||
c.GTS.BlockIDs().Trim(threshold)
|
||||
c.GTS.Emoji().Trim(threshold)
|
||||
c.GTS.EmojiCategory().Trim(threshold)
|
||||
c.GTS.Follow().Trim(threshold)
|
||||
c.GTS.FollowIDs().Trim(threshold)
|
||||
c.GTS.FollowRequest().Trim(threshold)
|
||||
c.GTS.FollowRequestIDs().Trim(threshold)
|
||||
c.GTS.Instance().Trim(threshold)
|
||||
c.GTS.List().Trim(threshold)
|
||||
c.GTS.ListEntry().Trim(threshold)
|
||||
c.GTS.Marker().Trim(threshold)
|
||||
c.GTS.Media().Trim(threshold)
|
||||
c.GTS.Mention().Trim(threshold)
|
||||
c.GTS.Notification().Trim(threshold)
|
||||
c.GTS.Report().Trim(threshold)
|
||||
c.GTS.Status().Trim(threshold)
|
||||
c.GTS.StatusFave().Trim(threshold)
|
||||
c.GTS.Tag().Trim(threshold)
|
||||
c.GTS.Tombstone().Trim(threshold)
|
||||
c.GTS.User().Trim(threshold)
|
||||
c.Visibility.Trim(threshold)
|
||||
}
|
||||
|
|
458
internal/cache/gts.go
vendored
458
internal/cache/gts.go
vendored
|
@ -18,15 +18,11 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-cache/v3/result"
|
||||
"codeberg.org/gruf/go-cache/v3/simple"
|
||||
"codeberg.org/gruf/go-cache/v3/ttl"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/cache/domain"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
type GTSCaches struct {
|
||||
|
@ -34,7 +30,6 @@ type GTSCaches struct {
|
|||
accountNote *result.Cache[*gtsmodel.AccountNote]
|
||||
block *result.Cache[*gtsmodel.Block]
|
||||
blockIDs *SliceCache[string]
|
||||
boostOfIDs *SliceCache[string]
|
||||
domainBlock *domain.BlockCache
|
||||
emoji *result.Cache[*gtsmodel.Emoji]
|
||||
emojiCategory *result.Cache[*gtsmodel.EmojiCategory]
|
||||
|
@ -43,7 +38,6 @@ type GTSCaches struct {
|
|||
followRequest *result.Cache[*gtsmodel.FollowRequest]
|
||||
followRequestIDs *SliceCache[string]
|
||||
instance *result.Cache[*gtsmodel.Instance]
|
||||
inReplyToIDs *SliceCache[string]
|
||||
list *result.Cache[*gtsmodel.List]
|
||||
listEntry *result.Cache[*gtsmodel.ListEntry]
|
||||
marker *result.Cache[*gtsmodel.Marker]
|
||||
|
@ -53,13 +47,12 @@ type GTSCaches struct {
|
|||
report *result.Cache[*gtsmodel.Report]
|
||||
status *result.Cache[*gtsmodel.Status]
|
||||
statusFave *result.Cache[*gtsmodel.StatusFave]
|
||||
statusFaveIDs *SliceCache[string]
|
||||
tag *result.Cache[*gtsmodel.Tag]
|
||||
tombstone *result.Cache[*gtsmodel.Tombstone]
|
||||
user *result.Cache[*gtsmodel.User]
|
||||
|
||||
// TODO: move out of GTS caches since unrelated to DB.
|
||||
webfinger *ttl.Cache[string, string] // TTL=24hr, sweep=5min
|
||||
webfinger *ttl.Cache[string, string]
|
||||
}
|
||||
|
||||
// Init will initialize all the gtsmodel caches in this collection.
|
||||
|
@ -69,7 +62,6 @@ func (c *GTSCaches) Init() {
|
|||
c.initAccountNote()
|
||||
c.initBlock()
|
||||
c.initBlockIDs()
|
||||
c.initBoostOfIDs()
|
||||
c.initDomainBlock()
|
||||
c.initEmoji()
|
||||
c.initEmojiCategory()
|
||||
|
@ -77,7 +69,6 @@ func (c *GTSCaches) Init() {
|
|||
c.initFollowIDs()
|
||||
c.initFollowRequest()
|
||||
c.initFollowRequestIDs()
|
||||
c.initInReplyToIDs()
|
||||
c.initInstance()
|
||||
c.initList()
|
||||
c.initListEntry()
|
||||
|
@ -89,7 +80,6 @@ func (c *GTSCaches) Init() {
|
|||
c.initStatus()
|
||||
c.initStatusFave()
|
||||
c.initTag()
|
||||
c.initStatusFaveIDs()
|
||||
c.initTombstone()
|
||||
c.initUser()
|
||||
c.initWebfinger()
|
||||
|
@ -97,14 +87,98 @@ func (c *GTSCaches) Init() {
|
|||
|
||||
// Start will attempt to start all of the gtsmodel caches, or panic.
|
||||
func (c *GTSCaches) Start() {
|
||||
tryStart(c.account, config.GetCacheGTSAccountSweepFreq())
|
||||
tryStart(c.accountNote, config.GetCacheGTSAccountNoteSweepFreq())
|
||||
tryStart(c.block, config.GetCacheGTSBlockSweepFreq())
|
||||
tryUntil("starting block IDs cache", 5, func() bool {
|
||||
if sweep := config.GetCacheGTSBlockIDsSweepFreq(); sweep > 0 {
|
||||
return c.blockIDs.Start(sweep)
|
||||
}
|
||||
return true
|
||||
})
|
||||
tryStart(c.emoji, config.GetCacheGTSEmojiSweepFreq())
|
||||
tryStart(c.emojiCategory, config.GetCacheGTSEmojiCategorySweepFreq())
|
||||
tryStart(c.follow, config.GetCacheGTSFollowSweepFreq())
|
||||
tryUntil("starting follow IDs cache", 5, func() bool {
|
||||
if sweep := config.GetCacheGTSFollowIDsSweepFreq(); sweep > 0 {
|
||||
return c.followIDs.Start(sweep)
|
||||
}
|
||||
return true
|
||||
})
|
||||
tryStart(c.followRequest, config.GetCacheGTSFollowRequestSweepFreq())
|
||||
tryUntil("starting follow request IDs cache", 5, func() bool {
|
||||
if sweep := config.GetCacheGTSFollowRequestIDsSweepFreq(); sweep > 0 {
|
||||
return c.followRequestIDs.Start(sweep)
|
||||
}
|
||||
return true
|
||||
})
|
||||
tryStart(c.instance, config.GetCacheGTSInstanceSweepFreq())
|
||||
tryStart(c.list, config.GetCacheGTSListSweepFreq())
|
||||
tryStart(c.listEntry, config.GetCacheGTSListEntrySweepFreq())
|
||||
tryStart(c.marker, config.GetCacheGTSMarkerSweepFreq())
|
||||
tryStart(c.media, config.GetCacheGTSMediaSweepFreq())
|
||||
tryStart(c.mention, config.GetCacheGTSMentionSweepFreq())
|
||||
tryStart(c.notification, config.GetCacheGTSNotificationSweepFreq())
|
||||
tryStart(c.report, config.GetCacheGTSReportSweepFreq())
|
||||
tryStart(c.status, config.GetCacheGTSStatusSweepFreq())
|
||||
tryStart(c.statusFave, config.GetCacheGTSStatusFaveSweepFreq())
|
||||
tryStart(c.tag, config.GetCacheGTSTagSweepFreq())
|
||||
tryStart(c.tombstone, config.GetCacheGTSTombstoneSweepFreq())
|
||||
tryStart(c.user, config.GetCacheGTSUserSweepFreq())
|
||||
tryUntil("starting *gtsmodel.Webfinger cache", 5, func() bool {
|
||||
return c.webfinger.Start(5 * time.Minute)
|
||||
if sweep := config.GetCacheGTSWebfingerSweepFreq(); sweep > 0 {
|
||||
return c.webfinger.Start(sweep)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Stop will attempt to stop all of the gtsmodel caches, or panic.
|
||||
func (c *GTSCaches) Stop() {
|
||||
tryUntil("stopping *gtsmodel.Webfinger cache", 5, c.webfinger.Stop)
|
||||
tryStop(c.account, config.GetCacheGTSAccountSweepFreq())
|
||||
tryStop(c.accountNote, config.GetCacheGTSAccountNoteSweepFreq())
|
||||
tryStop(c.block, config.GetCacheGTSBlockSweepFreq())
|
||||
tryUntil("stopping block IDs cache", 5, func() bool {
|
||||
if config.GetCacheGTSBlockIDsSweepFreq() > 0 {
|
||||
return c.blockIDs.Stop()
|
||||
}
|
||||
return true
|
||||
})
|
||||
tryStop(c.emoji, config.GetCacheGTSEmojiSweepFreq())
|
||||
tryStop(c.emojiCategory, config.GetCacheGTSEmojiCategorySweepFreq())
|
||||
tryStop(c.follow, config.GetCacheGTSFollowSweepFreq())
|
||||
tryUntil("stopping follow IDs cache", 5, func() bool {
|
||||
if config.GetCacheGTSFollowIDsSweepFreq() > 0 {
|
||||
return c.followIDs.Stop()
|
||||
}
|
||||
return true
|
||||
})
|
||||
tryStop(c.followRequest, config.GetCacheGTSFollowRequestSweepFreq())
|
||||
tryUntil("stopping follow request IDs cache", 5, func() bool {
|
||||
if config.GetCacheGTSFollowRequestIDsSweepFreq() > 0 {
|
||||
return c.followRequestIDs.Stop()
|
||||
}
|
||||
return true
|
||||
})
|
||||
tryStop(c.instance, config.GetCacheGTSInstanceSweepFreq())
|
||||
tryStop(c.list, config.GetCacheGTSListSweepFreq())
|
||||
tryStop(c.listEntry, config.GetCacheGTSListEntrySweepFreq())
|
||||
tryStop(c.marker, config.GetCacheGTSMarkerSweepFreq())
|
||||
tryStop(c.media, config.GetCacheGTSMediaSweepFreq())
|
||||
tryStop(c.mention, config.GetCacheGTSNotificationSweepFreq())
|
||||
tryStop(c.notification, config.GetCacheGTSNotificationSweepFreq())
|
||||
tryStop(c.report, config.GetCacheGTSReportSweepFreq())
|
||||
tryStop(c.status, config.GetCacheGTSStatusSweepFreq())
|
||||
tryStop(c.statusFave, config.GetCacheGTSStatusFaveSweepFreq())
|
||||
tryStop(c.tag, config.GetCacheGTSTagSweepFreq())
|
||||
tryStop(c.tombstone, config.GetCacheGTSTombstoneSweepFreq())
|
||||
tryStop(c.user, config.GetCacheGTSUserSweepFreq())
|
||||
tryUntil("stopping *gtsmodel.Webfinger cache", 5, func() bool {
|
||||
if config.GetCacheGTSWebfingerSweepFreq() > 0 {
|
||||
return c.webfinger.Stop()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Account provides access to the gtsmodel Account database cache.
|
||||
|
@ -127,11 +201,6 @@ func (c *GTSCaches) BlockIDs() *SliceCache[string] {
|
|||
return c.blockIDs
|
||||
}
|
||||
|
||||
// BoostOfIDs provides access to the boost of IDs list database cache.
|
||||
func (c *GTSCaches) BoostOfIDs() *SliceCache[string] {
|
||||
return c.boostOfIDs
|
||||
}
|
||||
|
||||
// DomainBlock provides access to the domain block database cache.
|
||||
func (c *GTSCaches) DomainBlock() *domain.BlockCache {
|
||||
return c.domainBlock
|
||||
|
@ -180,11 +249,6 @@ func (c *GTSCaches) Instance() *result.Cache[*gtsmodel.Instance] {
|
|||
return c.instance
|
||||
}
|
||||
|
||||
// InReplyToIDs provides access to the status in reply to IDs list database cache.
|
||||
func (c *GTSCaches) InReplyToIDs() *SliceCache[string] {
|
||||
return c.inReplyToIDs
|
||||
}
|
||||
|
||||
// List provides access to the gtsmodel List database cache.
|
||||
func (c *GTSCaches) List() *result.Cache[*gtsmodel.List] {
|
||||
return c.list
|
||||
|
@ -235,11 +299,6 @@ func (c *GTSCaches) Tag() *result.Cache[*gtsmodel.Tag] {
|
|||
return c.tag
|
||||
}
|
||||
|
||||
// StatusFaveIDs provides access to the status fave IDs list database cache.
|
||||
func (c *GTSCaches) StatusFaveIDs() *SliceCache[string] {
|
||||
return c.statusFaveIDs
|
||||
}
|
||||
|
||||
// Tombstone provides access to the gtsmodel Tombstone database cache.
|
||||
func (c *GTSCaches) Tombstone() *result.Cache[*gtsmodel.Tombstone] {
|
||||
return c.tombstone
|
||||
|
@ -256,19 +315,11 @@ func (c *GTSCaches) Webfinger() *ttl.Cache[string, string] {
|
|||
}
|
||||
|
||||
func (c *GTSCaches) initAccount() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofAccount(), // model in-mem size.
|
||||
config.GetCacheAccountMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Account cache size = %d", cap)
|
||||
|
||||
c.account = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "URI"},
|
||||
{Name: "URL"},
|
||||
{Name: "Username.Domain", AllowZero: true /* domain can be zero i.e. "" */},
|
||||
{Name: "Username.Domain"},
|
||||
{Name: "PublicKeyURI"},
|
||||
{Name: "InboxURI"},
|
||||
{Name: "OutboxURI"},
|
||||
|
@ -278,19 +329,12 @@ func (c *GTSCaches) initAccount() {
|
|||
a2 := new(gtsmodel.Account)
|
||||
*a2 = *a1
|
||||
return a2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSAccountMaxSize())
|
||||
c.account.SetTTL(config.GetCacheGTSAccountTTL(), true)
|
||||
c.account.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initAccountNote() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofAccountNote(), // model in-mem size.
|
||||
config.GetCacheAccountNoteMemRatio(),
|
||||
)
|
||||
log.Infof(nil, "AccountNote cache size = %d", cap)
|
||||
|
||||
c.accountNote = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "AccountID.TargetAccountID"},
|
||||
|
@ -298,20 +342,12 @@ func (c *GTSCaches) initAccountNote() {
|
|||
n2 := new(gtsmodel.AccountNote)
|
||||
*n2 = *n1
|
||||
return n2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSAccountNoteMaxSize())
|
||||
c.accountNote.SetTTL(config.GetCacheGTSAccountNoteTTL(), true)
|
||||
c.accountNote.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initBlock() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofBlock(), // model in-mem size.
|
||||
config.GetCacheBlockMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Block cache size = %d", cap)
|
||||
|
||||
c.block = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "URI"},
|
||||
|
@ -322,36 +358,16 @@ func (c *GTSCaches) initBlock() {
|
|||
b2 := new(gtsmodel.Block)
|
||||
*b2 = *b1
|
||||
return b2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSBlockMaxSize())
|
||||
c.block.SetTTL(config.GetCacheGTSBlockTTL(), true)
|
||||
c.block.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initBlockIDs() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateSliceCacheMax(
|
||||
config.GetCacheBlockIDsMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Block IDs cache size = %d", cap)
|
||||
|
||||
c.blockIDs = &SliceCache[string]{Cache: simple.New[string, []string](
|
||||
c.blockIDs = &SliceCache[string]{Cache: ttl.New[string, []string](
|
||||
0,
|
||||
cap,
|
||||
)}
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initBoostOfIDs() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateSliceCacheMax(
|
||||
config.GetCacheBoostOfIDsMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "BoostofIDs cache size = %d", cap)
|
||||
|
||||
c.boostOfIDs = &SliceCache[string]{Cache: simple.New[string, []string](
|
||||
0,
|
||||
cap,
|
||||
config.GetCacheGTSBlockIDsMaxSize(),
|
||||
config.GetCacheGTSBlockIDsTTL(),
|
||||
)}
|
||||
}
|
||||
|
||||
|
@ -360,38 +376,22 @@ func (c *GTSCaches) initDomainBlock() {
|
|||
}
|
||||
|
||||
func (c *GTSCaches) initEmoji() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofEmoji(), // model in-mem size.
|
||||
config.GetCacheEmojiMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Emoji cache size = %d", cap)
|
||||
|
||||
c.emoji = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "URI"},
|
||||
{Name: "Shortcode.Domain", AllowZero: true /* domain can be zero i.e. "" */},
|
||||
{Name: "Shortcode.Domain"},
|
||||
{Name: "ImageStaticURL"},
|
||||
{Name: "CategoryID", Multi: true},
|
||||
}, func(e1 *gtsmodel.Emoji) *gtsmodel.Emoji {
|
||||
e2 := new(gtsmodel.Emoji)
|
||||
*e2 = *e1
|
||||
return e2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSEmojiMaxSize())
|
||||
c.emoji.SetTTL(config.GetCacheGTSEmojiTTL(), true)
|
||||
c.emoji.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initEmojiCategory() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofEmojiCategory(), // model in-mem size.
|
||||
config.GetCacheEmojiCategoryMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "EmojiCategory cache size = %d", cap)
|
||||
|
||||
c.emojiCategory = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "Name"},
|
||||
|
@ -399,20 +399,12 @@ func (c *GTSCaches) initEmojiCategory() {
|
|||
c2 := new(gtsmodel.EmojiCategory)
|
||||
*c2 = *c1
|
||||
return c2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSEmojiCategoryMaxSize())
|
||||
c.emojiCategory.SetTTL(config.GetCacheGTSEmojiCategoryTTL(), true)
|
||||
c.emojiCategory.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initFollow() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofFollow(), // model in-mem size.
|
||||
config.GetCacheFollowMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Follow cache size = %d", cap)
|
||||
|
||||
c.follow = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "URI"},
|
||||
|
@ -423,34 +415,19 @@ func (c *GTSCaches) initFollow() {
|
|||
f2 := new(gtsmodel.Follow)
|
||||
*f2 = *f1
|
||||
return f2
|
||||
}, cap)
|
||||
|
||||
c.follow.IgnoreErrors(ignoreErrors)
|
||||
}, config.GetCacheGTSFollowMaxSize())
|
||||
c.follow.SetTTL(config.GetCacheGTSFollowTTL(), true)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initFollowIDs() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateSliceCacheMax(
|
||||
config.GetCacheFollowIDsMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Follow IDs cache size = %d", cap)
|
||||
|
||||
c.followIDs = &SliceCache[string]{Cache: simple.New[string, []string](
|
||||
c.followIDs = &SliceCache[string]{Cache: ttl.New[string, []string](
|
||||
0,
|
||||
cap,
|
||||
config.GetCacheGTSFollowIDsMaxSize(),
|
||||
config.GetCacheGTSFollowIDsTTL(),
|
||||
)}
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initFollowRequest() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofFollowRequest(), // model in-mem size.
|
||||
config.GetCacheFollowRequestMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "FollowRequest cache size = %d", cap)
|
||||
|
||||
c.followRequest = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "URI"},
|
||||
|
@ -461,48 +438,19 @@ func (c *GTSCaches) initFollowRequest() {
|
|||
f2 := new(gtsmodel.FollowRequest)
|
||||
*f2 = *f1
|
||||
return f2
|
||||
}, cap)
|
||||
|
||||
c.followRequest.IgnoreErrors(ignoreErrors)
|
||||
}, config.GetCacheGTSFollowRequestMaxSize())
|
||||
c.followRequest.SetTTL(config.GetCacheGTSFollowRequestTTL(), true)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initFollowRequestIDs() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateSliceCacheMax(
|
||||
config.GetCacheFollowRequestIDsMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Follow Request IDs cache size = %d", cap)
|
||||
|
||||
c.followRequestIDs = &SliceCache[string]{Cache: simple.New[string, []string](
|
||||
c.followRequestIDs = &SliceCache[string]{Cache: ttl.New[string, []string](
|
||||
0,
|
||||
cap,
|
||||
)}
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initInReplyToIDs() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateSliceCacheMax(
|
||||
config.GetCacheInReplyToIDsMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "InReplyTo IDs cache size = %d", cap)
|
||||
|
||||
c.inReplyToIDs = &SliceCache[string]{Cache: simple.New[string, []string](
|
||||
0,
|
||||
cap,
|
||||
config.GetCacheGTSFollowRequestIDsMaxSize(),
|
||||
config.GetCacheGTSFollowRequestIDsTTL(),
|
||||
)}
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initInstance() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofInstance(), // model in-mem size.
|
||||
config.GetCacheInstanceMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Instance cache size = %d", cap)
|
||||
|
||||
c.instance = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "Domain"},
|
||||
|
@ -510,40 +458,24 @@ func (c *GTSCaches) initInstance() {
|
|||
i2 := new(gtsmodel.Instance)
|
||||
*i2 = *i1
|
||||
return i1
|
||||
}, cap)
|
||||
|
||||
c.instance.IgnoreErrors(ignoreErrors)
|
||||
}, config.GetCacheGTSInstanceMaxSize())
|
||||
c.instance.SetTTL(config.GetCacheGTSInstanceTTL(), true)
|
||||
c.emojiCategory.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initList() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofList(), // model in-mem size.
|
||||
config.GetCacheListMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "List cache size = %d", cap)
|
||||
|
||||
c.list = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
}, func(l1 *gtsmodel.List) *gtsmodel.List {
|
||||
l2 := new(gtsmodel.List)
|
||||
*l2 = *l1
|
||||
return l2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSListMaxSize())
|
||||
c.list.SetTTL(config.GetCacheGTSListTTL(), true)
|
||||
c.list.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initListEntry() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofListEntry(), // model in-mem size.
|
||||
config.GetCacheListEntryMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "ListEntry cache size = %d", cap)
|
||||
|
||||
c.listEntry = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "ListID", Multi: true},
|
||||
|
@ -552,80 +484,48 @@ func (c *GTSCaches) initListEntry() {
|
|||
l2 := new(gtsmodel.ListEntry)
|
||||
*l2 = *l1
|
||||
return l2
|
||||
}, cap)
|
||||
|
||||
c.listEntry.IgnoreErrors(ignoreErrors)
|
||||
}, config.GetCacheGTSListEntryMaxSize())
|
||||
c.list.SetTTL(config.GetCacheGTSListEntryTTL(), true)
|
||||
c.list.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initMarker() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofMarker(), // model in-mem size.
|
||||
config.GetCacheMarkerMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Marker cache size = %d", cap)
|
||||
|
||||
c.marker = result.New([]result.Lookup{
|
||||
{Name: "AccountID.Name"},
|
||||
}, func(m1 *gtsmodel.Marker) *gtsmodel.Marker {
|
||||
m2 := new(gtsmodel.Marker)
|
||||
*m2 = *m1
|
||||
return m2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSMarkerMaxSize())
|
||||
c.marker.SetTTL(config.GetCacheGTSMarkerTTL(), true)
|
||||
c.marker.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initMedia() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofMedia(), // model in-mem size.
|
||||
config.GetCacheMediaMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Media cache size = %d", cap)
|
||||
|
||||
c.media = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
}, func(m1 *gtsmodel.MediaAttachment) *gtsmodel.MediaAttachment {
|
||||
m2 := new(gtsmodel.MediaAttachment)
|
||||
*m2 = *m1
|
||||
return m2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSMediaMaxSize())
|
||||
c.media.SetTTL(config.GetCacheGTSMediaTTL(), true)
|
||||
c.media.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initMention() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofMention(), // model in-mem size.
|
||||
config.GetCacheMentionMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Mention cache size = %d", cap)
|
||||
|
||||
c.mention = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
}, func(m1 *gtsmodel.Mention) *gtsmodel.Mention {
|
||||
m2 := new(gtsmodel.Mention)
|
||||
*m2 = *m1
|
||||
return m2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSMentionMaxSize())
|
||||
c.mention.SetTTL(config.GetCacheGTSMentionTTL(), true)
|
||||
c.mention.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initNotification() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofNotification(), // model in-mem size.
|
||||
config.GetCacheNotificationMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Notification cache size = %d", cap)
|
||||
|
||||
c.notification = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "NotificationType.TargetAccountID.OriginAccountID.StatusID"},
|
||||
|
@ -633,99 +533,51 @@ func (c *GTSCaches) initNotification() {
|
|||
n2 := new(gtsmodel.Notification)
|
||||
*n2 = *n1
|
||||
return n2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSNotificationMaxSize())
|
||||
c.notification.SetTTL(config.GetCacheGTSNotificationTTL(), true)
|
||||
c.notification.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initReport() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofReport(), // model in-mem size.
|
||||
config.GetCacheReportMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Report cache size = %d", cap)
|
||||
|
||||
c.report = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
}, func(r1 *gtsmodel.Report) *gtsmodel.Report {
|
||||
r2 := new(gtsmodel.Report)
|
||||
*r2 = *r1
|
||||
return r2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSReportMaxSize())
|
||||
c.report.SetTTL(config.GetCacheGTSReportTTL(), true)
|
||||
c.report.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initStatus() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofStatus(), // model in-mem size.
|
||||
config.GetCacheStatusMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Status cache size = %d", cap)
|
||||
|
||||
c.status = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "URI"},
|
||||
{Name: "URL"},
|
||||
{Name: "BoostOfID.AccountID"},
|
||||
}, func(s1 *gtsmodel.Status) *gtsmodel.Status {
|
||||
s2 := new(gtsmodel.Status)
|
||||
*s2 = *s1
|
||||
return s2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSStatusMaxSize())
|
||||
c.status.SetTTL(config.GetCacheGTSStatusTTL(), true)
|
||||
c.status.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initStatusFave() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofStatusFave(), // model in-mem size.
|
||||
config.GetCacheStatusFaveMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "StatusFave cache size = %d", cap)
|
||||
|
||||
c.statusFave = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "AccountID.StatusID"},
|
||||
{Name: "StatusID", Multi: true},
|
||||
}, func(f1 *gtsmodel.StatusFave) *gtsmodel.StatusFave {
|
||||
f2 := new(gtsmodel.StatusFave)
|
||||
*f2 = *f1
|
||||
return f2
|
||||
}, cap)
|
||||
|
||||
c.statusFave.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initStatusFaveIDs() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateSliceCacheMax(
|
||||
config.GetCacheStatusFaveIDsMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "StatusFave IDs cache size = %d", cap)
|
||||
|
||||
c.statusFaveIDs = &SliceCache[string]{Cache: simple.New[string, []string](
|
||||
0,
|
||||
cap,
|
||||
)}
|
||||
}, config.GetCacheGTSStatusFaveMaxSize())
|
||||
c.status.SetTTL(config.GetCacheGTSStatusFaveTTL(), true)
|
||||
c.status.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initTag() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofTag(), // model in-mem size.
|
||||
config.GetCacheTagMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Tag cache size = %d", cap)
|
||||
|
||||
c.tag = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "Name"},
|
||||
|
@ -733,20 +585,12 @@ func (c *GTSCaches) initTag() {
|
|||
m2 := new(gtsmodel.Tag)
|
||||
*m2 = *m1
|
||||
return m2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSTagMaxSize())
|
||||
c.tag.SetTTL(config.GetCacheGTSTagTTL(), true)
|
||||
c.tag.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initTombstone() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofTombstone(), // model in-mem size.
|
||||
config.GetCacheTombstoneMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Tombstone cache size = %d", cap)
|
||||
|
||||
c.tombstone = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "URI"},
|
||||
|
@ -754,20 +598,12 @@ func (c *GTSCaches) initTombstone() {
|
|||
t2 := new(gtsmodel.Tombstone)
|
||||
*t2 = *t1
|
||||
return t2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSTombstoneMaxSize())
|
||||
c.tombstone.SetTTL(config.GetCacheGTSTombstoneTTL(), true)
|
||||
c.tombstone.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initUser() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofUser(), // model in-mem size.
|
||||
config.GetCacheUserMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "User cache size = %d", cap)
|
||||
|
||||
c.user = result.New([]result.Lookup{
|
||||
{Name: "ID"},
|
||||
{Name: "AccountID"},
|
||||
|
@ -778,23 +614,15 @@ func (c *GTSCaches) initUser() {
|
|||
u2 := new(gtsmodel.User)
|
||||
*u2 = *u1
|
||||
return u2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheGTSUserMaxSize())
|
||||
c.user.SetTTL(config.GetCacheGTSUserTTL(), true)
|
||||
c.user.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
func (c *GTSCaches) initWebfinger() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateCacheMax(
|
||||
sizeofURIStr, sizeofURIStr,
|
||||
config.GetCacheWebfingerMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Webfinger cache size = %d", cap)
|
||||
|
||||
c.webfinger = ttl.New[string, string](
|
||||
0,
|
||||
cap,
|
||||
24*time.Hour,
|
||||
config.GetCacheGTSWebfingerMaxSize(),
|
||||
config.GetCacheGTSWebfingerTTL(),
|
||||
)
|
||||
}
|
||||
|
|
501
internal/cache/size.go
vendored
501
internal/cache/size.go
vendored
|
@ -1,501 +0,0 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"codeberg.org/gruf/go-cache/v3/simple"
|
||||
"github.com/DmitriyVTitov/size"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
)
|
||||
|
||||
const (
|
||||
// example data values.
|
||||
exampleID = id.Highest
|
||||
exampleURI = "https://social.bbc/users/ItsMePrinceCharlesInit"
|
||||
exampleText = `
|
||||
oh no me nan's gone and done it :shocked:
|
||||
|
||||
she fuckin killed the king :regicide:
|
||||
|
||||
nan what have you done :shocked:
|
||||
|
||||
no nan put down the knife, don't go after the landlords next! :knife:
|
||||
|
||||
you'll make society more equitable for all if you're not careful! :hammer_sickle:
|
||||
|
||||
#JustNanProblems #WhatWillSheDoNext #MaybeItWasntSuchABadThingAfterAll
|
||||
`
|
||||
|
||||
exampleTextSmall = "Small problem lads, me nan's gone on a bit of a rampage"
|
||||
exampleUsername = "@SexHaver1969"
|
||||
|
||||
// ID string size in memory (is always 26 char ULID).
|
||||
sizeofIDStr = unsafe.Sizeof(exampleID)
|
||||
|
||||
// URI string size in memory (use some random example URI).
|
||||
sizeofURIStr = unsafe.Sizeof(exampleURI)
|
||||
|
||||
// ID slice size in memory (using some estimate of length = 250).
|
||||
sizeofIDSlice = unsafe.Sizeof([]string{}) + 250*sizeofIDStr
|
||||
|
||||
// result cache key size estimate which is tricky. it can
|
||||
// be a serialized string of almost any type, so we pick a
|
||||
// nice serialized key size on the upper end of normal.
|
||||
sizeofResultKey = 2 * sizeofIDStr
|
||||
)
|
||||
|
||||
// calculateSliceCacheMax calculates the maximum capacity for a slice cache with given individual ratio.
|
||||
func calculateSliceCacheMax(ratio float64) int {
|
||||
return calculateCacheMax(sizeofIDStr, sizeofIDSlice, ratio)
|
||||
}
|
||||
|
||||
// calculateResultCacheMax calculates the maximum cache capacity for a result
|
||||
// cache's individual ratio number, and the size of the struct model in memory.
|
||||
func calculateResultCacheMax(structSz uintptr, ratio float64) int {
|
||||
// Estimate a worse-case scenario of extra lookup hash maps,
|
||||
// where lookups are the no. "keys" each result can be found under
|
||||
const lookups = 10
|
||||
|
||||
// Calculate the extra cache lookup map overheads.
|
||||
totalLookupKeySz := uintptr(lookups) * sizeofResultKey
|
||||
totalLookupValSz := uintptr(lookups) * unsafe.Sizeof(uint64(0))
|
||||
|
||||
// Primary cache sizes.
|
||||
pkeySz := unsafe.Sizeof(uint64(0))
|
||||
pvalSz := structSz
|
||||
|
||||
// The result cache wraps each struct result in a wrapping
|
||||
// struct with further information, and possible error. This
|
||||
// also needs to be taken into account when calculating value.
|
||||
const resultValueOverhead = unsafe.Sizeof(&struct {
|
||||
_ int64
|
||||
_ []any
|
||||
_ any
|
||||
_ error
|
||||
}{})
|
||||
|
||||
return calculateCacheMax(
|
||||
pkeySz+totalLookupKeySz,
|
||||
pvalSz+totalLookupValSz+resultValueOverhead,
|
||||
ratio,
|
||||
)
|
||||
}
|
||||
|
||||
// calculateCacheMax calculates the maximum cache capacity for a cache's
|
||||
// individual ratio number, and key + value object sizes in memory.
|
||||
func calculateCacheMax(keySz, valSz uintptr, ratio float64) int {
|
||||
if ratio < 0 {
|
||||
// Negative ratios are a secret little trick
|
||||
// to manually set the cache capacity sizes.
|
||||
return int(-1 * ratio)
|
||||
}
|
||||
|
||||
// see: https://golang.org/src/runtime/map.go
|
||||
const emptyBucketOverhead = 10.79
|
||||
|
||||
// This takes into account (roughly) that the underlying simple cache library wraps
|
||||
// elements within a simple.Entry{}, and the ordered map wraps each in a linked list elem.
|
||||
const cacheElemOverhead = unsafe.Sizeof(simple.Entry{}) + unsafe.Sizeof(struct {
|
||||
key, value interface{}
|
||||
next, prev uintptr
|
||||
}{})
|
||||
|
||||
// The inputted memory ratio does not take into account the
|
||||
// total of all ratios, so divide it here to get perc. ratio.
|
||||
totalRatio := ratio / totalOfRatios()
|
||||
|
||||
// TODO: we should also further weight this ratio depending
|
||||
// on the combined keySz + valSz as a ratio of all available
|
||||
// cache model memories. otherwise you can end up with a
|
||||
// low-ratio cache of tiny models with larger capacity than
|
||||
// a high-ratio cache of large models.
|
||||
|
||||
// Get max available cache memory, calculating max for
|
||||
// this cache by multiplying by this cache's mem ratio.
|
||||
maxMem := config.GetCacheMemoryTarget()
|
||||
fMaxMem := float64(maxMem) * totalRatio
|
||||
|
||||
// Cast to useable types.
|
||||
fKeySz := float64(keySz)
|
||||
fValSz := float64(valSz)
|
||||
|
||||
// Calculated using the internal cache map size:
|
||||
// (($keysz + $valsz) * $len) + ($len * $allOverheads) = $memSz
|
||||
return int(fMaxMem / (fKeySz + fValSz + emptyBucketOverhead + float64(cacheElemOverhead)))
|
||||
}
|
||||
|
||||
// totalOfRatios returns the total of all cache ratios added together.
|
||||
func totalOfRatios() float64 {
|
||||
// NOTE: this is not performant calculating
|
||||
// this every damn time (mainly the mutex unlocks
|
||||
// required to access each config var). fortunately
|
||||
// we only do this on init so fuck it :D
|
||||
return 0 +
|
||||
config.GetCacheAccountMemRatio() +
|
||||
config.GetCacheAccountNoteMemRatio() +
|
||||
config.GetCacheBlockMemRatio() +
|
||||
config.GetCacheBlockIDsMemRatio() +
|
||||
config.GetCacheEmojiMemRatio() +
|
||||
config.GetCacheEmojiCategoryMemRatio() +
|
||||
config.GetCacheFollowMemRatio() +
|
||||
config.GetCacheFollowIDsMemRatio() +
|
||||
config.GetCacheFollowRequestMemRatio() +
|
||||
config.GetCacheFollowRequestIDsMemRatio() +
|
||||
config.GetCacheInstanceMemRatio() +
|
||||
config.GetCacheListMemRatio() +
|
||||
config.GetCacheListEntryMemRatio() +
|
||||
config.GetCacheMarkerMemRatio() +
|
||||
config.GetCacheMediaMemRatio() +
|
||||
config.GetCacheMentionMemRatio() +
|
||||
config.GetCacheNotificationMemRatio() +
|
||||
config.GetCacheReportMemRatio() +
|
||||
config.GetCacheStatusMemRatio() +
|
||||
config.GetCacheStatusFaveMemRatio() +
|
||||
config.GetCacheTagMemRatio() +
|
||||
config.GetCacheTombstoneMemRatio() +
|
||||
config.GetCacheUserMemRatio() +
|
||||
config.GetCacheWebfingerMemRatio() +
|
||||
config.GetCacheVisibilityMemRatio()
|
||||
}
|
||||
|
||||
func sizeofAccount() uintptr {
|
||||
return uintptr(size.Of(>smodel.Account{
|
||||
ID: exampleID,
|
||||
Username: exampleUsername,
|
||||
AvatarMediaAttachmentID: exampleID,
|
||||
HeaderMediaAttachmentID: exampleID,
|
||||
DisplayName: exampleUsername,
|
||||
Note: exampleText,
|
||||
NoteRaw: exampleText,
|
||||
Memorial: func() *bool { ok := false; return &ok }(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
FetchedAt: time.Now(),
|
||||
Bot: func() *bool { ok := true; return &ok }(),
|
||||
Locked: func() *bool { ok := true; return &ok }(),
|
||||
Discoverable: func() *bool { ok := false; return &ok }(),
|
||||
Privacy: gtsmodel.VisibilityFollowersOnly,
|
||||
Sensitive: func() *bool { ok := true; return &ok }(),
|
||||
Language: "fr",
|
||||
URI: exampleURI,
|
||||
URL: exampleURI,
|
||||
InboxURI: exampleURI,
|
||||
OutboxURI: exampleURI,
|
||||
FollowersURI: exampleURI,
|
||||
FollowingURI: exampleURI,
|
||||
FeaturedCollectionURI: exampleURI,
|
||||
ActorType: ap.ActorPerson,
|
||||
PrivateKey: &rsa.PrivateKey{},
|
||||
PublicKey: &rsa.PublicKey{},
|
||||
PublicKeyURI: exampleURI,
|
||||
SensitizedAt: time.Time{},
|
||||
SilencedAt: time.Now(),
|
||||
SuspendedAt: time.Now(),
|
||||
HideCollections: func() *bool { ok := true; return &ok }(),
|
||||
SuspensionOrigin: "",
|
||||
EnableRSS: func() *bool { ok := true; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofAccountNote() uintptr {
|
||||
return uintptr(size.Of(>smodel.AccountNote{
|
||||
ID: exampleID,
|
||||
AccountID: exampleID,
|
||||
TargetAccountID: exampleID,
|
||||
Comment: exampleTextSmall,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofBlock() uintptr {
|
||||
return uintptr(size.Of(>smodel.Block{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
URI: exampleURI,
|
||||
AccountID: exampleID,
|
||||
TargetAccountID: exampleID,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofEmoji() uintptr {
|
||||
return uintptr(size.Of(>smodel.Emoji{
|
||||
ID: exampleID,
|
||||
Shortcode: exampleTextSmall,
|
||||
Domain: exampleURI,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ImageRemoteURL: exampleURI,
|
||||
ImageStaticRemoteURL: exampleURI,
|
||||
ImageURL: exampleURI,
|
||||
ImagePath: exampleURI,
|
||||
ImageStaticURL: exampleURI,
|
||||
ImageStaticPath: exampleURI,
|
||||
ImageContentType: "image/png",
|
||||
ImageStaticContentType: "image/png",
|
||||
ImageUpdatedAt: time.Now(),
|
||||
Disabled: func() *bool { ok := false; return &ok }(),
|
||||
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
||||
VisibleInPicker: func() *bool { ok := true; return &ok }(),
|
||||
CategoryID: "01GGQ8V4993XK67B2JB396YFB7",
|
||||
Cached: func() *bool { ok := true; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofEmojiCategory() uintptr {
|
||||
return uintptr(size.Of(>smodel.EmojiCategory{
|
||||
ID: exampleID,
|
||||
Name: exampleUsername,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofFollow() uintptr {
|
||||
return uintptr(size.Of(>smodel.Follow{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
AccountID: exampleID,
|
||||
TargetAccountID: exampleID,
|
||||
ShowReblogs: func() *bool { ok := true; return &ok }(),
|
||||
URI: exampleURI,
|
||||
Notify: func() *bool { ok := false; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofFollowRequest() uintptr {
|
||||
return uintptr(size.Of(>smodel.FollowRequest{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
AccountID: exampleID,
|
||||
TargetAccountID: exampleID,
|
||||
ShowReblogs: func() *bool { ok := true; return &ok }(),
|
||||
URI: exampleURI,
|
||||
Notify: func() *bool { ok := false; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofInstance() uintptr {
|
||||
return uintptr(size.Of(>smodel.Instance{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Domain: exampleURI,
|
||||
URI: exampleURI,
|
||||
Title: exampleTextSmall,
|
||||
ShortDescription: exampleText,
|
||||
Description: exampleText,
|
||||
ContactEmail: exampleUsername,
|
||||
ContactAccountUsername: exampleUsername,
|
||||
ContactAccountID: exampleID,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofList() uintptr {
|
||||
return uintptr(size.Of(>smodel.List{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Title: exampleTextSmall,
|
||||
AccountID: exampleID,
|
||||
RepliesPolicy: gtsmodel.RepliesPolicyFollowed,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofListEntry() uintptr {
|
||||
return uintptr(size.Of(>smodel.ListEntry{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ListID: exampleID,
|
||||
FollowID: exampleID,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofMarker() uintptr {
|
||||
return uintptr(size.Of(>smodel.Marker{
|
||||
AccountID: exampleID,
|
||||
Name: gtsmodel.MarkerNameHome,
|
||||
UpdatedAt: time.Now(),
|
||||
Version: 0,
|
||||
LastReadID: exampleID,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofMedia() uintptr {
|
||||
return uintptr(size.Of(>smodel.MediaAttachment{
|
||||
ID: exampleID,
|
||||
StatusID: exampleID,
|
||||
URL: exampleURI,
|
||||
RemoteURL: exampleURI,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
AccountID: exampleID,
|
||||
Description: exampleText,
|
||||
ScheduledStatusID: exampleID,
|
||||
Blurhash: exampleTextSmall,
|
||||
File: gtsmodel.File{
|
||||
Path: exampleURI,
|
||||
ContentType: "image/jpeg",
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Thumbnail: gtsmodel.Thumbnail{
|
||||
Path: exampleURI,
|
||||
ContentType: "image/jpeg",
|
||||
UpdatedAt: time.Now(),
|
||||
URL: exampleURI,
|
||||
RemoteURL: exampleURI,
|
||||
},
|
||||
Avatar: func() *bool { ok := false; return &ok }(),
|
||||
Header: func() *bool { ok := false; return &ok }(),
|
||||
Cached: func() *bool { ok := true; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofMention() uintptr {
|
||||
return uintptr(size.Of(>smodel.Mention{
|
||||
ID: exampleURI,
|
||||
StatusID: exampleURI,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
OriginAccountID: exampleURI,
|
||||
OriginAccountURI: exampleURI,
|
||||
TargetAccountID: exampleID,
|
||||
NameString: exampleUsername,
|
||||
TargetAccountURI: exampleURI,
|
||||
TargetAccountURL: exampleURI,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofNotification() uintptr {
|
||||
return uintptr(size.Of(>smodel.Notification{
|
||||
ID: exampleID,
|
||||
NotificationType: gtsmodel.NotificationFave,
|
||||
CreatedAt: time.Now(),
|
||||
TargetAccountID: exampleID,
|
||||
OriginAccountID: exampleID,
|
||||
StatusID: exampleID,
|
||||
Read: func() *bool { ok := false; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofReport() uintptr {
|
||||
return uintptr(size.Of(>smodel.Report{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
URI: exampleURI,
|
||||
AccountID: exampleID,
|
||||
TargetAccountID: exampleID,
|
||||
Comment: exampleText,
|
||||
StatusIDs: []string{exampleID, exampleID, exampleID},
|
||||
Forwarded: func() *bool { ok := true; return &ok }(),
|
||||
ActionTaken: exampleText,
|
||||
ActionTakenAt: time.Now(),
|
||||
ActionTakenByAccountID: exampleID,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofStatus() uintptr {
|
||||
return uintptr(size.Of(>smodel.Status{
|
||||
ID: exampleURI,
|
||||
URI: exampleURI,
|
||||
URL: exampleURI,
|
||||
Content: exampleText,
|
||||
Text: exampleText,
|
||||
AttachmentIDs: []string{exampleID, exampleID, exampleID},
|
||||
TagIDs: []string{exampleID, exampleID, exampleID},
|
||||
MentionIDs: []string{},
|
||||
EmojiIDs: []string{exampleID, exampleID, exampleID},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
FetchedAt: time.Now(),
|
||||
Local: func() *bool { ok := false; return &ok }(),
|
||||
AccountURI: exampleURI,
|
||||
AccountID: exampleID,
|
||||
InReplyToID: exampleID,
|
||||
InReplyToURI: exampleURI,
|
||||
InReplyToAccountID: exampleID,
|
||||
BoostOfID: exampleID,
|
||||
BoostOfAccountID: exampleID,
|
||||
ContentWarning: exampleUsername, // similar length
|
||||
Visibility: gtsmodel.VisibilityPublic,
|
||||
Sensitive: func() *bool { ok := false; return &ok }(),
|
||||
Language: "en",
|
||||
CreatedWithApplicationID: exampleID,
|
||||
Federated: func() *bool { ok := true; return &ok }(),
|
||||
Boostable: func() *bool { ok := true; return &ok }(),
|
||||
Replyable: func() *bool { ok := true; return &ok }(),
|
||||
Likeable: func() *bool { ok := true; return &ok }(),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofStatusFave() uintptr {
|
||||
return uintptr(size.Of(>smodel.StatusFave{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
AccountID: exampleID,
|
||||
TargetAccountID: exampleID,
|
||||
StatusID: exampleID,
|
||||
URI: exampleURI,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofTag() uintptr {
|
||||
return uintptr(size.Of(>smodel.Tag{
|
||||
ID: exampleID,
|
||||
Name: exampleUsername,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Useable: func() *bool { ok := true; return &ok }(),
|
||||
Listable: func() *bool { ok := true; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofTombstone() uintptr {
|
||||
return uintptr(size.Of(>smodel.Tombstone{
|
||||
ID: exampleID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Domain: exampleUsername,
|
||||
URI: exampleURI,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofVisibility() uintptr {
|
||||
return uintptr(size.Of(&CachedVisibility{
|
||||
ItemID: exampleID,
|
||||
RequesterID: exampleID,
|
||||
Type: VisibilityTypeAccount,
|
||||
Value: false,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofUser() uintptr {
|
||||
return uintptr(size.Of(>smodel.User{}))
|
||||
}
|
4
internal/cache/slice.go
vendored
4
internal/cache/slice.go
vendored
|
@ -18,14 +18,14 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"codeberg.org/gruf/go-cache/v3/simple"
|
||||
"codeberg.org/gruf/go-cache/v3/ttl"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// SliceCache wraps a ttl.Cache to provide simple loader-callback
|
||||
// functions for fetching + caching slices of objects (e.g. IDs).
|
||||
type SliceCache[T any] struct {
|
||||
*simple.Cache[string, []T]
|
||||
*ttl.Cache[string, []T]
|
||||
}
|
||||
|
||||
// Load will attempt to load an existing slice from the cache for the given key, else calling the provided load function and caching the result.
|
||||
|
|
22
internal/cache/util.go
vendored
22
internal/cache/util.go
vendored
|
@ -20,8 +20,10 @@ package cache
|
|||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-cache/v3/result"
|
||||
errorsv2 "codeberg.org/gruf/go-errors/v2"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
|
@ -54,6 +56,26 @@ func (*nocopy) Lock() {}
|
|||
|
||||
func (*nocopy) Unlock() {}
|
||||
|
||||
// tryStart will attempt to start the given cache only if sweep duration > 0 (sweeping is enabled).
|
||||
func tryStart[ValueType any](cache *result.Cache[ValueType], sweep time.Duration) {
|
||||
if sweep > 0 {
|
||||
var z ValueType
|
||||
msg := fmt.Sprintf("starting %T cache", z)
|
||||
tryUntil(msg, 5, func() bool {
|
||||
return cache.Start(sweep)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tryStop will attempt to stop the given cache only if sweep duration > 0 (sweeping is enabled).
|
||||
func tryStop[ValueType any](cache *result.Cache[ValueType], sweep time.Duration) {
|
||||
if sweep > 0 {
|
||||
var z ValueType
|
||||
msg := fmt.Sprintf("stopping %T cache", z)
|
||||
tryUntil(msg, 5, cache.Stop)
|
||||
}
|
||||
}
|
||||
|
||||
// tryUntil will attempt to call 'do' for 'count' attempts, before panicking with 'msg'.
|
||||
func tryUntil(msg string, count int, do func() bool) {
|
||||
for i := 0; i < count; i++ {
|
||||
|
|
15
internal/cache/visibility.go
vendored
15
internal/cache/visibility.go
vendored
|
@ -20,7 +20,6 @@ package cache
|
|||
import (
|
||||
"codeberg.org/gruf/go-cache/v3/result"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
type VisibilityCache struct {
|
||||
|
@ -30,14 +29,6 @@ type VisibilityCache struct {
|
|||
// Init will initialize the visibility cache in this collection.
|
||||
// NOTE: the cache MUST NOT be in use anywhere, this is not thread-safe.
|
||||
func (c *VisibilityCache) Init() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofVisibility(), // model in-mem size.
|
||||
config.GetCacheVisibilityMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "Visibility cache size = %d", cap)
|
||||
|
||||
c.Cache = result.New([]result.Lookup{
|
||||
{Name: "ItemID", Multi: true},
|
||||
{Name: "RequesterID", Multi: true},
|
||||
|
@ -46,17 +37,19 @@ func (c *VisibilityCache) Init() {
|
|||
v2 := new(CachedVisibility)
|
||||
*v2 = *v1
|
||||
return v2
|
||||
}, cap)
|
||||
|
||||
}, config.GetCacheVisibilityMaxSize())
|
||||
c.Cache.SetTTL(config.GetCacheVisibilityTTL(), true)
|
||||
c.Cache.IgnoreErrors(ignoreErrors)
|
||||
}
|
||||
|
||||
// Start will attempt to start the visibility cache, or panic.
|
||||
func (c *VisibilityCache) Start() {
|
||||
tryStart(c.Cache, config.GetCacheVisibilitySweepFreq())
|
||||
}
|
||||
|
||||
// Stop will attempt to stop the visibility cache, or panic.
|
||||
func (c *VisibilityCache) Stop() {
|
||||
tryStop(c.Cache, config.GetCacheVisibilitySweepFreq())
|
||||
}
|
||||
|
||||
// VisibilityType represents a visibility lookup type.
|
||||
|
|
|
@ -83,23 +83,19 @@ func (c *Cleaner) removeFiles(ctx context.Context, files ...string) (int, error)
|
|||
return len(files), nil
|
||||
}
|
||||
|
||||
var (
|
||||
errs gtserror.MultiError
|
||||
errCount int
|
||||
)
|
||||
var errs gtserror.MultiError
|
||||
|
||||
for _, path := range files {
|
||||
// Remove each provided storage path.
|
||||
log.Debugf(ctx, "removing file: %s", path)
|
||||
err := c.state.Storage.Delete(ctx, path)
|
||||
if err != nil && !errors.Is(err, storage.ErrNotFound) {
|
||||
errs.Appendf("error removing %s: %w", path, err)
|
||||
errCount++
|
||||
errs.Appendf("error removing %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate no. files removed.
|
||||
diff := len(files) - errCount
|
||||
diff := len(files) - len(errs)
|
||||
|
||||
// Wrap the combined error slice.
|
||||
if err := errs.Combine(); err != nil {
|
||||
|
|
|
@ -175,35 +175,113 @@ type HTTPClientConfiguration struct {
|
|||
}
|
||||
|
||||
type CacheConfiguration struct {
|
||||
MemoryTarget bytesize.Size `name:"memory-target"`
|
||||
AccountMemRatio float64 `name:"account-mem-ratio"`
|
||||
AccountNoteMemRatio float64 `name:"account-note-mem-ratio"`
|
||||
BlockMemRatio float64 `name:"block-mem-ratio"`
|
||||
BlockIDsMemRatio float64 `name:"block-mem-ratio"`
|
||||
BoostOfIDsMemRatio float64 `name:"boost-of-ids-mem-ratio"`
|
||||
EmojiMemRatio float64 `name:"emoji-mem-ratio"`
|
||||
EmojiCategoryMemRatio float64 `name:"emoji-category-mem-ratio"`
|
||||
FollowMemRatio float64 `name:"follow-mem-ratio"`
|
||||
FollowIDsMemRatio float64 `name:"follow-ids-mem-ratio"`
|
||||
FollowRequestMemRatio float64 `name:"follow-request-mem-ratio"`
|
||||
FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
|
||||
InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
|
||||
InstanceMemRatio float64 `name:"instance-mem-ratio"`
|
||||
ListMemRatio float64 `name:"list-mem-ratio"`
|
||||
ListEntryMemRatio float64 `name:"list-entry-mem-ratio"`
|
||||
MarkerMemRatio float64 `name:"marker-mem-ratio"`
|
||||
MediaMemRatio float64 `name:"media-mem-ratio"`
|
||||
MentionMemRatio float64 `name:"mention-mem-ratio"`
|
||||
NotificationMemRatio float64 `name:"notification-mem-ratio"`
|
||||
ReportMemRatio float64 `name:"report-mem-ratio"`
|
||||
StatusMemRatio float64 `name:"status-mem-ratio"`
|
||||
StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
|
||||
StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
|
||||
TagMemRatio float64 `name:"tag-mem-ratio"`
|
||||
TombstoneMemRatio float64 `name:"tombstone-mem-ratio"`
|
||||
UserMemRatio float64 `name:"user-mem-ratio"`
|
||||
WebfingerMemRatio float64 `name:"webfinger-mem-ratio"`
|
||||
VisibilityMemRatio float64 `name:"visibility-mem-ratio"`
|
||||
GTS GTSCacheConfiguration `name:"gts"`
|
||||
|
||||
VisibilityMaxSize int `name:"visibility-max-size"`
|
||||
VisibilityTTL time.Duration `name:"visibility-ttl"`
|
||||
VisibilitySweepFreq time.Duration `name:"visibility-sweep-freq"`
|
||||
}
|
||||
|
||||
type GTSCacheConfiguration struct {
|
||||
AccountMaxSize int `name:"account-max-size"`
|
||||
AccountTTL time.Duration `name:"account-ttl"`
|
||||
AccountSweepFreq time.Duration `name:"account-sweep-freq"`
|
||||
|
||||
AccountNoteMaxSize int `name:"account-note-max-size"`
|
||||
AccountNoteTTL time.Duration `name:"account-note-ttl"`
|
||||
AccountNoteSweepFreq time.Duration `name:"account-note-sweep-freq"`
|
||||
|
||||
BlockMaxSize int `name:"block-max-size"`
|
||||
BlockTTL time.Duration `name:"block-ttl"`
|
||||
BlockSweepFreq time.Duration `name:"block-sweep-freq"`
|
||||
|
||||
BlockIDsMaxSize int `name:"block-ids-max-size"`
|
||||
BlockIDsTTL time.Duration `name:"block-ids-ttl"`
|
||||
BlockIDsSweepFreq time.Duration `name:"block-ids-sweep-freq"`
|
||||
|
||||
DomainBlockMaxSize int `name:"domain-block-max-size"`
|
||||
DomainBlockTTL time.Duration `name:"domain-block-ttl"`
|
||||
DomainBlockSweepFreq time.Duration `name:"domain-block-sweep-freq"`
|
||||
|
||||
EmojiMaxSize int `name:"emoji-max-size"`
|
||||
EmojiTTL time.Duration `name:"emoji-ttl"`
|
||||
EmojiSweepFreq time.Duration `name:"emoji-sweep-freq"`
|
||||
|
||||
EmojiCategoryMaxSize int `name:"emoji-category-max-size"`
|
||||
EmojiCategoryTTL time.Duration `name:"emoji-category-ttl"`
|
||||
EmojiCategorySweepFreq time.Duration `name:"emoji-category-sweep-freq"`
|
||||
|
||||
FollowMaxSize int `name:"follow-max-size"`
|
||||
FollowTTL time.Duration `name:"follow-ttl"`
|
||||
FollowSweepFreq time.Duration `name:"follow-sweep-freq"`
|
||||
|
||||
FollowIDsMaxSize int `name:"follow-ids-max-size"`
|
||||
FollowIDsTTL time.Duration `name:"follow-ids-ttl"`
|
||||
FollowIDsSweepFreq time.Duration `name:"follow-ids-sweep-freq"`
|
||||
|
||||
FollowRequestMaxSize int `name:"follow-request-max-size"`
|
||||
FollowRequestTTL time.Duration `name:"follow-request-ttl"`
|
||||
FollowRequestSweepFreq time.Duration `name:"follow-request-sweep-freq"`
|
||||
|
||||
FollowRequestIDsMaxSize int `name:"follow-request-ids-max-size"`
|
||||
FollowRequestIDsTTL time.Duration `name:"follow-request-ids-ttl"`
|
||||
FollowRequestIDsSweepFreq time.Duration `name:"follow-request-ids-sweep-freq"`
|
||||
|
||||
InstanceMaxSize int `name:"instance-max-size"`
|
||||
InstanceTTL time.Duration `name:"instance-ttl"`
|
||||
InstanceSweepFreq time.Duration `name:"instance-sweep-freq"`
|
||||
|
||||
ListMaxSize int `name:"list-max-size"`
|
||||
ListTTL time.Duration `name:"list-ttl"`
|
||||
ListSweepFreq time.Duration `name:"list-sweep-freq"`
|
||||
|
||||
ListEntryMaxSize int `name:"list-entry-max-size"`
|
||||
ListEntryTTL time.Duration `name:"list-entry-ttl"`
|
||||
ListEntrySweepFreq time.Duration `name:"list-entry-sweep-freq"`
|
||||
|
||||
MarkerMaxSize int `name:"marker-max-size"`
|
||||
MarkerTTL time.Duration `name:"marker-ttl"`
|
||||
MarkerSweepFreq time.Duration `name:"marker-sweep-freq"`
|
||||
|
||||
MediaMaxSize int `name:"media-max-size"`
|
||||
MediaTTL time.Duration `name:"media-ttl"`
|
||||
MediaSweepFreq time.Duration `name:"media-sweep-freq"`
|
||||
|
||||
MentionMaxSize int `name:"mention-max-size"`
|
||||
MentionTTL time.Duration `name:"mention-ttl"`
|
||||
MentionSweepFreq time.Duration `name:"mention-sweep-freq"`
|
||||
|
||||
NotificationMaxSize int `name:"notification-max-size"`
|
||||
NotificationTTL time.Duration `name:"notification-ttl"`
|
||||
NotificationSweepFreq time.Duration `name:"notification-sweep-freq"`
|
||||
|
||||
ReportMaxSize int `name:"report-max-size"`
|
||||
ReportTTL time.Duration `name:"report-ttl"`
|
||||
ReportSweepFreq time.Duration `name:"report-sweep-freq"`
|
||||
|
||||
StatusMaxSize int `name:"status-max-size"`
|
||||
StatusTTL time.Duration `name:"status-ttl"`
|
||||
StatusSweepFreq time.Duration `name:"status-sweep-freq"`
|
||||
|
||||
StatusFaveMaxSize int `name:"status-fave-max-size"`
|
||||
StatusFaveTTL time.Duration `name:"status-fave-ttl"`
|
||||
StatusFaveSweepFreq time.Duration `name:"status-fave-sweep-freq"`
|
||||
|
||||
TagMaxSize int `name:"tag-max-size"`
|
||||
TagTTL time.Duration `name:"tag-ttl"`
|
||||
TagSweepFreq time.Duration `name:"tag-sweep-freq"`
|
||||
|
||||
TombstoneMaxSize int `name:"tombstone-max-size"`
|
||||
TombstoneTTL time.Duration `name:"tombstone-ttl"`
|
||||
TombstoneSweepFreq time.Duration `name:"tombstone-sweep-freq"`
|
||||
|
||||
UserMaxSize int `name:"user-max-size"`
|
||||
UserTTL time.Duration `name:"user-ttl"`
|
||||
UserSweepFreq time.Duration `name:"user-sweep-freq"`
|
||||
|
||||
WebfingerMaxSize int `name:"webfinger-max-size"`
|
||||
WebfingerTTL time.Duration `name:"webfinger-ttl"`
|
||||
WebfingerSweepFreq time.Duration `name:"webfinger-sweep-freq"`
|
||||
}
|
||||
|
||||
// MarshalMap will marshal current Configuration into a map structure (useful for JSON/TOML/YAML).
|
||||
|
|
|
@ -126,53 +126,111 @@ var Defaults = Configuration{
|
|||
AdvancedSenderMultiplier: 2, // 2 senders per CPU
|
||||
|
||||
Cache: CacheConfiguration{
|
||||
// Rough memory target that the total
|
||||
// size of all State.Caches will attempt
|
||||
// to remain with. Emphasis on *rough*.
|
||||
MemoryTarget: 200 * bytesize.MiB,
|
||||
GTS: GTSCacheConfiguration{
|
||||
AccountMaxSize: 2000,
|
||||
AccountTTL: time.Minute * 30,
|
||||
AccountSweepFreq: time.Minute,
|
||||
|
||||
// These ratios signal what percentage
|
||||
// of the available cache target memory
|
||||
// is allocated to each object type's
|
||||
// cache.
|
||||
//
|
||||
// These are weighted by a totally
|
||||
// assorted mixture of priority, and
|
||||
// manual twiddling to get the generated
|
||||
// cache capacity ratios within normal
|
||||
// amounts dependent size of the models.
|
||||
//
|
||||
// when TODO items in the size.go source
|
||||
// file have been addressed, these should
|
||||
// be able to make some more sense :D
|
||||
AccountMemRatio: 18,
|
||||
AccountNoteMemRatio: 0.1,
|
||||
BlockMemRatio: 3,
|
||||
BlockIDsMemRatio: 3,
|
||||
BoostOfIDsMemRatio: 3,
|
||||
EmojiMemRatio: 3,
|
||||
EmojiCategoryMemRatio: 0.1,
|
||||
FollowMemRatio: 4,
|
||||
FollowIDsMemRatio: 4,
|
||||
FollowRequestMemRatio: 2,
|
||||
FollowRequestIDsMemRatio: 2,
|
||||
InReplyToIDsMemRatio: 3,
|
||||
InstanceMemRatio: 1,
|
||||
ListMemRatio: 3,
|
||||
ListEntryMemRatio: 3,
|
||||
MarkerMemRatio: 0.5,
|
||||
MediaMemRatio: 4,
|
||||
MentionMemRatio: 5,
|
||||
NotificationMemRatio: 5,
|
||||
ReportMemRatio: 1,
|
||||
StatusMemRatio: 18,
|
||||
StatusFaveMemRatio: 5,
|
||||
StatusFaveIDsMemRatio: 3,
|
||||
TagMemRatio: 3,
|
||||
TombstoneMemRatio: 2,
|
||||
UserMemRatio: 0.1,
|
||||
WebfingerMemRatio: 0.1,
|
||||
VisibilityMemRatio: 2,
|
||||
AccountNoteMaxSize: 1000,
|
||||
AccountNoteTTL: time.Minute * 30,
|
||||
AccountNoteSweepFreq: time.Minute,
|
||||
|
||||
BlockMaxSize: 1000,
|
||||
BlockTTL: time.Minute * 30,
|
||||
BlockSweepFreq: time.Minute,
|
||||
|
||||
BlockIDsMaxSize: 500,
|
||||
BlockIDsTTL: time.Minute * 30,
|
||||
BlockIDsSweepFreq: time.Minute,
|
||||
|
||||
DomainBlockMaxSize: 2000,
|
||||
DomainBlockTTL: time.Hour * 24,
|
||||
DomainBlockSweepFreq: time.Minute,
|
||||
|
||||
EmojiMaxSize: 2000,
|
||||
EmojiTTL: time.Minute * 30,
|
||||
EmojiSweepFreq: time.Minute,
|
||||
|
||||
EmojiCategoryMaxSize: 100,
|
||||
EmojiCategoryTTL: time.Minute * 30,
|
||||
EmojiCategorySweepFreq: time.Minute,
|
||||
|
||||
FollowMaxSize: 2000,
|
||||
FollowTTL: time.Minute * 30,
|
||||
FollowSweepFreq: time.Minute,
|
||||
|
||||
FollowIDsMaxSize: 500,
|
||||
FollowIDsTTL: time.Minute * 30,
|
||||
FollowIDsSweepFreq: time.Minute,
|
||||
|
||||
FollowRequestMaxSize: 2000,
|
||||
FollowRequestTTL: time.Minute * 30,
|
||||
FollowRequestSweepFreq: time.Minute,
|
||||
|
||||
FollowRequestIDsMaxSize: 500,
|
||||
FollowRequestIDsTTL: time.Minute * 30,
|
||||
FollowRequestIDsSweepFreq: time.Minute,
|
||||
|
||||
InstanceMaxSize: 2000,
|
||||
InstanceTTL: time.Minute * 30,
|
||||
InstanceSweepFreq: time.Minute,
|
||||
|
||||
ListMaxSize: 2000,
|
||||
ListTTL: time.Minute * 30,
|
||||
ListSweepFreq: time.Minute,
|
||||
|
||||
ListEntryMaxSize: 2000,
|
||||
ListEntryTTL: time.Minute * 30,
|
||||
ListEntrySweepFreq: time.Minute,
|
||||
|
||||
MarkerMaxSize: 2000,
|
||||
MarkerTTL: time.Hour * 6,
|
||||
MarkerSweepFreq: time.Minute,
|
||||
|
||||
MediaMaxSize: 1000,
|
||||
MediaTTL: time.Minute * 30,
|
||||
MediaSweepFreq: time.Minute,
|
||||
|
||||
MentionMaxSize: 2000,
|
||||
MentionTTL: time.Minute * 30,
|
||||
MentionSweepFreq: time.Minute,
|
||||
|
||||
NotificationMaxSize: 1000,
|
||||
NotificationTTL: time.Minute * 30,
|
||||
NotificationSweepFreq: time.Minute,
|
||||
|
||||
ReportMaxSize: 100,
|
||||
ReportTTL: time.Minute * 30,
|
||||
ReportSweepFreq: time.Minute,
|
||||
|
||||
StatusMaxSize: 2000,
|
||||
StatusTTL: time.Minute * 30,
|
||||
StatusSweepFreq: time.Minute,
|
||||
|
||||
StatusFaveMaxSize: 2000,
|
||||
StatusFaveTTL: time.Minute * 30,
|
||||
StatusFaveSweepFreq: time.Minute,
|
||||
|
||||
TagMaxSize: 2000,
|
||||
TagTTL: time.Minute * 30,
|
||||
TagSweepFreq: time.Minute,
|
||||
|
||||
TombstoneMaxSize: 500,
|
||||
TombstoneTTL: time.Minute * 30,
|
||||
TombstoneSweepFreq: time.Minute,
|
||||
|
||||
UserMaxSize: 500,
|
||||
UserTTL: time.Minute * 30,
|
||||
UserSweepFreq: time.Minute,
|
||||
|
||||
WebfingerMaxSize: 250,
|
||||
WebfingerTTL: time.Hour * 24,
|
||||
WebfingerSweepFreq: time.Minute * 15,
|
||||
},
|
||||
|
||||
VisibilityMaxSize: 2000,
|
||||
VisibilityTTL: time.Minute * 30,
|
||||
VisibilitySweepFreq: time.Minute,
|
||||
},
|
||||
|
||||
HTTPClient: HTTPClientConfiguration{
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -20,6 +20,7 @@ package bundb
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -254,7 +255,7 @@ func (a *accountDB) getAccount(ctx context.Context, lookup string, dbQuery func(
|
|||
func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(3)
|
||||
errs = make(gtserror.MultiError, 0, 3)
|
||||
)
|
||||
|
||||
if account.AvatarMediaAttachment == nil && account.AvatarMediaAttachmentID != "" {
|
||||
|
@ -264,7 +265,7 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
|
|||
account.AvatarMediaAttachmentID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating account avatar: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating account avatar: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,7 +276,7 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
|
|||
account.HeaderMediaAttachmentID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating account header: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating account header: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,15 +287,11 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
|
|||
account.EmojiIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating account emojis: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating account emojis: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||
|
@ -471,13 +468,24 @@ func (a *accountDB) GetAccountCustomCSSByUsername(ctx context.Context, username
|
|||
func (a *accountDB) GetAccountsUsingEmoji(ctx context.Context, emojiID string) ([]*gtsmodel.Account, error) {
|
||||
var accountIDs []string
|
||||
|
||||
// SELECT all accounts using this emoji,
|
||||
// using a relational table for improved perf.
|
||||
if _, err := a.db.NewSelect().
|
||||
Table("account_to_emojis").
|
||||
Column("account_id").
|
||||
Where("? = ?", bun.Ident("emoji_id"), emojiID).
|
||||
Exec(ctx, &accountIDs); err != nil {
|
||||
// Create SELECT account query.
|
||||
q := a.db.NewSelect().
|
||||
Table("accounts").
|
||||
Column("id")
|
||||
|
||||
// Append a WHERE LIKE clause to the query
|
||||
// that checks the `emoji` column for any
|
||||
// text containing this specific emoji ID.
|
||||
//
|
||||
// The reason we do this instead of doing a
|
||||
// `WHERE ? IN (emojis)` is that the latter
|
||||
// ends up being much MUCH slower, and the
|
||||
// database stores this ID-array-column as
|
||||
// text anyways, allowing a simple LIKE query.
|
||||
q = whereLike(q, "emojis", emojiID)
|
||||
|
||||
// Execute the query, scanning destination into accountIDs.
|
||||
if _, err := q.Exec(ctx, &accountIDs); err != nil {
|
||||
return nil, a.db.ProcessError(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -106,55 +106,39 @@ func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) error {
|
|||
}
|
||||
|
||||
return e.db.RunInTx(ctx, func(tx bun.Tx) error {
|
||||
// Delete relational links between this emoji
|
||||
// and any statuses using it, returning the
|
||||
// status IDs so we can later update them.
|
||||
if _, err := tx.NewDelete().
|
||||
Table("status_to_emojis").
|
||||
Where("? = ?", bun.Ident("emoji_id"), id).
|
||||
Returning("status_id").
|
||||
Exec(ctx, &statusIDs); err != nil {
|
||||
// delete links between this emoji and any statuses that use it
|
||||
// TODO: remove when we delete this table
|
||||
if _, err := tx.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("status_to_emojis"), bun.Ident("status_to_emoji")).
|
||||
Where("? = ?", bun.Ident("status_to_emoji.emoji_id"), id).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete relational links between this emoji
|
||||
// and any accounts using it, returning the
|
||||
// account IDs so we can later update them.
|
||||
if _, err := tx.NewDelete().
|
||||
Table("account_to_emojis").
|
||||
Where("? = ?", bun.Ident("emoji_id"), id).
|
||||
Returning("account_id").
|
||||
Exec(ctx, &accountIDs); err != nil {
|
||||
// delete links between this emoji and any accounts that use it
|
||||
// TODO: remove when we delete this table
|
||||
if _, err := tx.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("account_to_emojis"), bun.Ident("account_to_emoji")).
|
||||
Where("? = ?", bun.Ident("account_to_emoji.emoji_id"), id).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range statusIDs {
|
||||
var emojiIDs []string
|
||||
// Prepare a SELECT query with a WHERE LIKE
|
||||
// that checks the `emoji` column for any
|
||||
// text containing this specific emoji ID.
|
||||
//
|
||||
// (see GetStatusesUsingEmoji() for details.)
|
||||
aq := tx.NewSelect().Table("accounts").Column("id")
|
||||
aq = whereLike(aq, "emojis", id)
|
||||
|
||||
// Select statuses with ID.
|
||||
if _, err := tx.NewSelect().
|
||||
Table("statuses").
|
||||
Column("emojis").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Exec(ctx); err != nil &&
|
||||
err != sql.ErrNoRows {
|
||||
// Select all accounts using this emoji into accountIDss.
|
||||
if _, err := aq.Exec(ctx, &accountIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drop ID from account emojis.
|
||||
emojiIDs = dropID(emojiIDs, id)
|
||||
|
||||
// Update status emoji IDs.
|
||||
if _, err := tx.NewUpdate().
|
||||
Table("statuses").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Set("emojis = ?", emojiIDs).
|
||||
Exec(ctx); err != nil &&
|
||||
err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range accountIDs {
|
||||
var emojiIDs []string
|
||||
|
||||
|
@ -162,7 +146,7 @@ func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) error {
|
|||
if _, err := tx.NewSelect().
|
||||
Table("accounts").
|
||||
Column("emojis").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx); err != nil &&
|
||||
err != sql.ErrNoRows {
|
||||
return err
|
||||
|
@ -174,7 +158,47 @@ func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) error {
|
|||
// Update account emoji IDs.
|
||||
if _, err := tx.NewUpdate().
|
||||
Table("accounts").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Where("id = ?", id).
|
||||
Set("emojis = ?", emojiIDs).
|
||||
Exec(ctx); err != nil &&
|
||||
err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare a SELECT query with a WHERE LIKE
|
||||
// that checks the `emoji` column for any
|
||||
// text containing this specific emoji ID.
|
||||
//
|
||||
// (see GetStatusesUsingEmoji() for details.)
|
||||
sq := tx.NewSelect().Table("statuses").Column("id")
|
||||
sq = whereLike(sq, "emojis", id)
|
||||
|
||||
// Select all statuses using this emoji into statusIDs.
|
||||
if _, err := sq.Exec(ctx, &statusIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range statusIDs {
|
||||
var emojiIDs []string
|
||||
|
||||
// Select statuses with ID.
|
||||
if _, err := tx.NewSelect().
|
||||
Table("statuses").
|
||||
Column("emojis").
|
||||
Where("id = ?", id).
|
||||
Exec(ctx); err != nil &&
|
||||
err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drop ID from account emojis.
|
||||
emojiIDs = dropID(emojiIDs, id)
|
||||
|
||||
// Update status emoji IDs.
|
||||
if _, err := tx.NewUpdate().
|
||||
Table("statuses").
|
||||
Where("id = ?", id).
|
||||
Set("emojis = ?", emojiIDs).
|
||||
Exec(ctx); err != nil &&
|
||||
err != sql.ErrNoRows {
|
||||
|
@ -185,7 +209,7 @@ func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) error {
|
|||
// Delete emoji from database.
|
||||
if _, err := tx.NewDelete().
|
||||
Table("emojis").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -173,7 +173,7 @@ func (i *instanceDB) getInstance(ctx context.Context, lookup string, dbQuery fun
|
|||
func (i *instanceDB) populateInstance(ctx context.Context, instance *gtsmodel.Instance) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(2)
|
||||
errs = make(gtserror.MultiError, 0, 2)
|
||||
)
|
||||
|
||||
if instance.DomainBlockID != "" && instance.DomainBlock == nil {
|
||||
|
@ -183,7 +183,7 @@ func (i *instanceDB) populateInstance(ctx context.Context, instance *gtsmodel.In
|
|||
instance.Domain,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating instance domain block: %w", err)
|
||||
errs.Append(gtserror.Newf("error populating instance domain block: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,15 +194,11 @@ func (i *instanceDB) populateInstance(ctx context.Context, instance *gtsmodel.In
|
|||
instance.ContactAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating instance contact account: %w", err)
|
||||
errs.Append(gtserror.Newf("error populating instance contact account: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instance) error {
|
||||
|
|
|
@ -117,7 +117,7 @@ func (l *listDB) GetListsForAccountID(ctx context.Context, accountID string) ([]
|
|||
func (l *listDB) PopulateList(ctx context.Context, list *gtsmodel.List) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(2)
|
||||
errs = make(gtserror.MultiError, 0, 2)
|
||||
)
|
||||
|
||||
if list.Account == nil {
|
||||
|
@ -127,7 +127,7 @@ func (l *listDB) PopulateList(ctx context.Context, list *gtsmodel.List) error {
|
|||
list.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating list account: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating list account: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,15 +139,11 @@ func (l *listDB) PopulateList(ctx context.Context, list *gtsmodel.List) error {
|
|||
"", "", "", 0,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating list entries: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating list entries: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (l *listDB) PutList(ctx context.Context, list *gtsmodel.List) error {
|
||||
|
|
|
@ -160,7 +160,7 @@ func (r *relationshipDB) getFollow(ctx context.Context, lookup string, dbQuery f
|
|||
func (r *relationshipDB) PopulateFollow(ctx context.Context, follow *gtsmodel.Follow) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(2)
|
||||
errs = make(gtserror.MultiError, 0, 2)
|
||||
)
|
||||
|
||||
if follow.Account == nil {
|
||||
|
@ -170,7 +170,7 @@ func (r *relationshipDB) PopulateFollow(ctx context.Context, follow *gtsmodel.Fo
|
|||
follow.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating follow account: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating follow account: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,15 +181,11 @@ func (r *relationshipDB) PopulateFollow(ctx context.Context, follow *gtsmodel.Fo
|
|||
follow.TargetAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating follow target account: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating follow target account: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (r *relationshipDB) PutFollow(ctx context.Context, follow *gtsmodel.Follow) error {
|
||||
|
|
|
@ -20,7 +20,9 @@ package bundb
|
|||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
@ -41,6 +43,7 @@ func (s *statusDB) newStatusQ(status interface{}) *bun.SelectQuery {
|
|||
return s.db.
|
||||
NewSelect().
|
||||
Model(status).
|
||||
Relation("Tags").
|
||||
Relation("CreatedWithApplication")
|
||||
}
|
||||
|
||||
|
@ -95,26 +98,6 @@ func (s *statusDB) GetStatusByURL(ctx context.Context, url string) (*gtsmodel.St
|
|||
)
|
||||
}
|
||||
|
||||
func (s *statusDB) GetStatusBoost(ctx context.Context, boostOfID string, byAccountID string) (*gtsmodel.Status, error) {
|
||||
return s.getStatus(
|
||||
ctx,
|
||||
"BoostOfID.AccountID",
|
||||
func(status *gtsmodel.Status) error {
|
||||
return s.newStatusQ(status).
|
||||
Where("status.boost_of_id = ?", boostOfID).
|
||||
Where("status.account_id = ?", byAccountID).
|
||||
|
||||
// Our old code actually allowed a status to
|
||||
// be boosted multiple times by the same author,
|
||||
// so limit our query + order to fetch latest.
|
||||
Order("status.id DESC"). // our IDs are timestamped
|
||||
Limit(1).
|
||||
Scan(ctx)
|
||||
},
|
||||
boostOfID, byAccountID,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *statusDB) getStatus(ctx context.Context, lookup string, dbQuery func(*gtsmodel.Status) error, keyParts ...any) (*gtsmodel.Status, error) {
|
||||
// Fetch status from database cache with loader callback
|
||||
status, err := s.state.Caches.GTS.Status().Load(lookup, func() (*gtsmodel.Status, error) {
|
||||
|
@ -147,7 +130,7 @@ func (s *statusDB) getStatus(ctx context.Context, lookup string, dbQuery func(*g
|
|||
func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(9)
|
||||
errs = make(gtserror.MultiError, 0, 9)
|
||||
)
|
||||
|
||||
if status.Account == nil {
|
||||
|
@ -157,7 +140,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status author: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status author: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,7 +151,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.InReplyToID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status parent: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status parent: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,7 +163,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.InReplyToID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status parent: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status parent: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,7 +174,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.InReplyToAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status parent author: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status parent author: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +187,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.BoostOfID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status boost: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status boost: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,7 +198,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.BoostOfAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status boost author: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status boost author: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -227,7 +210,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.AttachmentIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status attachments: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status attachments: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,7 +221,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.TagIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status tags: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status tags: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,7 +232,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.MentionIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status mentions: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status mentions: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,7 +243,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
status.EmojiIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status emojis: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status emojis: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -457,13 +440,24 @@ func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) error {
|
|||
func (s *statusDB) GetStatusesUsingEmoji(ctx context.Context, emojiID string) ([]*gtsmodel.Status, error) {
|
||||
var statusIDs []string
|
||||
|
||||
// SELECT all statuses using this emoji,
|
||||
// using a relational table for improved perf.
|
||||
if _, err := s.db.NewSelect().
|
||||
Table("status_to_emojis").
|
||||
Column("status_id").
|
||||
Where("? = ?", bun.Ident("emoji_id"), emojiID).
|
||||
Exec(ctx, &statusIDs); err != nil {
|
||||
// Create SELECT status query.
|
||||
q := s.db.NewSelect().
|
||||
Table("statuses").
|
||||
Column("id")
|
||||
|
||||
// Append a WHERE LIKE clause to the query
|
||||
// that checks the `emoji` column for any
|
||||
// text containing this specific emoji ID.
|
||||
//
|
||||
// The reason we do this instead of doing a
|
||||
// `WHERE ? IN (emojis)` is that the latter
|
||||
// ends up being much MUCH slower, and the
|
||||
// database stores this ID-array-column as
|
||||
// text anyways, allowing a simple LIKE query.
|
||||
q = whereLike(q, "emojis", emojiID)
|
||||
|
||||
// Execute the query, scanning destination into statusIDs.
|
||||
if _, err := q.Exec(ctx, &statusIDs); err != nil {
|
||||
return nil, s.db.ProcessError(err)
|
||||
}
|
||||
|
||||
|
@ -521,17 +515,25 @@ func (s *statusDB) GetStatusChildren(ctx context.Context, status *gtsmodel.Statu
|
|||
}
|
||||
|
||||
func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) {
|
||||
childIDs, err := s.getStatusReplyIDs(ctx, status.ID)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf(ctx, "error getting status %s children: %v", status.ID, err)
|
||||
var childIDs []string
|
||||
|
||||
q := s.db.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||
Column("status.id").
|
||||
Where("? = ?", bun.Ident("status.in_reply_to_id"), status.ID)
|
||||
if minID != "" {
|
||||
q = q.Where("? > ?", bun.Ident("status.id"), minID)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &childIDs); err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
log.Errorf(ctx, "error getting children for %q: %v", status.ID, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, id := range childIDs {
|
||||
if id <= minID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fetch child with ID from database
|
||||
child, err := s.GetStatusByID(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -560,80 +562,48 @@ func (s *statusDB) statusChildren(ctx context.Context, status *gtsmodel.Status,
|
|||
}
|
||||
}
|
||||
|
||||
func (s *statusDB) GetStatusReplies(ctx context.Context, statusID string) ([]*gtsmodel.Status, error) {
|
||||
statusIDs, err := s.getStatusReplyIDs(ctx, statusID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetStatusesByIDs(ctx, statusIDs)
|
||||
}
|
||||
|
||||
func (s *statusDB) CountStatusReplies(ctx context.Context, statusID string) (int, error) {
|
||||
statusIDs, err := s.getStatusReplyIDs(ctx, statusID)
|
||||
return len(statusIDs), err
|
||||
}
|
||||
|
||||
func (s *statusDB) getStatusReplyIDs(ctx context.Context, statusID string) ([]string, error) {
|
||||
return s.state.Caches.GTS.InReplyToIDs().Load(statusID, func() ([]string, error) {
|
||||
var statusIDs []string
|
||||
|
||||
// Status reply IDs not in cache, perform DB query!
|
||||
if err := s.db.
|
||||
func (s *statusDB) CountStatusReplies(ctx context.Context, status *gtsmodel.Status) (int, error) {
|
||||
return s.db.
|
||||
NewSelect().
|
||||
Table("statuses").
|
||||
Column("id").
|
||||
Where("? = ?", bun.Ident("in_reply_to_id"), statusID).
|
||||
Order("id DESC").
|
||||
Scan(ctx, &statusIDs); err != nil {
|
||||
return nil, s.db.ProcessError(err)
|
||||
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||
Where("? = ?", bun.Ident("status.in_reply_to_id"), status.ID).
|
||||
Count(ctx)
|
||||
}
|
||||
|
||||
return statusIDs, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statusDB) GetStatusBoosts(ctx context.Context, statusID string) ([]*gtsmodel.Status, error) {
|
||||
statusIDs, err := s.getStatusBoostIDs(ctx, statusID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetStatusesByIDs(ctx, statusIDs)
|
||||
}
|
||||
|
||||
func (s *statusDB) IsStatusBoostedBy(ctx context.Context, statusID string, accountID string) (bool, error) {
|
||||
boost, err := s.GetStatusBoost(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
statusID,
|
||||
accountID,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return false, err
|
||||
}
|
||||
return (boost != nil), nil
|
||||
}
|
||||
|
||||
func (s *statusDB) CountStatusBoosts(ctx context.Context, statusID string) (int, error) {
|
||||
statusIDs, err := s.getStatusBoostIDs(ctx, statusID)
|
||||
return len(statusIDs), err
|
||||
}
|
||||
|
||||
func (s *statusDB) getStatusBoostIDs(ctx context.Context, statusID string) ([]string, error) {
|
||||
return s.state.Caches.GTS.BoostOfIDs().Load(statusID, func() ([]string, error) {
|
||||
var statusIDs []string
|
||||
|
||||
// Status boost IDs not in cache, perform DB query!
|
||||
if err := s.db.
|
||||
func (s *statusDB) CountStatusReblogs(ctx context.Context, status *gtsmodel.Status) (int, error) {
|
||||
return s.db.
|
||||
NewSelect().
|
||||
Table("statuses").
|
||||
Column("id").
|
||||
Where("? = ?", bun.Ident("boost_of_id"), statusID).
|
||||
Order("id DESC").
|
||||
Scan(ctx, &statusIDs); err != nil {
|
||||
return nil, s.db.ProcessError(err)
|
||||
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||
Where("? = ?", bun.Ident("status.boost_of_id"), status.ID).
|
||||
Count(ctx)
|
||||
}
|
||||
|
||||
return statusIDs, nil
|
||||
})
|
||||
func (s *statusDB) CountStatusFaves(ctx context.Context, status *gtsmodel.Status) (int, error) {
|
||||
return s.db.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")).
|
||||
Where("? = ?", bun.Ident("status_fave.status_id"), status.ID).
|
||||
Count(ctx)
|
||||
}
|
||||
|
||||
func (s *statusDB) IsStatusFavedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
q := s.db.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")).
|
||||
Where("? = ?", bun.Ident("status_fave.status_id"), status.ID).
|
||||
Where("? = ?", bun.Ident("status_fave.account_id"), accountID)
|
||||
|
||||
return s.db.Exists(ctx, q)
|
||||
}
|
||||
|
||||
func (s *statusDB) IsStatusRebloggedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
q := s.db.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||
Where("? = ?", bun.Ident("status.boost_of_id"), status.ID).
|
||||
Where("? = ?", bun.Ident("status.account_id"), accountID)
|
||||
|
||||
return s.db.Exists(ctx, q)
|
||||
}
|
||||
|
||||
func (s *statusDB) IsStatusMutedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
|
@ -655,3 +625,16 @@ func (s *statusDB) IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.St
|
|||
|
||||
return s.db.Exists(ctx, q)
|
||||
}
|
||||
|
||||
func (s *statusDB) GetStatusReblogs(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.Status, error) {
|
||||
reblogs := []*gtsmodel.Status{}
|
||||
|
||||
q := s.
|
||||
newStatusQ(&reblogs).
|
||||
Where("? = ?", bun.Ident("status.boost_of_id"), status.ID)
|
||||
|
||||
if err := q.Scan(ctx); err != nil {
|
||||
return nil, s.db.ProcessError(err)
|
||||
}
|
||||
return reblogs, nil
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package bundb
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
|
@ -45,14 +44,8 @@ func (s *statusFaveDB) GetStatusFave(ctx context.Context, accountID string, stat
|
|||
return s.db.
|
||||
NewSelect().
|
||||
Model(fave).
|
||||
Where("status_fave.account_id = ?", accountID).
|
||||
Where("status_fave.status_id = ?", statusID).
|
||||
|
||||
// Our old code actually allowed a status to
|
||||
// be faved multiple times by the same author,
|
||||
// so limit our query + order to fetch latest.
|
||||
Order("status_fave.id DESC"). // our IDs are timestamped
|
||||
Limit(1).
|
||||
Where("? = ?", bun.Ident("account_id"), accountID).
|
||||
Where("? = ?", bun.Ident("status_id"), statusID).
|
||||
Scan(ctx)
|
||||
},
|
||||
accountID,
|
||||
|
@ -96,72 +89,67 @@ func (s *statusFaveDB) getStatusFave(ctx context.Context, lookup string, dbQuery
|
|||
return fave, nil
|
||||
}
|
||||
|
||||
// Populate the status favourite model.
|
||||
if err := s.PopulateStatusFave(ctx, fave); err != nil {
|
||||
return nil, fmt.Errorf("error(s) populating status fave: %w", err)
|
||||
// Fetch the status fave author account.
|
||||
fave.Account, err = s.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
fave.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status fave account %q: %w", fave.AccountID, err)
|
||||
}
|
||||
|
||||
// Fetch the status fave target account.
|
||||
fave.TargetAccount, err = s.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
fave.TargetAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status fave target account %q: %w", fave.TargetAccountID, err)
|
||||
}
|
||||
|
||||
// Fetch the status fave target status.
|
||||
fave.Status, err = s.state.DB.GetStatusByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
fave.StatusID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status fave status %q: %w", fave.StatusID, err)
|
||||
}
|
||||
|
||||
return fave, nil
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) GetStatusFaves(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, error) {
|
||||
// Fetch the status fave IDs for status.
|
||||
faveIDs, err := s.getStatusFaveIDs(ctx, statusID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (s *statusFaveDB) GetStatusFavesForStatus(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, error) {
|
||||
ids := []string{}
|
||||
|
||||
if err := s.db.
|
||||
NewSelect().
|
||||
Table("status_faves").
|
||||
Column("id").
|
||||
Where("? = ?", bun.Ident("status_id"), statusID).
|
||||
Scan(ctx, &ids); err != nil {
|
||||
return nil, s.db.ProcessError(err)
|
||||
}
|
||||
|
||||
// Preallocate a slice of expected status fave capacity.
|
||||
faves := make([]*gtsmodel.StatusFave, 0, len(faveIDs))
|
||||
faves := make([]*gtsmodel.StatusFave, 0, len(ids))
|
||||
|
||||
for _, id := range faveIDs {
|
||||
// Fetch status fave model for each ID.
|
||||
for _, id := range ids {
|
||||
fave, err := s.GetStatusFaveByID(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting status fave %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
faves = append(faves, fave)
|
||||
}
|
||||
|
||||
return faves, nil
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) IsStatusFavedBy(ctx context.Context, statusID string, accountID string) (bool, error) {
|
||||
fave, err := s.GetStatusFave(ctx, accountID, statusID)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return false, err
|
||||
}
|
||||
return (fave != nil), nil
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) CountStatusFaves(ctx context.Context, statusID string) (int, error) {
|
||||
faveIDs, err := s.getStatusFaveIDs(ctx, statusID)
|
||||
return len(faveIDs), err
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) getStatusFaveIDs(ctx context.Context, statusID string) ([]string, error) {
|
||||
return s.state.Caches.GTS.StatusFaveIDs().Load(statusID, func() ([]string, error) {
|
||||
var faveIDs []string
|
||||
|
||||
// Status fave IDs not in cache, perform DB query!
|
||||
if err := s.db.
|
||||
NewSelect().
|
||||
Table("status_faves").
|
||||
Column("id").
|
||||
Where("? = ?", bun.Ident("status_id"), statusID).
|
||||
Scan(ctx, &faveIDs); err != nil {
|
||||
return nil, s.db.ProcessError(err)
|
||||
}
|
||||
|
||||
return faveIDs, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(3)
|
||||
errs = make(gtserror.MultiError, 0, 3)
|
||||
)
|
||||
|
||||
if statusFave.Account == nil {
|
||||
|
@ -171,7 +159,7 @@ func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmo
|
|||
statusFave.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status fave author: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status fave author: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,7 +170,7 @@ func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmo
|
|||
statusFave.TargetAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status fave target account: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status fave target account: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,15 +181,11 @@ func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmo
|
|||
statusFave.StatusID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status fave status: %w", err)
|
||||
errs.Append(fmt.Errorf("error populating status fave status: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
|
@ -215,46 +199,39 @@ func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusF
|
|||
}
|
||||
|
||||
func (s *statusFaveDB) DeleteStatusFaveByID(ctx context.Context, id string) error {
|
||||
var statusID string
|
||||
defer s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
|
||||
|
||||
// Perform DELETE on status fave,
|
||||
// returning the status ID it was for.
|
||||
if _, err := s.db.NewDelete().
|
||||
Table("status_faves").
|
||||
Where("id = ?", id).
|
||||
Returning("status_id").
|
||||
Exec(ctx, &statusID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// Not an issue, only due
|
||||
// to us doing a RETURNING.
|
||||
// Load fave into cache before attempting a delete,
|
||||
// as we need it cached in order to trigger the invalidate
|
||||
// callback. This in turn invalidates others.
|
||||
_, err := s.GetStatusFaveByID(gtscontext.SetBarebones(ctx), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
// not an issue.
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Finally delete fave from DB.
|
||||
_, err = s.db.NewDelete().
|
||||
Table("status_faves").
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Exec(ctx)
|
||||
return s.db.ProcessError(err)
|
||||
}
|
||||
|
||||
if statusID != "" {
|
||||
// Invalidate any cached status faves for this status.
|
||||
s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
|
||||
|
||||
// Invalidate any cached status fave IDs for this status.
|
||||
s.state.Caches.GTS.StatusFaveIDs().Invalidate(statusID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) error {
|
||||
if targetAccountID == "" && originAccountID == "" {
|
||||
return errors.New("DeleteStatusFaves: one of targetAccountID or originAccountID must be set")
|
||||
}
|
||||
|
||||
var statusIDs []string
|
||||
var faveIDs []string
|
||||
|
||||
// Prepare DELETE query returning
|
||||
// the deleted faves for status IDs.
|
||||
q := s.db.NewDelete().
|
||||
Table("status_faves").
|
||||
Returning("status_id")
|
||||
q := s.db.
|
||||
NewSelect().
|
||||
Column("id").
|
||||
Table("status_faves")
|
||||
|
||||
if targetAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("target_account_id"), targetAccountID)
|
||||
|
@ -264,46 +241,69 @@ func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID st
|
|||
q = q.Where("? = ?", bun.Ident("account_id"), originAccountID)
|
||||
}
|
||||
|
||||
// Execute query, store favourited status IDs.
|
||||
if _, err := q.Exec(ctx, &statusIDs); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// Not an issue, only due
|
||||
// to us doing a RETURNING.
|
||||
err = nil
|
||||
}
|
||||
if _, err := q.Exec(ctx, &faveIDs); err != nil {
|
||||
return s.db.ProcessError(err)
|
||||
}
|
||||
|
||||
// Collate (deduplicating) status IDs.
|
||||
statusIDs = collate(func(i int) string {
|
||||
return statusIDs[i]
|
||||
}, len(statusIDs))
|
||||
|
||||
for _, id := range statusIDs {
|
||||
// Invalidate any cached status faves for this status.
|
||||
defer func() {
|
||||
// Invalidate all IDs on return.
|
||||
for _, id := range faveIDs {
|
||||
s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
|
||||
}
|
||||
}()
|
||||
|
||||
// Invalidate any cached status fave IDs for this status.
|
||||
s.state.Caches.GTS.StatusFaveIDs().Invalidate(id)
|
||||
// Load all faves into cache, this *really* isn't great
|
||||
// but it is the only way we can ensure we invalidate all
|
||||
// related caches correctly (e.g. visibility).
|
||||
for _, id := range faveIDs {
|
||||
_, err := s.GetStatusFaveByID(ctx, id)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
// Finally delete all from DB.
|
||||
_, err := s.db.NewDelete().
|
||||
Table("status_faves").
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(faveIDs)).
|
||||
Exec(ctx)
|
||||
return s.db.ProcessError(err)
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) DeleteStatusFavesForStatus(ctx context.Context, statusID string) error {
|
||||
// Delete all status faves for status.
|
||||
if _, err := s.db.NewDelete().
|
||||
// Capture fave IDs in a RETURNING statement.
|
||||
var faveIDs []string
|
||||
|
||||
q := s.db.
|
||||
NewSelect().
|
||||
Column("id").
|
||||
Table("status_faves").
|
||||
Where("status_id = ?", statusID).
|
||||
Exec(ctx); err != nil {
|
||||
Where("? = ?", bun.Ident("status_id"), statusID)
|
||||
if _, err := q.Exec(ctx, &faveIDs); err != nil {
|
||||
return s.db.ProcessError(err)
|
||||
}
|
||||
|
||||
// Invalidate any cached status faves for this status.
|
||||
s.state.Caches.GTS.StatusFave().Invalidate("ID", statusID)
|
||||
|
||||
// Invalidate any cached status fave IDs for this status.
|
||||
s.state.Caches.GTS.StatusFaveIDs().Invalidate(statusID)
|
||||
|
||||
return nil
|
||||
defer func() {
|
||||
// Invalidate all IDs on return.
|
||||
for _, id := range faveIDs {
|
||||
s.state.Caches.GTS.StatusFave().Invalidate("ID", id)
|
||||
}
|
||||
}()
|
||||
|
||||
// Load all faves into cache, this *really* isn't great
|
||||
// but it is the only way we can ensure we invalidate all
|
||||
// related caches correctly (e.g. visibility).
|
||||
for _, id := range faveIDs {
|
||||
_, err := s.GetStatusFaveByID(ctx, id)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Finally delete all from DB.
|
||||
_, err := s.db.NewDelete().
|
||||
Table("status_faves").
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(faveIDs)).
|
||||
Exec(ctx)
|
||||
return s.db.ProcessError(err)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ type StatusFaveTestSuite struct {
|
|||
func (suite *StatusFaveTestSuite) TestGetStatusFaves() {
|
||||
testStatus := suite.testStatuses["admin_account_status_1"]
|
||||
|
||||
faves, err := suite.db.GetStatusFaves(context.Background(), testStatus.ID)
|
||||
faves, err := suite.db.GetStatusFavesForStatus(context.Background(), testStatus.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func (suite *StatusFaveTestSuite) TestGetStatusFaves() {
|
|||
func (suite *StatusFaveTestSuite) TestGetStatusFavesNone() {
|
||||
testStatus := suite.testStatuses["admin_account_status_4"]
|
||||
|
||||
faves, err := suite.db.GetStatusFaves(context.Background(), testStatus.ID)
|
||||
faves, err := suite.db.GetStatusFavesForStatus(context.Background(), testStatus.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
|
@ -41,10 +41,10 @@ type Media interface {
|
|||
// DeleteAttachment deletes the attachment with given ID from the database.
|
||||
DeleteAttachment(ctx context.Context, id string) error
|
||||
|
||||
// GetAttachments fetches media attachments up to a given max ID, and at most limit.
|
||||
// GetAttachments ...
|
||||
GetAttachments(ctx context.Context, maxID string, limit int) ([]*gtsmodel.MediaAttachment, error)
|
||||
|
||||
// GetRemoteAttachments fetches media attachments with a non-empty domain, up to a given max ID, and at most limit.
|
||||
// GetRemoteAttachments ...
|
||||
GetRemoteAttachments(ctx context.Context, maxID string, limit int) ([]*gtsmodel.MediaAttachment, error)
|
||||
|
||||
// GetCachedAttachmentsOlderThan gets limit n remote attachments (including avatars and headers) older than
|
||||
|
|
|
@ -34,9 +34,6 @@ type Status interface {
|
|||
// GetStatusByURL returns one status from the database, with no rel fields populated, only their linking ID / URIs
|
||||
GetStatusByURL(ctx context.Context, uri string) (*gtsmodel.Status, error)
|
||||
|
||||
// GetStatusBoost fetches the status whose boost_of_id column refers to boostOfID, authored by given account ID.
|
||||
GetStatusBoost(ctx context.Context, boostOfID string, byAccountID string) (*gtsmodel.Status, error)
|
||||
|
||||
// PopulateStatus ensures that all sub-models of a status are populated (e.g. mentions, attachments, etc).
|
||||
PopulateStatus(ctx context.Context, status *gtsmodel.Status) error
|
||||
|
||||
|
@ -49,27 +46,21 @@ type Status interface {
|
|||
// DeleteStatusByID deletes one status from the database.
|
||||
DeleteStatusByID(ctx context.Context, id string) error
|
||||
|
||||
// CountStatusReplies returns the amount of replies recorded for a status, or an error if something goes wrong
|
||||
CountStatusReplies(ctx context.Context, status *gtsmodel.Status) (int, error)
|
||||
|
||||
// CountStatusReblogs returns the amount of reblogs/boosts recorded for a status, or an error if something goes wrong
|
||||
CountStatusReblogs(ctx context.Context, status *gtsmodel.Status) (int, error)
|
||||
|
||||
// CountStatusFaves returns the amount of faves/likes recorded for a status, or an error if something goes wrong
|
||||
CountStatusFaves(ctx context.Context, status *gtsmodel.Status) (int, error)
|
||||
|
||||
// GetStatuses gets a slice of statuses corresponding to the given status IDs.
|
||||
GetStatusesByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetStatusesUsingEmoji fetches all status models using emoji with given ID stored in their 'emojis' column.
|
||||
GetStatusesUsingEmoji(ctx context.Context, emojiID string) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetStatusReplies returns the *direct* (i.e. in_reply_to_id column) replies to this status ID.
|
||||
GetStatusReplies(ctx context.Context, statusID string) ([]*gtsmodel.Status, error)
|
||||
|
||||
// CountStatusReplies returns the number of stored *direct* (i.e. in_reply_to_id column) replies to this status ID.
|
||||
CountStatusReplies(ctx context.Context, statusID string) (int, error)
|
||||
|
||||
// GetStatusBoosts returns all statuses whose boost_of_id column refer to given status ID.
|
||||
GetStatusBoosts(ctx context.Context, statusID string) ([]*gtsmodel.Status, error)
|
||||
|
||||
// CountStatusBoosts returns the number of stored boosts for status ID.
|
||||
CountStatusBoosts(ctx context.Context, statusID string) (int, error)
|
||||
|
||||
// IsStatusBoostedBy checks whether the given status ID is boosted by account ID.
|
||||
IsStatusBoostedBy(ctx context.Context, statusID string, accountID string) (bool, error)
|
||||
|
||||
// GetStatusParents gets the parent statuses of a given status.
|
||||
//
|
||||
// If onlyDirect is true, only the immediate parent will be returned.
|
||||
|
@ -80,9 +71,19 @@ type Status interface {
|
|||
// If onlyDirect is true, only the immediate children will be returned.
|
||||
GetStatusChildren(ctx context.Context, status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error)
|
||||
|
||||
// IsStatusFavedBy checks if a given status has been faved by a given account ID
|
||||
IsStatusFavedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
// IsStatusRebloggedBy checks if a given status has been reblogged/boosted by a given account ID
|
||||
IsStatusRebloggedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
// IsStatusMutedBy checks if a given status has been muted by a given account ID
|
||||
IsStatusMutedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
// IsStatusBookmarkedBy checks if a given status has been bookmarked by a given account ID
|
||||
IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
// GetStatusReblogs returns a slice of statuses that are a boost/reblog of the given status.
|
||||
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
||||
GetStatusReblogs(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.Status, error)
|
||||
}
|
||||
|
|
|
@ -24,15 +24,16 @@ import (
|
|||
)
|
||||
|
||||
type StatusFave interface {
|
||||
// GetStatusFaveByAccountID gets one status fave created by the given accountID, targeting the given statusID.
|
||||
// GetStatusFaveByAccountID gets one status fave created by the given
|
||||
// accountID, targeting the given statusID.
|
||||
GetStatusFave(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusFave, error)
|
||||
|
||||
// GetStatusFave returns one status fave with the given id.
|
||||
GetStatusFaveByID(ctx context.Context, id string) (*gtsmodel.StatusFave, error)
|
||||
|
||||
// GetStatusFaves returns a slice of faves/likes of the status with given ID.
|
||||
// GetStatusFaves returns a slice of faves/likes of the given status.
|
||||
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
||||
GetStatusFaves(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, error)
|
||||
GetStatusFavesForStatus(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, error)
|
||||
|
||||
// PopulateStatusFave ensures that all sub-models of a fave are populated (account, status, etc).
|
||||
PopulateStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) error
|
||||
|
@ -58,13 +59,8 @@ type StatusFave interface {
|
|||
// At least one parameter must not be an empty string.
|
||||
DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) error
|
||||
|
||||
// DeleteStatusFavesForStatus deletes all status faves that target the given status ID.
|
||||
// This is useful when a status has been deleted, and you need to clean up after it.
|
||||
// DeleteStatusFavesForStatus deletes all status faves that target the
|
||||
// given status ID. This is useful when a status has been deleted, and you need
|
||||
// to clean up after it.
|
||||
DeleteStatusFavesForStatus(ctx context.Context, statusID string) error
|
||||
|
||||
// CountStatusFaves returns the number of status favourites registered for status with ID.
|
||||
CountStatusFaves(ctx context.Context, statusID string) (int, error)
|
||||
|
||||
// IsStatusFavedBy returns whether the status with ID has been favourited by account with ID.
|
||||
IsStatusFavedBy(ctx context.Context, statusID string, accountID string) (bool, error)
|
||||
}
|
||||
|
|
|
@ -19,45 +19,26 @@ package gtserror
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MultiError allows encapsulating multiple
|
||||
// errors under a singular instance, which
|
||||
// is useful when you only want to log on
|
||||
// errors, not return early / bubble up.
|
||||
type MultiError []error
|
||||
// MultiError allows encapsulating multiple errors under a singular instance,
|
||||
// which is useful when you only want to log on errors, not return early / bubble up.
|
||||
type MultiError []string
|
||||
|
||||
// NewMultiError returns a *MultiError with
|
||||
// the capacity of its underlying error slice
|
||||
// set to the provided value.
|
||||
//
|
||||
// This capacity can be exceeded if necessary,
|
||||
// but it saves a teeny tiny bit of memory if
|
||||
// callers set it correctly.
|
||||
//
|
||||
// If you don't know in advance what the capacity
|
||||
// must be, just use new(MultiError) instead.
|
||||
func NewMultiError(capacity int) MultiError {
|
||||
return make([]error, 0, capacity)
|
||||
func (e *MultiError) Append(err error) {
|
||||
*e = append(*e, err.Error())
|
||||
}
|
||||
|
||||
// Append the given error to the MultiError.
|
||||
func (m *MultiError) Append(err error) {
|
||||
(*m) = append((*m), err)
|
||||
func (e *MultiError) Appendf(format string, args ...any) {
|
||||
*e = append(*e, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Append the given format string to the MultiError.
|
||||
//
|
||||
// It is valid to use %w in the format string
|
||||
// to wrap any other errors.
|
||||
func (m *MultiError) Appendf(format string, args ...any) {
|
||||
err := newfAt(3, format, args...)
|
||||
(*m) = append((*m), err)
|
||||
// Combine converts this multiError to a singular error instance, returning nil if empty.
|
||||
func (e MultiError) Combine() error {
|
||||
if len(e) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Combine the MultiError into a single error.
|
||||
//
|
||||
// Unwrap will work on the returned error as expected.
|
||||
func (m MultiError) Combine() error {
|
||||
return errors.Join(m...)
|
||||
return errors.New(`"` + strings.Join(e, `","`) + `"`)
|
||||
}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtserror_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
func TestMultiError(t *testing.T) {
|
||||
errs := gtserror.MultiError([]error{
|
||||
db.ErrNoEntries,
|
||||
errors.New("oopsie woopsie we did a fucky wucky etc"),
|
||||
})
|
||||
|
||||
errs.Appendf("appended + wrapped error: %w", db.ErrAlreadyExists)
|
||||
|
||||
err := errs.Combine()
|
||||
|
||||
if !errors.Is(err, db.ErrNoEntries) {
|
||||
t.Error("should be db.ErrNoEntries")
|
||||
}
|
||||
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
t.Error("should be db.ErrAlreadyExists")
|
||||
}
|
||||
|
||||
if errors.Is(err, db.ErrBusyTimeout) {
|
||||
t.Error("should not be db.ErrBusyTimeout")
|
||||
}
|
||||
|
||||
errString := err.Error()
|
||||
expected := `sql: no rows in result set
|
||||
oopsie woopsie we did a fucky wucky etc
|
||||
TestMultiError: appended + wrapped error: already exists`
|
||||
if errString != expected {
|
||||
t.Errorf("errString '%s' should be '%s'", errString, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiErrorEmpty(t *testing.T) {
|
||||
err := new(gtserror.MultiError).Combine()
|
||||
if err != nil {
|
||||
t.Errorf("should be nil")
|
||||
}
|
||||
}
|
|
@ -108,7 +108,7 @@ type Config struct {
|
|||
// - request logging
|
||||
type Client struct {
|
||||
client http.Client
|
||||
badHosts cache.TTLCache[string, struct{}]
|
||||
badHosts cache.Cache[string, struct{}]
|
||||
bodyMax int64
|
||||
}
|
||||
|
||||
|
@ -178,7 +178,7 @@ func New(cfg Config) *Client {
|
|||
}
|
||||
|
||||
// Initiate outgoing bad hosts lookup cache.
|
||||
c.badHosts = cache.NewTTL[string, struct{}](0, 1000, 0)
|
||||
c.badHosts = cache.New[string, struct{}](0, 1000, 0)
|
||||
c.badHosts.SetTTL(time.Hour, false)
|
||||
if !c.badHosts.Start(time.Minute) {
|
||||
log.Panic(nil, "failed to start transport controller cache")
|
||||
|
|
|
@ -330,7 +330,7 @@ statusLoop:
|
|||
})
|
||||
|
||||
// Look for any boosts of this status in DB.
|
||||
boosts, err := p.state.DB.GetStatusBoosts(ctx, status.ID)
|
||||
boosts, err := p.state.DB.GetStatusReblogs(ctx, status)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return gtserror.Newf("error fetching status reblogs for %s: %w", status.ID, err)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package processing
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
@ -41,13 +42,13 @@ import (
|
|||
func (p *Processor) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
// Ensure status fully populated; including account, mentions, etc.
|
||||
if err := p.state.DB.PopulateStatus(ctx, status); err != nil {
|
||||
return gtserror.Newf("error populating status with id %s: %w", status.ID, err)
|
||||
return fmt.Errorf("timelineAndNotifyStatus: error populating status with id %s: %w", status.ID, err)
|
||||
}
|
||||
|
||||
// Get local followers of the account that posted the status.
|
||||
follows, err := p.state.DB.GetAccountLocalFollowers(ctx, status.AccountID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting local followers for account id %s: %w", status.AccountID, err)
|
||||
return fmt.Errorf("timelineAndNotifyStatus: error getting local followers for account id %s: %w", status.AccountID, err)
|
||||
}
|
||||
|
||||
// If the poster is also local, add a fake entry for them
|
||||
|
@ -65,12 +66,12 @@ func (p *Processor) timelineAndNotifyStatus(ctx context.Context, status *gtsmode
|
|||
// This will also handle notifying any followers with notify
|
||||
// set to true on their follow.
|
||||
if err := p.timelineAndNotifyStatusForFollowers(ctx, status, follows); err != nil {
|
||||
return gtserror.Newf("error timelining status %s for followers: %w", status.ID, err)
|
||||
return fmt.Errorf("timelineAndNotifyStatus: error timelining status %s for followers: %w", status.ID, err)
|
||||
}
|
||||
|
||||
// Notify each local account that's mentioned by this status.
|
||||
if err := p.notifyStatusMentions(ctx, status); err != nil {
|
||||
return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
|
||||
return fmt.Errorf("timelineAndNotifyStatus: error notifying status mentions for status %s: %w", status.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -78,7 +79,7 @@ func (p *Processor) timelineAndNotifyStatus(ctx context.Context, status *gtsmode
|
|||
|
||||
func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, status *gtsmodel.Status, follows []*gtsmodel.Follow) error {
|
||||
var (
|
||||
errs = gtserror.NewMultiError(len(follows))
|
||||
errs = make(gtserror.MultiError, 0, len(follows))
|
||||
boost = status.BoostOfID != ""
|
||||
reply = status.InReplyToURI != ""
|
||||
)
|
||||
|
@ -99,7 +100,7 @@ func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, sta
|
|||
follow.ID,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
errs.Appendf("error list timelining status: %w", err)
|
||||
errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error list timelining status: %w", err))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -112,7 +113,7 @@ func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, sta
|
|||
status,
|
||||
stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
|
||||
); err != nil {
|
||||
errs.Appendf("error list timelining status: %w", err)
|
||||
errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error list timelining status: %w", err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +128,7 @@ func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, sta
|
|||
status,
|
||||
stream.TimelineHome,
|
||||
); err != nil {
|
||||
errs.Appendf("error home timelining status: %w", err)
|
||||
errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error home timelining status: %w", err))
|
||||
continue
|
||||
} else if !timelined {
|
||||
// Status wasn't added to home tomeline,
|
||||
|
@ -161,15 +162,11 @@ func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, sta
|
|||
status.AccountID,
|
||||
status.ID,
|
||||
); err != nil {
|
||||
errs.Appendf("error notifying account %s about new status: %w", follow.AccountID, err)
|
||||
errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error notifying account %s about new status: %w", follow.AccountID, err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
// timelineStatus uses the provided ingest function to put the given
|
||||
|
@ -188,7 +185,7 @@ func (p *Processor) timelineStatus(
|
|||
// Make sure the status is timelineable.
|
||||
// This works for both home and list timelines.
|
||||
if timelineable, err := p.filter.StatusHomeTimelineable(ctx, account, status); err != nil {
|
||||
err = gtserror.Newf("error getting timelineability for status for timeline with id %s: %w", account.ID, err)
|
||||
err = fmt.Errorf("timelineStatusForAccount: error getting timelineability for status for timeline with id %s: %w", account.ID, err)
|
||||
return false, err
|
||||
} else if !timelineable {
|
||||
// Nothing to do.
|
||||
|
@ -197,7 +194,7 @@ func (p *Processor) timelineStatus(
|
|||
|
||||
// Ingest status into given timeline using provided function.
|
||||
if inserted, err := ingest(ctx, timelineID, status); err != nil {
|
||||
err = gtserror.Newf("error ingesting status %s: %w", status.ID, err)
|
||||
err = fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %w", status.ID, err)
|
||||
return false, err
|
||||
} else if !inserted {
|
||||
// Nothing more to do.
|
||||
|
@ -207,12 +204,12 @@ func (p *Processor) timelineStatus(
|
|||
// The status was inserted so stream it to the user.
|
||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, account)
|
||||
if err != nil {
|
||||
err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
|
||||
err = fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %w", status.ID, err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
if err := p.stream.Update(apiStatus, account, []string{streamType}); err != nil {
|
||||
err = gtserror.Newf("error streaming update for status %s: %w", status.ID, err)
|
||||
err = fmt.Errorf("timelineStatusForAccount: error streaming update for status %s: %w", status.ID, err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
|
@ -220,7 +217,7 @@ func (p *Processor) timelineStatus(
|
|||
}
|
||||
|
||||
func (p *Processor) notifyStatusMentions(ctx context.Context, status *gtsmodel.Status) error {
|
||||
errs := gtserror.NewMultiError(len(status.Mentions))
|
||||
errs := make(gtserror.MultiError, 0, len(status.Mentions))
|
||||
|
||||
for _, m := range status.Mentions {
|
||||
if err := p.notify(
|
||||
|
@ -234,11 +231,7 @@ func (p *Processor) notifyStatusMentions(ctx context.Context, status *gtsmodel.S
|
|||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (p *Processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error {
|
||||
|
@ -262,13 +255,13 @@ func (p *Processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, t
|
|||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
// Proper error while checking.
|
||||
return gtserror.Newf("db error checking for previous follow request notification: %w", err)
|
||||
return fmt.Errorf("notifyFollow: db error checking for previous follow request notification: %w", err)
|
||||
}
|
||||
|
||||
if prevNotif != nil {
|
||||
// Previous notification existed, delete.
|
||||
if err := p.state.DB.DeleteNotificationByID(ctx, prevNotif.ID); err != nil {
|
||||
return gtserror.Newf("db error removing previous follow request notification %s: %w", prevNotif.ID, err)
|
||||
return fmt.Errorf("notifyFollow: db error removing previous follow request notification %s: %w", prevNotif.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,7 +319,7 @@ func (p *Processor) notify(
|
|||
) error {
|
||||
targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting target account %s: %w", targetAccountID, err)
|
||||
return fmt.Errorf("notify: error getting target account %s: %w", targetAccountID, err)
|
||||
}
|
||||
|
||||
if !targetAccount.IsLocal() {
|
||||
|
@ -347,7 +340,7 @@ func (p *Processor) notify(
|
|||
return nil
|
||||
} else if !errors.Is(err, db.ErrNoEntries) {
|
||||
// Real error.
|
||||
return gtserror.Newf("error checking existence of notification: %w", err)
|
||||
return fmt.Errorf("notify: error checking existence of notification: %w", err)
|
||||
}
|
||||
|
||||
// Notification doesn't yet exist, so
|
||||
|
@ -361,17 +354,17 @@ func (p *Processor) notify(
|
|||
}
|
||||
|
||||
if err := p.state.DB.PutNotification(ctx, notif); err != nil {
|
||||
return gtserror.Newf("error putting notification in database: %w", err)
|
||||
return fmt.Errorf("notify: error putting notification in database: %w", err)
|
||||
}
|
||||
|
||||
// Stream notification to the user.
|
||||
apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error converting notification to api representation: %w", err)
|
||||
return fmt.Errorf("notify: error converting notification to api representation: %w", err)
|
||||
}
|
||||
|
||||
if err := p.stream.Notify(apiNotif, targetAccount); err != nil {
|
||||
return gtserror.Newf("error streaming notification to account: %w", err)
|
||||
return fmt.Errorf("notify: error streaming notification to account: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -380,8 +373,6 @@ func (p *Processor) notify(
|
|||
// wipeStatus contains common logic used to totally delete a status
|
||||
// + all its attachments, notifications, boosts, and timeline entries.
|
||||
func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Status, deleteAttachments bool) error {
|
||||
var errs gtserror.MultiError
|
||||
|
||||
// either delete all attachments for this status, or simply
|
||||
// unattach all attachments for this status, so they'll be
|
||||
// cleaned later by a separate process; reason to unattach rather
|
||||
|
@ -391,14 +382,14 @@ func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta
|
|||
// todo: p.state.DB.DeleteAttachmentsForStatus
|
||||
for _, a := range statusToDelete.AttachmentIDs {
|
||||
if err := p.media.Delete(ctx, a); err != nil {
|
||||
errs.Appendf("error deleting media: %w", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// todo: p.state.DB.UnattachAttachmentsForStatus
|
||||
for _, a := range statusToDelete.AttachmentIDs {
|
||||
if _, err := p.media.Unattach(ctx, statusToDelete.Account, a); err != nil {
|
||||
errs.Appendf("error unattaching media: %w", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -407,55 +398,44 @@ func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta
|
|||
// todo: p.state.DB.DeleteMentionsForStatus
|
||||
for _, id := range statusToDelete.MentionIDs {
|
||||
if err := p.state.DB.DeleteMentionByID(ctx, id); err != nil {
|
||||
errs.Appendf("error deleting status mention: %w", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete all notification entries generated by this status
|
||||
if err := p.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status notifications: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// delete all bookmarks that point to this status
|
||||
if err := p.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status bookmarks: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// delete all faves of this status
|
||||
if err := p.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status faves: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// delete all boosts for this status + remove them from timelines
|
||||
boosts, err := p.state.DB.GetStatusBoosts(
|
||||
// we MUST set a barebones context here,
|
||||
// as depending on where it came from the
|
||||
// original BoostOf may already be gone.
|
||||
gtscontext.SetBarebones(ctx),
|
||||
statusToDelete.ID)
|
||||
if err != nil {
|
||||
errs.Appendf("error fetching status boosts: %w", err)
|
||||
}
|
||||
if boosts, err := p.state.DB.GetStatusReblogs(ctx, statusToDelete); err == nil {
|
||||
for _, b := range boosts {
|
||||
if err := p.deleteStatusFromTimelines(ctx, b.ID); err != nil {
|
||||
errs.Appendf("error deleting boost from timelines: %w", err)
|
||||
return err
|
||||
}
|
||||
if err := p.state.DB.DeleteStatusByID(ctx, b.ID); err != nil {
|
||||
errs.Appendf("error deleting boost: %w", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete this status from any and all timelines
|
||||
if err := p.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status from timelines: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// finally, delete the status itself
|
||||
if err := p.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status: %w", err)
|
||||
}
|
||||
|
||||
return errs.Combine()
|
||||
// delete the status itself
|
||||
return p.state.DB.DeleteStatusByID(ctx, statusToDelete.ID)
|
||||
}
|
||||
|
||||
// deleteStatusFromTimelines completely removes the given status from all timelines.
|
||||
|
@ -499,7 +479,7 @@ func (p *Processor) invalidateStatusFromTimelines(ctx context.Context, statusID
|
|||
func (p *Processor) emailReport(ctx context.Context, report *gtsmodel.Report) error {
|
||||
instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting instance: %w", err)
|
||||
return fmt.Errorf("emailReport: error getting instance: %w", err)
|
||||
}
|
||||
|
||||
toAddresses, err := p.state.DB.GetInstanceModeratorAddresses(ctx)
|
||||
|
@ -508,20 +488,20 @@ func (p *Processor) emailReport(ctx context.Context, report *gtsmodel.Report) er
|
|||
// No registered moderator addresses.
|
||||
return nil
|
||||
}
|
||||
return gtserror.Newf("error getting instance moderator addresses: %w", err)
|
||||
return fmt.Errorf("emailReport: error getting instance moderator addresses: %w", err)
|
||||
}
|
||||
|
||||
if report.Account == nil {
|
||||
report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting report account: %w", err)
|
||||
return fmt.Errorf("emailReport: error getting report account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if report.TargetAccount == nil {
|
||||
report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting report target account: %w", err)
|
||||
return fmt.Errorf("emailReport: error getting report target account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -534,7 +514,7 @@ func (p *Processor) emailReport(ctx context.Context, report *gtsmodel.Report) er
|
|||
}
|
||||
|
||||
if err := p.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil {
|
||||
return gtserror.Newf("error emailing instance moderators: %w", err)
|
||||
return fmt.Errorf("emailReport: error emailing instance moderators: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -543,7 +523,7 @@ func (p *Processor) emailReport(ctx context.Context, report *gtsmodel.Report) er
|
|||
func (p *Processor) emailReportClosed(ctx context.Context, report *gtsmodel.Report) error {
|
||||
user, err := p.state.DB.GetUserByAccountID(ctx, report.Account.ID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("db error getting user: %w", err)
|
||||
return fmt.Errorf("emailReportClosed: db error getting user: %w", err)
|
||||
}
|
||||
|
||||
if user.ConfirmedAt.IsZero() || !*user.Approved || *user.Disabled || user.Email == "" {
|
||||
|
@ -557,20 +537,20 @@ func (p *Processor) emailReportClosed(ctx context.Context, report *gtsmodel.Repo
|
|||
|
||||
instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
|
||||
if err != nil {
|
||||
return gtserror.Newf("db error getting instance: %w", err)
|
||||
return fmt.Errorf("emailReportClosed: db error getting instance: %w", err)
|
||||
}
|
||||
|
||||
if report.Account == nil {
|
||||
report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting report account: %w", err)
|
||||
return fmt.Errorf("emailReportClosed: error getting report account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if report.TargetAccount == nil {
|
||||
report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting report target account: %w", err)
|
||||
return fmt.Errorf("emailReportClosed: error getting report target account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,13 +49,6 @@ func (p *Processor) Accounts(
|
|||
resolve bool,
|
||||
following bool,
|
||||
) ([]*apimodel.Account, gtserror.WithCode) {
|
||||
// Don't include instance accounts in this search.
|
||||
//
|
||||
// We don't want someone to start typing '@mastodon'
|
||||
// and then get a million instance service accounts
|
||||
// in their search results.
|
||||
const includeInstanceAccounts = false
|
||||
|
||||
var (
|
||||
foundAccounts = make([]*gtsmodel.Account, 0, limit)
|
||||
appendAccount = func(foundAccount *gtsmodel.Account) { foundAccounts = append(foundAccounts, foundAccount) }
|
||||
|
@ -90,12 +83,7 @@ func (p *Processor) Accounts(
|
|||
// if caller supplied an offset greater than 0, return
|
||||
// nothing as though there were no additional results.
|
||||
if offset > 0 {
|
||||
return p.packageAccounts(
|
||||
ctx,
|
||||
requestingAccount,
|
||||
foundAccounts,
|
||||
includeInstanceAccounts,
|
||||
)
|
||||
return p.packageAccounts(ctx, requestingAccount, foundAccounts)
|
||||
}
|
||||
|
||||
// Return all accounts we can find that match the
|
||||
|
@ -118,10 +106,5 @@ func (p *Processor) Accounts(
|
|||
}
|
||||
|
||||
// Return whatever we got (if anything).
|
||||
return p.packageAccounts(
|
||||
ctx,
|
||||
requestingAccount,
|
||||
foundAccounts,
|
||||
includeInstanceAccounts,
|
||||
)
|
||||
return p.packageAccounts(ctx, requestingAccount, foundAccounts)
|
||||
}
|
||||
|
|
|
@ -70,13 +70,6 @@ func (p *Processor) Get(
|
|||
queryType = strings.TrimSpace(strings.ToLower(req.QueryType)) // Trim trailing/leading whitespace; convert to lowercase.
|
||||
resolve = req.Resolve
|
||||
following = req.Following
|
||||
|
||||
// Include instance accounts in the first
|
||||
// parts of this search. This will be
|
||||
// changed to 'false' when doing text
|
||||
// search in the database in the latter
|
||||
// parts of this function.
|
||||
includeInstanceAccounts = true
|
||||
)
|
||||
|
||||
// Validate query.
|
||||
|
@ -116,12 +109,7 @@ func (p *Processor) Get(
|
|||
// supply an offset greater than 0, return nothing as
|
||||
// though there were no additional results.
|
||||
if req.Offset > 0 {
|
||||
return p.packageSearchResult(
|
||||
ctx,
|
||||
account,
|
||||
nil, nil, nil, // No results.
|
||||
req.APIv1, includeInstanceAccounts,
|
||||
)
|
||||
return p.packageSearchResult(ctx, account, nil, nil, nil, req.APIv1)
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -179,7 +167,6 @@ func (p *Processor) Get(
|
|||
foundStatuses,
|
||||
foundTags,
|
||||
req.APIv1,
|
||||
includeInstanceAccounts,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +196,6 @@ func (p *Processor) Get(
|
|||
foundStatuses,
|
||||
foundTags,
|
||||
req.APIv1,
|
||||
includeInstanceAccounts,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -250,20 +236,11 @@ func (p *Processor) Get(
|
|||
foundStatuses,
|
||||
foundTags,
|
||||
req.APIv1,
|
||||
includeInstanceAccounts,
|
||||
)
|
||||
}
|
||||
|
||||
// As a last resort, search for accounts and
|
||||
// statuses using the query as arbitrary text.
|
||||
//
|
||||
// At this point we no longer want to include
|
||||
// instance accounts in the results, since searching
|
||||
// for something like 'mastodon', for example, will
|
||||
// include a million instance/service accounts that
|
||||
// have 'mastodon' in the domain, and therefore in
|
||||
// the username, making the search results useless.
|
||||
includeInstanceAccounts = false
|
||||
if err := p.byText(
|
||||
ctx,
|
||||
account,
|
||||
|
@ -290,7 +267,6 @@ func (p *Processor) Get(
|
|||
foundStatuses,
|
||||
foundTags,
|
||||
req.APIv1,
|
||||
includeInstanceAccounts,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -44,13 +44,6 @@ func (p *Processor) Lookup(
|
|||
requestingAccount *gtsmodel.Account,
|
||||
query string,
|
||||
) (*apimodel.Account, gtserror.WithCode) {
|
||||
// Include instance accounts in this search.
|
||||
//
|
||||
// Lookup is for one specific account so we
|
||||
// can't return loads of instance accounts by
|
||||
// accident.
|
||||
const includeInstanceAccounts = true
|
||||
|
||||
// Validate query.
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
|
@ -103,12 +96,7 @@ func (p *Processor) Lookup(
|
|||
// using the packageAccounts function to return it. This
|
||||
// may cause the account to be filtered out if it's not
|
||||
// visible to the caller, so anticipate this.
|
||||
accounts, errWithCode := p.packageAccounts(
|
||||
ctx,
|
||||
requestingAccount,
|
||||
[]*gtsmodel.Account{account},
|
||||
includeInstanceAccounts,
|
||||
)
|
||||
accounts, errWithCode := p.packageAccounts(ctx, requestingAccount, []*gtsmodel.Account{account})
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
|
|
@ -48,12 +48,11 @@ func (p *Processor) packageAccounts(
|
|||
ctx context.Context,
|
||||
requestingAccount *gtsmodel.Account,
|
||||
accounts []*gtsmodel.Account,
|
||||
includeInstanceAccounts bool,
|
||||
) ([]*apimodel.Account, gtserror.WithCode) {
|
||||
apiAccounts := make([]*apimodel.Account, 0, len(accounts))
|
||||
|
||||
for _, account := range accounts {
|
||||
if !includeInstanceAccounts && account.IsInstance() {
|
||||
if account.IsInstance() {
|
||||
// No need to show instance accounts.
|
||||
continue
|
||||
}
|
||||
|
@ -170,9 +169,8 @@ func (p *Processor) packageSearchResult(
|
|||
statuses []*gtsmodel.Status,
|
||||
tags []*gtsmodel.Tag,
|
||||
v1 bool,
|
||||
includeInstanceAccounts bool,
|
||||
) (*apimodel.SearchResult, gtserror.WithCode) {
|
||||
apiAccounts, errWithCode := p.packageAccounts(ctx, requestingAccount, accounts, includeInstanceAccounts)
|
||||
apiAccounts, errWithCode := p.packageAccounts(ctx, requestingAccount, accounts)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
|
|
@ -106,24 +106,47 @@ func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel
|
|||
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
||||
}
|
||||
|
||||
// Check whether the requesting account has boosted the given status ID.
|
||||
boost, err := p.state.DB.GetStatusBoost(ctx, targetStatusID, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error checking status boost %s: %w", targetStatusID, err))
|
||||
// check if we actually have a boost for this status
|
||||
var toUnboost bool
|
||||
|
||||
gtsBoost := >smodel.Status{}
|
||||
where := []db.Where{
|
||||
{
|
||||
Key: "boost_of_id",
|
||||
Value: targetStatusID,
|
||||
},
|
||||
{
|
||||
Key: "account_id",
|
||||
Value: requestingAccount.ID,
|
||||
},
|
||||
}
|
||||
err = p.state.DB.GetWhere(ctx, where, gtsBoost)
|
||||
if err == nil {
|
||||
// we have a boost
|
||||
toUnboost = true
|
||||
}
|
||||
|
||||
if boost != nil {
|
||||
if err != nil {
|
||||
// something went wrong in the db finding the boost
|
||||
if err != db.ErrNoEntries {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
|
||||
}
|
||||
// we just don't have a boost
|
||||
toUnboost = false
|
||||
}
|
||||
|
||||
if toUnboost {
|
||||
// pin some stuff onto the boost while we have it out of the db
|
||||
boost.Account = requestingAccount
|
||||
boost.BoostOf = targetStatus
|
||||
boost.BoostOfAccount = targetStatus.Account
|
||||
boost.BoostOf.Account = targetStatus.Account
|
||||
gtsBoost.Account = requestingAccount
|
||||
gtsBoost.BoostOf = targetStatus
|
||||
gtsBoost.BoostOfAccount = targetStatus.Account
|
||||
gtsBoost.BoostOf.Account = targetStatus.Account
|
||||
|
||||
// send it back to the processor for async processing
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityAnnounce,
|
||||
APActivityType: ap.ActivityUndo,
|
||||
GTSModel: boost,
|
||||
GTSModel: gtsBoost,
|
||||
OriginAccount: requestingAccount,
|
||||
TargetAccount: targetStatus.Account,
|
||||
})
|
||||
|
@ -166,15 +189,15 @@ func (p *Processor) StatusBoostedBy(ctx context.Context, requestingAccount *gtsm
|
|||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
statusBoosts, err := p.state.DB.GetStatusBoosts(ctx, targetStatus.ID)
|
||||
statusReblogs, err := p.state.DB.GetStatusReblogs(ctx, targetStatus)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("BoostedBy: error seeing who boosted status: %s", err)
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
// filter account IDs so the user doesn't see accounts they blocked or which blocked them
|
||||
accountIDs := make([]string, 0, len(statusBoosts))
|
||||
for _, s := range statusBoosts {
|
||||
accountIDs := make([]string, 0, len(statusReblogs))
|
||||
for _, s := range statusReblogs {
|
||||
blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, s.AccountID)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("BoostedBy: error checking blocks: %s", err)
|
||||
|
|
|
@ -112,7 +112,7 @@ func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Acc
|
|||
return nil, errWithCode
|
||||
}
|
||||
|
||||
statusFaves, err := p.state.DB.GetStatusFaves(ctx, targetStatus.ID)
|
||||
statusFaves, err := p.state.DB.GetStatusFavesForStatus(ctx, targetStatus.ID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("FavedBy: error seeing who faved status: %s", err))
|
||||
}
|
||||
|
|
|
@ -181,7 +181,6 @@ func New(ctx context.Context) (Router, error) {
|
|||
// create the actual engine here -- this is the core request routing handler for gts
|
||||
engine := gin.New()
|
||||
engine.MaxMultipartMemory = maxMultipartMemory
|
||||
engine.HandleMethodNotAllowed = true
|
||||
|
||||
// set up IP forwarding via x-forward-* headers.
|
||||
trustedProxies := config.GetTrustedProxies()
|
||||
|
|
|
@ -190,18 +190,18 @@ func (m *manager) GetOldestIndexedID(ctx context.Context, timelineID string) str
|
|||
}
|
||||
|
||||
func (m *manager) WipeItemFromAllTimelines(ctx context.Context, itemID string) error {
|
||||
errs := new(gtserror.MultiError)
|
||||
errors := gtserror.MultiError{}
|
||||
|
||||
m.timelines.Range(func(_ any, v any) bool {
|
||||
if _, err := v.(Timeline).Remove(ctx, itemID); err != nil {
|
||||
errs.Append(err)
|
||||
errors.Append(err)
|
||||
}
|
||||
|
||||
return true // always continue range
|
||||
})
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("error(s) wiping status %s: %w", itemID, errs.Combine())
|
||||
if len(errors) > 0 {
|
||||
return gtserror.Newf("error(s) wiping status %s: %w", itemID, errors.Combine())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -213,21 +213,21 @@ func (m *manager) WipeItemsFromAccountID(ctx context.Context, timelineID string,
|
|||
}
|
||||
|
||||
func (m *manager) UnprepareItemFromAllTimelines(ctx context.Context, itemID string) error {
|
||||
errs := new(gtserror.MultiError)
|
||||
errors := gtserror.MultiError{}
|
||||
|
||||
// Work through all timelines held by this
|
||||
// manager, and call Unprepare for each.
|
||||
m.timelines.Range(func(_ any, v any) bool {
|
||||
// nolint:forcetypeassert
|
||||
if err := v.(Timeline).Unprepare(ctx, itemID); err != nil {
|
||||
errs.Append(err)
|
||||
errors.Append(err)
|
||||
}
|
||||
|
||||
return true // always continue range
|
||||
})
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("error(s) unpreparing status %s: %w", itemID, errs.Combine())
|
||||
if len(errors) > 0 {
|
||||
return gtserror.Newf("error(s) unpreparing status %s: %w", itemID, errors.Combine())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -50,7 +50,7 @@ type controller struct {
|
|||
fedDB federatingdb.DB
|
||||
clock pub.Clock
|
||||
client httpclient.SigningClient
|
||||
trspCache cache.TTLCache[string, *transport]
|
||||
trspCache cache.Cache[string, *transport]
|
||||
userAgent string
|
||||
senders int // no. concurrent batch delivery routines.
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ func NewController(state *state.State, federatingDB federatingdb.DB, clock pub.C
|
|||
fedDB: federatingDB,
|
||||
clock: clock,
|
||||
client: client,
|
||||
trspCache: cache.NewTTL[string, *transport](0, 100, 0),
|
||||
trspCache: cache.New[string, *transport](0, 100, 0),
|
||||
userAgent: fmt.Sprintf("%s (+%s://%s) gotosocial/%s", applicationName, proto, host, version),
|
||||
senders: senders,
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
|
@ -631,10 +630,8 @@ func (c *converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno
|
|||
// Extract published time for the boost.
|
||||
published, err := ap.ExtractPublished(announceable)
|
||||
if err != nil {
|
||||
// If not available, use the current time.
|
||||
published = time.Now().UTC()
|
||||
//err = gtserror.Newf("error extracting published: %w", err)
|
||||
//return nil, isNew, err
|
||||
err = gtserror.Newf("error extracting published: %w", err)
|
||||
return nil, isNew, err
|
||||
}
|
||||
status.CreatedAt = published
|
||||
status.UpdatedAt = published
|
||||
|
|
|
@ -600,17 +600,17 @@ func (c *converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r
|
|||
return nil, fmt.Errorf("error converting status author: %w", err)
|
||||
}
|
||||
|
||||
repliesCount, err := c.db.CountStatusReplies(ctx, s.ID)
|
||||
repliesCount, err := c.db.CountStatusReplies(ctx, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error counting replies: %w", err)
|
||||
}
|
||||
|
||||
reblogsCount, err := c.db.CountStatusBoosts(ctx, s.ID)
|
||||
reblogsCount, err := c.db.CountStatusReblogs(ctx, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error counting reblogs: %w", err)
|
||||
}
|
||||
|
||||
favesCount, err := c.db.CountStatusFaves(ctx, s.ID)
|
||||
favesCount, err := c.db.CountStatusFaves(ctx, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error counting faves: %w", err)
|
||||
}
|
||||
|
|
|
@ -40,13 +40,13 @@ func (c *converter) interactionsWithStatusForAccount(ctx context.Context, s *gts
|
|||
si := &statusInteractions{}
|
||||
|
||||
if requestingAccount != nil {
|
||||
faved, err := c.db.IsStatusFavedBy(ctx, s.ID, requestingAccount.ID)
|
||||
faved, err := c.db.IsStatusFavedBy(ctx, s, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking if requesting account has faved status: %s", err)
|
||||
}
|
||||
si.Faved = faved
|
||||
|
||||
reblogged, err := c.db.IsStatusBoostedBy(ctx, s.ID, requestingAccount.ID)
|
||||
reblogged, err := c.db.IsStatusRebloggedBy(ctx, s, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err)
|
||||
}
|
||||
|
|
|
@ -19,10 +19,10 @@ package visibility
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ func (f *Filter) isAccountVisibleTo(ctx context.Context, requester *gtsmodel.Acc
|
|||
// Check whether target account is visible to anyone.
|
||||
visible, err := f.isAccountVisible(ctx, account)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("error checking account %s visibility: %w", account.ID, err)
|
||||
return false, fmt.Errorf("isAccountVisibleTo: error checking account %s visibility: %w", account.ID, err)
|
||||
}
|
||||
|
||||
if !visible {
|
||||
|
@ -83,7 +83,7 @@ func (f *Filter) isAccountVisibleTo(ctx context.Context, requester *gtsmodel.Acc
|
|||
// If requester is not visible, they cannot *see* either.
|
||||
visible, err = f.isAccountVisible(ctx, requester)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("error checking account %s visibility: %w", account.ID, err)
|
||||
return false, fmt.Errorf("isAccountVisibleTo: error checking account %s visibility: %w", account.ID, err)
|
||||
}
|
||||
|
||||
if !visible {
|
||||
|
@ -97,7 +97,7 @@ func (f *Filter) isAccountVisibleTo(ctx context.Context, requester *gtsmodel.Acc
|
|||
account.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("error checking account blocks: %w", err)
|
||||
return false, fmt.Errorf("isAccountVisibleTo: error checking account blocks: %w", err)
|
||||
}
|
||||
|
||||
if blocked {
|
||||
|
@ -121,7 +121,6 @@ func (f *Filter) isAccountVisible(ctx context.Context, account *gtsmodel.Account
|
|||
// Fetch the local user model for this account.
|
||||
user, err := f.state.DB.GetUserByAccountID(ctx, account.ID)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("db error getting user for account %s: %w", account.ID, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
|
|
@ -29,8 +29,8 @@ import (
|
|||
"codeberg.org/gruf/go-cache/v3"
|
||||
)
|
||||
|
||||
func newETagCache() cache.TTLCache[string, eTagCacheEntry] {
|
||||
eTagCache := cache.NewTTL[string, eTagCacheEntry](0, 1000, 0)
|
||||
func newETagCache() cache.Cache[string, eTagCacheEntry] {
|
||||
eTagCache := cache.New[string, eTagCacheEntry](0, 1000, 0)
|
||||
eTagCache.SetTTL(time.Hour, false)
|
||||
if !eTagCache.Start(time.Minute) {
|
||||
log.Panic(nil, "could not start eTagCache")
|
||||
|
|
|
@ -18,34 +18,86 @@ EXPECT=$(cat << "EOF"
|
|||
"application-name": "gts",
|
||||
"bind-address": "127.0.0.1",
|
||||
"cache": {
|
||||
"account-mem-ratio": 18,
|
||||
"account-note-mem-ratio": 0.1,
|
||||
"block-mem-ratio": 3,
|
||||
"boost-of-ids-mem-ratio": 3,
|
||||
"emoji-category-mem-ratio": 0.1,
|
||||
"emoji-mem-ratio": 3,
|
||||
"follow-ids-mem-ratio": 4,
|
||||
"follow-mem-ratio": 4,
|
||||
"follow-request-ids-mem-ratio": 2,
|
||||
"follow-request-mem-ratio": 2,
|
||||
"in-reply-to-ids-mem-ratio": 3,
|
||||
"instance-mem-ratio": 1,
|
||||
"list-entry-mem-ratio": 3,
|
||||
"list-mem-ratio": 3,
|
||||
"marker-mem-ratio": 0.5,
|
||||
"media-mem-ratio": 4,
|
||||
"memory-target": 209715200,
|
||||
"mention-mem-ratio": 5,
|
||||
"notification-mem-ratio": 5,
|
||||
"report-mem-ratio": 1,
|
||||
"status-fave-ids-mem-ratio": 3,
|
||||
"status-fave-mem-ratio": 5,
|
||||
"status-mem-ratio": 18,
|
||||
"tag-mem-ratio": 3,
|
||||
"tombstone-mem-ratio": 2,
|
||||
"user-mem-ratio": 0.1,
|
||||
"visibility-mem-ratio": 2,
|
||||
"webfinger-mem-ratio": 0.1
|
||||
"gts": {
|
||||
"account-max-size": 99,
|
||||
"account-note-max-size": 1000,
|
||||
"account-note-sweep-freq": 60000000000,
|
||||
"account-note-ttl": 1800000000000,
|
||||
"account-sweep-freq": 1000000000,
|
||||
"account-ttl": 10800000000000,
|
||||
"block-ids-max-size": 500,
|
||||
"block-ids-sweep-freq": 60000000000,
|
||||
"block-ids-ttl": 1800000000000,
|
||||
"block-max-size": 1000,
|
||||
"block-sweep-freq": 60000000000,
|
||||
"block-ttl": 1800000000000,
|
||||
"domain-block-max-size": 2000,
|
||||
"domain-block-sweep-freq": 60000000000,
|
||||
"domain-block-ttl": 86400000000000,
|
||||
"emoji-category-max-size": 100,
|
||||
"emoji-category-sweep-freq": 60000000000,
|
||||
"emoji-category-ttl": 1800000000000,
|
||||
"emoji-max-size": 2000,
|
||||
"emoji-sweep-freq": 60000000000,
|
||||
"emoji-ttl": 1800000000000,
|
||||
"follow-ids-max-size": 500,
|
||||
"follow-ids-sweep-freq": 60000000000,
|
||||
"follow-ids-ttl": 1800000000000,
|
||||
"follow-max-size": 2000,
|
||||
"follow-request-ids-max-size": 500,
|
||||
"follow-request-ids-sweep-freq": 60000000000,
|
||||
"follow-request-ids-ttl": 1800000000000,
|
||||
"follow-request-max-size": 2000,
|
||||
"follow-request-sweep-freq": 60000000000,
|
||||
"follow-request-ttl": 1800000000000,
|
||||
"follow-sweep-freq": 60000000000,
|
||||
"follow-ttl": 1800000000000,
|
||||
"instance-max-size": 2000,
|
||||
"instance-sweep-freq": 60000000000,
|
||||
"instance-ttl": 1800000000000,
|
||||
"list-entry-max-size": 2000,
|
||||
"list-entry-sweep-freq": 60000000000,
|
||||
"list-entry-ttl": 1800000000000,
|
||||
"list-max-size": 2000,
|
||||
"list-sweep-freq": 60000000000,
|
||||
"list-ttl": 1800000000000,
|
||||
"marker-max-size": 2000,
|
||||
"marker-sweep-freq": 60000000000,
|
||||
"marker-ttl": 21600000000000,
|
||||
"media-max-size": 1000,
|
||||
"media-sweep-freq": 60000000000,
|
||||
"media-ttl": 1800000000000,
|
||||
"mention-max-size": 2000,
|
||||
"mention-sweep-freq": 60000000000,
|
||||
"mention-ttl": 1800000000000,
|
||||
"notification-max-size": 1000,
|
||||
"notification-sweep-freq": 60000000000,
|
||||
"notification-ttl": 1800000000000,
|
||||
"report-max-size": 100,
|
||||
"report-sweep-freq": 60000000000,
|
||||
"report-ttl": 1800000000000,
|
||||
"status-fave-max-size": 2000,
|
||||
"status-fave-sweep-freq": 60000000000,
|
||||
"status-fave-ttl": 1800000000000,
|
||||
"status-max-size": 2000,
|
||||
"status-sweep-freq": 60000000000,
|
||||
"status-ttl": 1800000000000,
|
||||
"tag-max-size": 2000,
|
||||
"tag-sweep-freq": 60000000000,
|
||||
"tag-ttl": 1800000000000,
|
||||
"tombstone-max-size": 500,
|
||||
"tombstone-sweep-freq": 60000000000,
|
||||
"tombstone-ttl": 1800000000000,
|
||||
"user-max-size": 500,
|
||||
"user-sweep-freq": 60000000000,
|
||||
"user-ttl": 1800000000000,
|
||||
"webfinger-max-size": 250,
|
||||
"webfinger-sweep-freq": 900000000000,
|
||||
"webfinger-ttl": 86400000000000
|
||||
},
|
||||
"visibility-max-size": 2000,
|
||||
"visibility-sweep-freq": 60000000000,
|
||||
"visibility-ttl": 1800000000000
|
||||
},
|
||||
"config-path": "internal/config/testdata/test.yaml",
|
||||
"db-address": ":memory:",
|
||||
|
|
2
vendor/codeberg.org/gruf/go-cache/v3/LICENSE
generated
vendored
2
vendor/codeberg.org/gruf/go-cache/v3/LICENSE
generated
vendored
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) gruf
|
||||
Copyright (c) 2022 gruf
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
12
vendor/codeberg.org/gruf/go-cache/v3/README.md
generated
vendored
12
vendor/codeberg.org/gruf/go-cache/v3/README.md
generated
vendored
|
@ -1,14 +1,14 @@
|
|||
# go-cache
|
||||
|
||||
Provides access to simple, yet flexible, and performant caches (with TTL if required) via the `cache.Cache{}` and `cache.TTLCache{}` interfaces.
|
||||
|
||||
## simple
|
||||
|
||||
A `cache.Cache{}` implementation with much more of the inner workings exposed. Designed to be used as a base for your own customizations, or used as-is.
|
||||
Provides access to a simple yet flexible, performant TTL cache via the `Cache{}` interface and `cache.New()`. Under the hood this is returning a `ttl.Cache{}`.
|
||||
|
||||
## ttl
|
||||
|
||||
A `cache.TTLCache{}` implementation with much more of the inner workings exposed. Designed to be used as a base for your own customizations, or used as-is.
|
||||
A TTL cache implementation with much of the inner workings exposed, designed to be used as a base for your own customizations, or used as-is. Access via the base package `cache.New()` is recommended in the latter case, to prevent accidental use of unsafe methods.
|
||||
|
||||
## lookup
|
||||
|
||||
`lookup.Cache` is an example of a more complex cache implementation using `ttl.Cache{}` as its underpinning. It provides caching of items under multiple keys.
|
||||
|
||||
## result
|
||||
|
||||
|
|
30
vendor/codeberg.org/gruf/go-cache/v3/cache.go
generated
vendored
30
vendor/codeberg.org/gruf/go-cache/v3/cache.go
generated
vendored
|
@ -3,33 +3,26 @@ package cache
|
|||
import (
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-cache/v3/simple"
|
||||
"codeberg.org/gruf/go-cache/v3/ttl"
|
||||
ttlcache "codeberg.org/gruf/go-cache/v3/ttl"
|
||||
)
|
||||
|
||||
// TTLCache represents a TTL cache with customizable callbacks, it exists here to abstract away the "unsafe" methods in the case that you do not want your own implementation atop ttl.Cache{}.
|
||||
type TTLCache[Key comparable, Value any] interface {
|
||||
// Cache represents a TTL cache with customizable callbacks, it exists here to abstract away the "unsafe" methods in the case that you do not want your own implementation atop ttl.Cache{}.
|
||||
type Cache[Key comparable, Value any] interface {
|
||||
// Start will start the cache background eviction routine with given sweep frequency. If already running or a freq <= 0 provided, this is a no-op. This will block until the eviction routine has started.
|
||||
Start(freq time.Duration) bool
|
||||
|
||||
// Stop will stop cache background eviction routine. If not running this is a no-op. This will block until the eviction routine has stopped.
|
||||
Stop() bool
|
||||
|
||||
// SetTTL sets the cache item TTL. Update can be specified to force updates of existing items in the cache, this will simply add the change in TTL to their current expiry time.
|
||||
SetTTL(ttl time.Duration, update bool)
|
||||
|
||||
// implements base cache.
|
||||
Cache[Key, Value]
|
||||
}
|
||||
|
||||
// Cache represents a cache with customizable callbacks, it exists here to abstract away the "unsafe" methods in the case that you do not want your own implementation atop simple.Cache{}.
|
||||
type Cache[Key comparable, Value any] interface {
|
||||
// SetEvictionCallback sets the eviction callback to the provided hook.
|
||||
SetEvictionCallback(hook func(Key, Value))
|
||||
|
||||
// SetInvalidateCallback sets the invalidate callback to the provided hook.
|
||||
SetInvalidateCallback(hook func(Key, Value))
|
||||
|
||||
// SetTTL sets the cache item TTL. Update can be specified to force updates of existing items in the cache, this will simply add the change in TTL to their current expiry time.
|
||||
SetTTL(ttl time.Duration, update bool)
|
||||
|
||||
// Get fetches the value with key from the cache, extending its TTL.
|
||||
Get(key Key) (value Value, ok bool)
|
||||
|
||||
|
@ -64,12 +57,7 @@ type Cache[Key comparable, Value any] interface {
|
|||
Cap() int
|
||||
}
|
||||
|
||||
// New returns a new initialized Cache with given initial length, maximum capacity.
|
||||
func New[K comparable, V any](len, cap int) Cache[K, V] {
|
||||
return simple.New[K, V](len, cap)
|
||||
}
|
||||
|
||||
// NewTTL returns a new initialized TTLCache with given initial length, maximum capacity and TTL duration.
|
||||
func NewTTL[K comparable, V any](len, cap int, _ttl time.Duration) TTLCache[K, V] {
|
||||
return ttl.New[K, V](len, cap, _ttl)
|
||||
// New returns a new initialized Cache with given initial length, maximum capacity and item TTL.
|
||||
func New[K comparable, V any](len, cap int, ttl time.Duration) Cache[K, V] {
|
||||
return ttlcache.New[K, V](len, cap, ttl)
|
||||
}
|
||||
|
|
224
vendor/codeberg.org/gruf/go-cache/v3/result/cache.go
generated
vendored
224
vendor/codeberg.org/gruf/go-cache/v3/result/cache.go
generated
vendored
|
@ -2,17 +2,14 @@ package result
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"time"
|
||||
_ "unsafe"
|
||||
|
||||
"codeberg.org/gruf/go-cache/v3/simple"
|
||||
"codeberg.org/gruf/go-cache/v3/ttl"
|
||||
"codeberg.org/gruf/go-errors/v2"
|
||||
)
|
||||
|
||||
var ErrUnsupportedZero = errors.New("")
|
||||
|
||||
// Lookup represents a struct object lookup method in the cache.
|
||||
type Lookup struct {
|
||||
// Name is a period ('.') separated string
|
||||
|
@ -26,23 +23,26 @@ type Lookup struct {
|
|||
// Multi allows specifying a key capable of storing
|
||||
// multiple results. Note this only supports invalidate.
|
||||
Multi bool
|
||||
|
||||
// TODO: support toggling case sensitive lookups.
|
||||
// CaseSensitive bool
|
||||
}
|
||||
|
||||
// Cache provides a means of caching value structures, along with
|
||||
// the results of attempting to load them. An example usecase of this
|
||||
// cache would be in wrapping a database, allowing caching of sql.ErrNoRows.
|
||||
type Cache[T any] struct {
|
||||
cache simple.Cache[int64, *result] // underlying result cache
|
||||
type Cache[Value any] struct {
|
||||
cache ttl.Cache[int64, result[Value]] // underlying result cache
|
||||
invalid func(Value) // store unwrapped invalidate callback.
|
||||
lookups structKeys // pre-determined struct lookups
|
||||
invalid func(T) // store unwrapped invalidate callback.
|
||||
ignore func(error) bool // determines cacheable errors
|
||||
copy func(T) T // copies a Value type
|
||||
copy func(Value) Value // copies a Value type
|
||||
next int64 // update key counter
|
||||
}
|
||||
|
||||
// New returns a new initialized Cache, with given lookups, underlying value copy function and provided capacity.
|
||||
func New[T any](lookups []Lookup, copy func(T) T, cap int) *Cache[T] {
|
||||
var z T
|
||||
func New[Value any](lookups []Lookup, copy func(Value) Value, cap int) *Cache[Value] {
|
||||
var z Value
|
||||
|
||||
// Determine generic type
|
||||
t := reflect.TypeOf(z)
|
||||
|
@ -58,7 +58,7 @@ func New[T any](lookups []Lookup, copy func(T) T, cap int) *Cache[T] {
|
|||
}
|
||||
|
||||
// Allocate new cache object
|
||||
c := &Cache[T]{copy: copy}
|
||||
c := &Cache[Value]{copy: copy}
|
||||
c.lookups = make([]structKey, len(lookups))
|
||||
|
||||
for i, lookup := range lookups {
|
||||
|
@ -67,20 +67,38 @@ func New[T any](lookups []Lookup, copy func(T) T, cap int) *Cache[T] {
|
|||
}
|
||||
|
||||
// Create and initialize underlying cache
|
||||
c.cache.Init(0, cap)
|
||||
c.cache.Init(0, cap, 0)
|
||||
c.SetEvictionCallback(nil)
|
||||
c.SetInvalidateCallback(nil)
|
||||
c.IgnoreErrors(nil)
|
||||
return c
|
||||
}
|
||||
|
||||
// Start will start the cache background eviction routine with given sweep frequency. If already
|
||||
// running or a freq <= 0 provided, this is a no-op. This will block until eviction routine started.
|
||||
func (c *Cache[Value]) Start(freq time.Duration) bool {
|
||||
return c.cache.Start(freq)
|
||||
}
|
||||
|
||||
// Stop will stop cache background eviction routine. If not running this
|
||||
// is a no-op. This will block until the eviction routine has stopped.
|
||||
func (c *Cache[Value]) Stop() bool {
|
||||
return c.cache.Stop()
|
||||
}
|
||||
|
||||
// SetTTL sets the cache item TTL. Update can be specified to force updates of existing items
|
||||
// in the cache, this will simply add the change in TTL to their current expiry time.
|
||||
func (c *Cache[Value]) SetTTL(ttl time.Duration, update bool) {
|
||||
c.cache.SetTTL(ttl, update)
|
||||
}
|
||||
|
||||
// SetEvictionCallback sets the eviction callback to the provided hook.
|
||||
func (c *Cache[T]) SetEvictionCallback(hook func(T)) {
|
||||
func (c *Cache[Value]) SetEvictionCallback(hook func(Value)) {
|
||||
if hook == nil {
|
||||
// Ensure non-nil hook.
|
||||
hook = func(T) {}
|
||||
hook = func(Value) {}
|
||||
}
|
||||
c.cache.SetEvictionCallback(func(pkey int64, res *result) {
|
||||
c.cache.SetEvictionCallback(func(pkey int64, res result[Value]) {
|
||||
c.cache.Lock()
|
||||
for _, key := range res.Keys {
|
||||
// Delete key->pkey lookup
|
||||
|
@ -90,25 +108,23 @@ func (c *Cache[T]) SetEvictionCallback(hook func(T)) {
|
|||
c.cache.Unlock()
|
||||
|
||||
if res.Error != nil {
|
||||
// Skip value hooks
|
||||
// Skip error hooks
|
||||
return
|
||||
}
|
||||
|
||||
// Free result and call hook.
|
||||
v := getResultValue[T](res)
|
||||
putResult(res)
|
||||
hook(v)
|
||||
// Call user hook.
|
||||
hook(res.Value)
|
||||
})
|
||||
}
|
||||
|
||||
// SetInvalidateCallback sets the invalidate callback to the provided hook.
|
||||
func (c *Cache[T]) SetInvalidateCallback(hook func(T)) {
|
||||
func (c *Cache[Value]) SetInvalidateCallback(hook func(Value)) {
|
||||
if hook == nil {
|
||||
// Ensure non-nil hook.
|
||||
hook = func(T) {}
|
||||
hook = func(Value) {}
|
||||
} // store hook.
|
||||
c.invalid = hook
|
||||
c.cache.SetInvalidateCallback(func(pkey int64, res *result) {
|
||||
c.cache.SetInvalidateCallback(func(pkey int64, res result[Value]) {
|
||||
c.cache.Lock()
|
||||
for _, key := range res.Keys {
|
||||
// Delete key->pkey lookup
|
||||
|
@ -118,19 +134,17 @@ func (c *Cache[T]) SetInvalidateCallback(hook func(T)) {
|
|||
c.cache.Unlock()
|
||||
|
||||
if res.Error != nil {
|
||||
// Skip value hooks
|
||||
// Skip error hooks
|
||||
return
|
||||
}
|
||||
|
||||
// Free result and call hook.
|
||||
v := getResultValue[T](res)
|
||||
putResult(res)
|
||||
hook(v)
|
||||
// Call user hook.
|
||||
hook(res.Value)
|
||||
})
|
||||
}
|
||||
|
||||
// IgnoreErrors allows setting a function hook to determine which error types should / not be cached.
|
||||
func (c *Cache[T]) IgnoreErrors(ignore func(error) bool) {
|
||||
func (c *Cache[Value]) IgnoreErrors(ignore func(error) bool) {
|
||||
if ignore == nil {
|
||||
ignore = func(err error) bool {
|
||||
return errors.Comparable(
|
||||
|
@ -146,10 +160,11 @@ func (c *Cache[T]) IgnoreErrors(ignore func(error) bool) {
|
|||
}
|
||||
|
||||
// Load will attempt to load an existing result from the cacche for the given lookup and key parts, else calling the provided load function and caching the result.
|
||||
func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any) (T, error) {
|
||||
func (c *Cache[Value]) Load(lookup string, load func() (Value, error), keyParts ...any) (Value, error) {
|
||||
var (
|
||||
zero T
|
||||
res *result
|
||||
zero Value
|
||||
res result[Value]
|
||||
ok bool
|
||||
)
|
||||
|
||||
// Get lookup key info by name.
|
||||
|
@ -167,22 +182,24 @@ func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any)
|
|||
// Look for primary cache key
|
||||
pkeys := keyInfo.pkeys[ckey]
|
||||
|
||||
if len(pkeys) > 0 {
|
||||
if ok = (len(pkeys) > 0); ok {
|
||||
var entry *ttl.Entry[int64, result[Value]]
|
||||
|
||||
// Fetch the result for primary key
|
||||
entry, ok := c.cache.Cache.Get(pkeys[0])
|
||||
entry, ok = c.cache.Cache.Get(pkeys[0])
|
||||
if ok {
|
||||
// Since the invalidation / eviction hooks acquire a mutex
|
||||
// lock separately, and only at this point are the pkeys
|
||||
// updated, there is a chance that a primary key may return
|
||||
// no matching entry. Hence we have to check for it here.
|
||||
res = entry.Value.(*result)
|
||||
res = entry.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Done with lock
|
||||
c.cache.Unlock()
|
||||
|
||||
if res == nil {
|
||||
if !ok {
|
||||
// Generate fresh result.
|
||||
value, err := load()
|
||||
|
||||
|
@ -192,9 +209,6 @@ func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any)
|
|||
return zero, err
|
||||
}
|
||||
|
||||
// Alloc result.
|
||||
res = getResult()
|
||||
|
||||
// Store error result.
|
||||
res.Error = err
|
||||
|
||||
|
@ -205,9 +219,6 @@ func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any)
|
|||
key: ckey,
|
||||
}}
|
||||
} else {
|
||||
// Alloc result.
|
||||
res = getResult()
|
||||
|
||||
// Store value result.
|
||||
res.Value = value
|
||||
|
||||
|
@ -234,29 +245,28 @@ func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any)
|
|||
evict = c.store(res)
|
||||
}
|
||||
|
||||
// Catch and return cached error
|
||||
if err := res.Error; err != nil {
|
||||
return zero, err
|
||||
// Catch and return error
|
||||
if res.Error != nil {
|
||||
return zero, res.Error
|
||||
}
|
||||
|
||||
// Copy value from cached result.
|
||||
v := c.copy(getResultValue[T](res))
|
||||
|
||||
return v, nil
|
||||
// Return a copy of value from cache
|
||||
return c.copy(res.Value), nil
|
||||
}
|
||||
|
||||
// Store will call the given store function, and on success store the value in the cache as a positive result.
|
||||
func (c *Cache[T]) Store(value T, store func() error) error {
|
||||
func (c *Cache[Value]) Store(value Value, store func() error) error {
|
||||
// Attempt to store this value.
|
||||
if err := store(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare cached result.
|
||||
result := getResult()
|
||||
result.Keys = c.lookups.generate(value)
|
||||
result.Value = c.copy(value)
|
||||
result.Error = nil
|
||||
result := result[Value]{
|
||||
Keys: c.lookups.generate(value),
|
||||
Value: c.copy(value),
|
||||
Error: nil,
|
||||
}
|
||||
|
||||
var evict func()
|
||||
|
||||
|
@ -283,8 +293,9 @@ func (c *Cache[T]) Store(value T, store func() error) error {
|
|||
}
|
||||
|
||||
// Has checks the cache for a positive result under the given lookup and key parts.
|
||||
func (c *Cache[T]) Has(lookup string, keyParts ...any) bool {
|
||||
var res *result
|
||||
func (c *Cache[Value]) Has(lookup string, keyParts ...any) bool {
|
||||
var res result[Value]
|
||||
var ok bool
|
||||
|
||||
// Get lookup key info by name.
|
||||
keyInfo := c.lookups.get(lookup)
|
||||
|
@ -301,29 +312,29 @@ func (c *Cache[T]) Has(lookup string, keyParts ...any) bool {
|
|||
// Look for primary key for cache key
|
||||
pkeys := keyInfo.pkeys[ckey]
|
||||
|
||||
if len(pkeys) > 0 {
|
||||
if ok = (len(pkeys) > 0); ok {
|
||||
var entry *ttl.Entry[int64, result[Value]]
|
||||
|
||||
// Fetch the result for primary key
|
||||
entry, ok := c.cache.Cache.Get(pkeys[0])
|
||||
entry, ok = c.cache.Cache.Get(pkeys[0])
|
||||
if ok {
|
||||
// Since the invalidation / eviction hooks acquire a mutex
|
||||
// lock separately, and only at this point are the pkeys
|
||||
// updated, there is a chance that a primary key may return
|
||||
// no matching entry. Hence we have to check for it here.
|
||||
res = entry.Value.(*result)
|
||||
res = entry.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Check for result AND non-error result.
|
||||
ok := (res != nil && res.Error == nil)
|
||||
|
||||
// Done with lock
|
||||
c.cache.Unlock()
|
||||
|
||||
return ok
|
||||
// Check for non-error result.
|
||||
return ok && (res.Error == nil)
|
||||
}
|
||||
|
||||
// Invalidate will invalidate any result from the cache found under given lookup and key parts.
|
||||
func (c *Cache[T]) Invalidate(lookup string, keyParts ...any) {
|
||||
func (c *Cache[Value]) Invalidate(lookup string, keyParts ...any) {
|
||||
// Get lookup key info by name.
|
||||
keyInfo := c.lookups.get(lookup)
|
||||
|
||||
|
@ -340,20 +351,15 @@ func (c *Cache[T]) Invalidate(lookup string, keyParts ...any) {
|
|||
c.cache.InvalidateAll(pkeys...)
|
||||
}
|
||||
|
||||
// Clear empties the cache, calling the invalidate callback where necessary.
|
||||
func (c *Cache[T]) Clear() { c.Trim(100) }
|
||||
|
||||
// Trim ensures the cache stays within percentage of total capacity, truncating where necessary.
|
||||
func (c *Cache[T]) Trim(perc float64) { c.cache.Trim(perc) }
|
||||
// Clear empties the cache, calling the invalidate callback.
|
||||
func (c *Cache[Value]) Clear() { c.cache.Clear() }
|
||||
|
||||
// store will cache this result under all of its required cache keys.
|
||||
func (c *Cache[T]) store(res *result) (evict func()) {
|
||||
var toEvict []*result
|
||||
|
||||
func (c *Cache[Value]) store(res result[Value]) (evict func()) {
|
||||
// Get primary key
|
||||
res.PKey = c.next
|
||||
pnext := c.next
|
||||
c.next++
|
||||
if res.PKey > c.next {
|
||||
if pnext > c.next {
|
||||
panic("cache primary key overflow")
|
||||
}
|
||||
|
||||
|
@ -365,19 +371,15 @@ func (c *Cache[T]) store(res *result) (evict func()) {
|
|||
for _, conflict := range pkeys {
|
||||
// Get the overlapping result with this key.
|
||||
entry, _ := c.cache.Cache.Get(conflict)
|
||||
confRes := entry.Value.(*result)
|
||||
|
||||
// From conflicting entry, drop this key, this
|
||||
// will prevent eviction cleanup key confusion.
|
||||
confRes.Keys.drop(key.info.name)
|
||||
entry.Value.Keys.drop(key.info.name)
|
||||
|
||||
if len(res.Keys) == 0 {
|
||||
if len(entry.Value.Keys) == 0 {
|
||||
// We just over-wrote the only lookup key for
|
||||
// this value, so we drop its primary key too.
|
||||
c.cache.Cache.Delete(conflict)
|
||||
|
||||
// Add finished result to evict queue.
|
||||
toEvict = append(toEvict, confRes)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,58 +388,42 @@ func (c *Cache[T]) store(res *result) (evict func()) {
|
|||
}
|
||||
|
||||
// Store primary key lookup.
|
||||
pkeys = append(pkeys, res.PKey)
|
||||
pkeys = append(pkeys, pnext)
|
||||
key.info.pkeys[key.key] = pkeys
|
||||
}
|
||||
|
||||
// Acquire new cache entry.
|
||||
entry := simple.GetEntry()
|
||||
entry.Key = res.PKey
|
||||
entry.Value = res
|
||||
// Store main entry under primary key, using evict hook if needed
|
||||
c.cache.Cache.SetWithHook(pnext, &ttl.Entry[int64, result[Value]]{
|
||||
Expiry: c.expiry(),
|
||||
Key: pnext,
|
||||
Value: res,
|
||||
}, func(_ int64, item *ttl.Entry[int64, result[Value]]) {
|
||||
evict = func() { c.cache.Evict(item.Key, item.Value) }
|
||||
})
|
||||
|
||||
evictFn := func(_ int64, entry *simple.Entry) {
|
||||
// on evict during set, store evicted result.
|
||||
toEvict = append(toEvict, entry.Value.(*result))
|
||||
return evict
|
||||
}
|
||||
|
||||
// Store main entry under primary key, catch evicted.
|
||||
c.cache.Cache.SetWithHook(res.PKey, entry, evictFn)
|
||||
//go:linkname runtime_nanotime runtime.nanotime
|
||||
func runtime_nanotime() uint64
|
||||
|
||||
if len(toEvict) == 0 {
|
||||
// none evicted.
|
||||
return nil
|
||||
// expiry returns an the next expiry time to use for an entry,
|
||||
// which is equivalent to time.Now().Add(ttl), or zero if disabled.
|
||||
func (c *Cache[Value]) expiry() uint64 {
|
||||
if ttl := c.cache.TTL; ttl > 0 {
|
||||
return runtime_nanotime() +
|
||||
uint64(c.cache.TTL)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
return func() {
|
||||
for i := range toEvict {
|
||||
// Rescope result.
|
||||
res := toEvict[i]
|
||||
|
||||
// Call evict hook on each entry.
|
||||
c.cache.Evict(res.PKey, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type result struct {
|
||||
// Result primary key
|
||||
PKey int64
|
||||
|
||||
type result[Value any] struct {
|
||||
// keys accessible under
|
||||
Keys cacheKeys
|
||||
|
||||
// cached value
|
||||
Value any
|
||||
Value Value
|
||||
|
||||
// cached error
|
||||
Error error
|
||||
}
|
||||
|
||||
// getResultValue is a safe way of casting and fetching result value.
|
||||
func getResultValue[T any](res *result) T {
|
||||
v, ok := res.Value.(T)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "!! BUG: unexpected value type in result: %T\n", res.Value)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
|
145
vendor/codeberg.org/gruf/go-cache/v3/result/key.go
generated
vendored
145
vendor/codeberg.org/gruf/go-cache/v3/result/key.go
generated
vendored
|
@ -47,32 +47,27 @@ func (sk structKeys) generate(a any) []cacheKey {
|
|||
buf := getBuf()
|
||||
defer putBuf(buf)
|
||||
|
||||
outer:
|
||||
for i := range sk {
|
||||
// Reset buffer
|
||||
buf.Reset()
|
||||
buf.B = buf.B[:0]
|
||||
|
||||
// Append each field value to buffer.
|
||||
for _, field := range sk[i].fields {
|
||||
fv := v.Field(field.index)
|
||||
fi := fv.Interface()
|
||||
|
||||
// Mangle this key part into buffer.
|
||||
ok := field.manglePart(buf, fi)
|
||||
|
||||
if !ok {
|
||||
// don't generate keys
|
||||
// for zero value parts.
|
||||
continue outer
|
||||
}
|
||||
|
||||
// Append part separator.
|
||||
buf.B = field.mangle(buf.B, fi)
|
||||
buf.B = append(buf.B, '.')
|
||||
}
|
||||
|
||||
// Drop last '.'
|
||||
buf.Truncate(1)
|
||||
|
||||
// Don't generate keys for zero values
|
||||
if allowZero := sk[i].zero == ""; // nocollapse
|
||||
!allowZero && buf.String() == sk[i].zero {
|
||||
continue
|
||||
}
|
||||
|
||||
// Append new cached key to slice
|
||||
keys = append(keys, cacheKey{
|
||||
info: &sk[i],
|
||||
|
@ -119,6 +114,14 @@ type structKey struct {
|
|||
// period ('.') separated struct field names.
|
||||
name string
|
||||
|
||||
// zero is the possible zero value for this key.
|
||||
// if set, this will _always_ be non-empty, as
|
||||
// the mangled cache key will never be empty.
|
||||
//
|
||||
// i.e. zero = "" --> allow zero value keys
|
||||
// zero != "" --> don't allow zero value keys
|
||||
zero string
|
||||
|
||||
// unique determines whether this structKey supports
|
||||
// multiple or just the singular unique result.
|
||||
unique bool
|
||||
|
@ -132,10 +135,47 @@ type structKey struct {
|
|||
pkeys map[string][]int64
|
||||
}
|
||||
|
||||
type structField struct {
|
||||
// index is the reflect index of this struct field.
|
||||
index int
|
||||
|
||||
// mangle is the mangler function for
|
||||
// serializing values of this struct field.
|
||||
mangle mangler.Mangler
|
||||
}
|
||||
|
||||
// genKey generates a cache key string for given key parts (i.e. serializes them using "go-mangler").
|
||||
func (sk structKey) genKey(parts []any) string {
|
||||
// Check this expected no. key parts.
|
||||
if len(parts) != len(sk.fields) {
|
||||
panic(fmt.Sprintf("incorrect no. key parts provided: want=%d received=%d", len(parts), len(sk.fields)))
|
||||
}
|
||||
|
||||
// Acquire byte buffer
|
||||
buf := getBuf()
|
||||
defer putBuf(buf)
|
||||
buf.Reset()
|
||||
|
||||
// Encode each key part
|
||||
for i, part := range parts {
|
||||
buf.B = sk.fields[i].mangle(buf.B, part)
|
||||
buf.B = append(buf.B, '.')
|
||||
}
|
||||
|
||||
// Drop last '.'
|
||||
buf.Truncate(1)
|
||||
|
||||
// Return string copy
|
||||
return string(buf.B)
|
||||
}
|
||||
|
||||
// newStructKey will generate a structKey{} information object for user-given lookup
|
||||
// key information, and the receiving generic paramter's type information. Panics on error.
|
||||
func newStructKey(lk Lookup, t reflect.Type) structKey {
|
||||
var sk structKey
|
||||
var (
|
||||
sk structKey
|
||||
zeros []any
|
||||
)
|
||||
|
||||
// Set the lookup name
|
||||
sk.name = lk.Name
|
||||
|
@ -143,6 +183,9 @@ func newStructKey(lk Lookup, t reflect.Type) structKey {
|
|||
// Split dot-separated lookup to get
|
||||
// the individual struct field names
|
||||
names := strings.Split(lk.Name, ".")
|
||||
if len(names) == 0 {
|
||||
panic("no key fields specified")
|
||||
}
|
||||
|
||||
// Allocate the mangler and field indices slice.
|
||||
sk.fields = make([]structField, len(names))
|
||||
|
@ -170,12 +213,16 @@ func newStructKey(lk Lookup, t reflect.Type) structKey {
|
|||
sk.fields[i].mangle = mangler.Get(ft.Type)
|
||||
|
||||
if !lk.AllowZero {
|
||||
// Append the mangled zero value interface
|
||||
zero := sk.fields[i].mangle(nil, v.Interface())
|
||||
sk.fields[i].zero = string(zero)
|
||||
// Append the zero value interface
|
||||
zeros = append(zeros, v.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
if len(zeros) > 0 {
|
||||
// Generate zero value string
|
||||
sk.zero = sk.genKey(zeros)
|
||||
}
|
||||
|
||||
// Set unique lookup flag.
|
||||
sk.unique = !lk.Multi
|
||||
|
||||
|
@ -185,68 +232,6 @@ func newStructKey(lk Lookup, t reflect.Type) structKey {
|
|||
return sk
|
||||
}
|
||||
|
||||
// genKey generates a cache key string for given key parts (i.e. serializes them using "go-mangler").
|
||||
func (sk *structKey) genKey(parts []any) string {
|
||||
// Check this expected no. key parts.
|
||||
if len(parts) != len(sk.fields) {
|
||||
panic(fmt.Sprintf("incorrect no. key parts provided: want=%d received=%d", len(parts), len(sk.fields)))
|
||||
}
|
||||
|
||||
// Acquire byte buffer
|
||||
buf := getBuf()
|
||||
defer putBuf(buf)
|
||||
buf.Reset()
|
||||
|
||||
for i, part := range parts {
|
||||
// Mangle this key part into buffer.
|
||||
// specifically ignoring whether this
|
||||
// is returning a zero value key part.
|
||||
_ = sk.fields[i].manglePart(buf, part)
|
||||
|
||||
// Append part separator.
|
||||
buf.B = append(buf.B, '.')
|
||||
}
|
||||
|
||||
// Drop last '.'
|
||||
buf.Truncate(1)
|
||||
|
||||
// Return string copy
|
||||
return string(buf.B)
|
||||
}
|
||||
|
||||
type structField struct {
|
||||
// index is the reflect index of this struct field.
|
||||
index int
|
||||
|
||||
// zero is the possible zero value for this
|
||||
// key part. if set, this will _always_ be
|
||||
// non-empty due to how the mangler works.
|
||||
//
|
||||
// i.e. zero = "" --> allow zero value keys
|
||||
// zero != "" --> don't allow zero value keys
|
||||
zero string
|
||||
|
||||
// mangle is the mangler function for
|
||||
// serializing values of this struct field.
|
||||
mangle mangler.Mangler
|
||||
}
|
||||
|
||||
// manglePart ...
|
||||
func (field *structField) manglePart(buf *byteutil.Buffer, part any) bool {
|
||||
// Start of part bytes.
|
||||
start := len(buf.B)
|
||||
|
||||
// Mangle this key part into buffer.
|
||||
buf.B = field.mangle(buf.B, part)
|
||||
|
||||
// End of part bytes.
|
||||
end := len(buf.B)
|
||||
|
||||
// Return whether this is zero value.
|
||||
return (field.zero == "" ||
|
||||
string(buf.B[start:end]) != field.zero)
|
||||
}
|
||||
|
||||
// isExported checks whether function name is exported.
|
||||
func isExported(fnName string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(fnName)
|
||||
|
@ -261,12 +246,10 @@ var bufPool = sync.Pool{
|
|||
},
|
||||
}
|
||||
|
||||
// getBuf acquires a byte buffer from memory pool.
|
||||
func getBuf() *byteutil.Buffer {
|
||||
return bufPool.Get().(*byteutil.Buffer)
|
||||
}
|
||||
|
||||
// putBuf replaces a byte buffer back in memory pool.
|
||||
func putBuf(buf *byteutil.Buffer) {
|
||||
if buf.Cap() > int(^uint16(0)) {
|
||||
return // drop large bufs
|
||||
|
|
24
vendor/codeberg.org/gruf/go-cache/v3/result/pool.go
generated
vendored
24
vendor/codeberg.org/gruf/go-cache/v3/result/pool.go
generated
vendored
|
@ -1,24 +0,0 @@
|
|||
package result
|
||||
|
||||
import "sync"
|
||||
|
||||
// resultPool is a global pool for result
|
||||
// objects, regardless of cache type.
|
||||
var resultPool sync.Pool
|
||||
|
||||
// getEntry fetches a result from pool, or allocates new.
|
||||
func getResult() *result {
|
||||
v := resultPool.Get()
|
||||
if v == nil {
|
||||
return new(result)
|
||||
}
|
||||
return v.(*result)
|
||||
}
|
||||
|
||||
// putResult replaces a result in the pool.
|
||||
func putResult(r *result) {
|
||||
r.Keys = nil
|
||||
r.Value = nil
|
||||
r.Error = nil
|
||||
resultPool.Put(r)
|
||||
}
|
454
vendor/codeberg.org/gruf/go-cache/v3/simple/cache.go
generated
vendored
454
vendor/codeberg.org/gruf/go-cache/v3/simple/cache.go
generated
vendored
|
@ -1,454 +0,0 @@
|
|||
package simple
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"codeberg.org/gruf/go-maps"
|
||||
)
|
||||
|
||||
// Entry represents an item in the cache.
|
||||
type Entry struct {
|
||||
Key any
|
||||
Value any
|
||||
}
|
||||
|
||||
// Cache is the underlying Cache implementation, providing both the base Cache interface and unsafe access to underlying map to allow flexibility in building your own.
|
||||
type Cache[Key comparable, Value any] struct {
|
||||
// Evict is the hook that is called when an item is evicted from the cache.
|
||||
Evict func(Key, Value)
|
||||
|
||||
// Invalid is the hook that is called when an item's data in the cache is invalidated, includes Add/Set.
|
||||
Invalid func(Key, Value)
|
||||
|
||||
// Cache is the underlying hashmap used for this cache.
|
||||
Cache maps.LRUMap[Key, *Entry]
|
||||
|
||||
// Embedded mutex.
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// New returns a new initialized Cache with given initial length, maximum capacity and item TTL.
|
||||
func New[K comparable, V any](len, cap int) *Cache[K, V] {
|
||||
c := new(Cache[K, V])
|
||||
c.Init(len, cap)
|
||||
return c
|
||||
}
|
||||
|
||||
// Init will initialize this cache with given initial length, maximum capacity and item TTL.
|
||||
func (c *Cache[K, V]) Init(len, cap int) {
|
||||
c.SetEvictionCallback(nil)
|
||||
c.SetInvalidateCallback(nil)
|
||||
c.Cache.Init(len, cap)
|
||||
}
|
||||
|
||||
// SetEvictionCallback: implements cache.Cache's SetEvictionCallback().
|
||||
func (c *Cache[K, V]) SetEvictionCallback(hook func(K, V)) {
|
||||
c.locked(func() { c.Evict = hook })
|
||||
}
|
||||
|
||||
// SetInvalidateCallback: implements cache.Cache's SetInvalidateCallback().
|
||||
func (c *Cache[K, V]) SetInvalidateCallback(hook func(K, V)) {
|
||||
c.locked(func() { c.Invalid = hook })
|
||||
}
|
||||
|
||||
// Get: implements cache.Cache's Get().
|
||||
func (c *Cache[K, V]) Get(key K) (V, bool) {
|
||||
var (
|
||||
// did exist in cache?
|
||||
ok bool
|
||||
|
||||
// cached value.
|
||||
v V
|
||||
)
|
||||
|
||||
c.locked(func() {
|
||||
var item *Entry
|
||||
|
||||
// Check for item in cache
|
||||
item, ok = c.Cache.Get(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Set item value.
|
||||
v = item.Value.(V)
|
||||
})
|
||||
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Add: implements cache.Cache's Add().
|
||||
func (c *Cache[K, V]) Add(key K, value V) bool {
|
||||
var (
|
||||
// did exist in cache?
|
||||
ok bool
|
||||
|
||||
// was entry evicted?
|
||||
ev bool
|
||||
|
||||
// evicted key values.
|
||||
evcK K
|
||||
evcV V
|
||||
|
||||
// hook func ptrs.
|
||||
evict func(K, V)
|
||||
)
|
||||
|
||||
c.locked(func() {
|
||||
// Check if in cache.
|
||||
ok = c.Cache.Has(key)
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Alloc new entry.
|
||||
new := GetEntry()
|
||||
new.Key = key
|
||||
new.Value = value
|
||||
|
||||
// Add new entry to cache and catched any evicted item.
|
||||
c.Cache.SetWithHook(key, new, func(_ K, item *Entry) {
|
||||
evcK = item.Key.(K)
|
||||
evcV = item.Value.(V)
|
||||
ev = true
|
||||
PutEntry(item)
|
||||
})
|
||||
|
||||
// Set hook func ptr.
|
||||
evict = c.Evict
|
||||
})
|
||||
|
||||
if ev && evict != nil {
|
||||
// Pass to eviction hook.
|
||||
evict(evcK, evcV)
|
||||
}
|
||||
|
||||
return !ok
|
||||
}
|
||||
|
||||
// Set: implements cache.Cache's Set().
|
||||
func (c *Cache[K, V]) Set(key K, value V) {
|
||||
var (
|
||||
// did exist in cache?
|
||||
ok bool
|
||||
|
||||
// was entry evicted?
|
||||
ev bool
|
||||
|
||||
// old value.
|
||||
oldV V
|
||||
|
||||
// evicted key values.
|
||||
evcK K
|
||||
evcV V
|
||||
|
||||
// hook func ptrs.
|
||||
invalid func(K, V)
|
||||
evict func(K, V)
|
||||
)
|
||||
|
||||
c.locked(func() {
|
||||
var item *Entry
|
||||
|
||||
// Check for item in cache
|
||||
item, ok = c.Cache.Get(key)
|
||||
|
||||
if ok {
|
||||
// Set old value.
|
||||
oldV = item.Value.(V)
|
||||
|
||||
// Update the existing item.
|
||||
item.Value = value
|
||||
} else {
|
||||
// Alloc new entry.
|
||||
new := GetEntry()
|
||||
new.Key = key
|
||||
new.Value = value
|
||||
|
||||
// Add new entry to cache and catched any evicted item.
|
||||
c.Cache.SetWithHook(key, new, func(_ K, item *Entry) {
|
||||
evcK = item.Key.(K)
|
||||
evcV = item.Value.(V)
|
||||
ev = true
|
||||
PutEntry(item)
|
||||
})
|
||||
}
|
||||
|
||||
// Set hook func ptrs.
|
||||
invalid = c.Invalid
|
||||
evict = c.Evict
|
||||
})
|
||||
|
||||
if ok && invalid != nil {
|
||||
// Pass to invalidate hook.
|
||||
invalid(key, oldV)
|
||||
}
|
||||
|
||||
if ev && evict != nil {
|
||||
// Pass to eviction hook.
|
||||
evict(evcK, evcV)
|
||||
}
|
||||
}
|
||||
|
||||
// CAS: implements cache.Cache's CAS().
|
||||
func (c *Cache[K, V]) CAS(key K, old V, new V, cmp func(V, V) bool) bool {
|
||||
var (
|
||||
// did exist in cache?
|
||||
ok bool
|
||||
|
||||
// swapped value.
|
||||
oldV V
|
||||
|
||||
// hook func ptrs.
|
||||
invalid func(K, V)
|
||||
)
|
||||
|
||||
c.locked(func() {
|
||||
var item *Entry
|
||||
|
||||
// Check for item in cache
|
||||
item, ok = c.Cache.Get(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Set old value.
|
||||
oldV = item.Value.(V)
|
||||
|
||||
// Perform the comparison
|
||||
if !cmp(old, oldV) {
|
||||
var zero V
|
||||
oldV = zero
|
||||
return
|
||||
}
|
||||
|
||||
// Update value.
|
||||
item.Value = new
|
||||
|
||||
// Set hook func ptr.
|
||||
invalid = c.Invalid
|
||||
})
|
||||
|
||||
if ok && invalid != nil {
|
||||
// Pass to invalidate hook.
|
||||
invalid(key, oldV)
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// Swap: implements cache.Cache's Swap().
|
||||
func (c *Cache[K, V]) Swap(key K, swp V) V {
|
||||
var (
|
||||
// did exist in cache?
|
||||
ok bool
|
||||
|
||||
// swapped value.
|
||||
oldV V
|
||||
|
||||
// hook func ptrs.
|
||||
invalid func(K, V)
|
||||
)
|
||||
|
||||
c.locked(func() {
|
||||
var item *Entry
|
||||
|
||||
// Check for item in cache
|
||||
item, ok = c.Cache.Get(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Set old value.
|
||||
oldV = item.Value.(V)
|
||||
|
||||
// Update value.
|
||||
item.Value = swp
|
||||
|
||||
// Set hook func ptr.
|
||||
invalid = c.Invalid
|
||||
})
|
||||
|
||||
if ok && invalid != nil {
|
||||
// Pass to invalidate hook.
|
||||
invalid(key, oldV)
|
||||
}
|
||||
|
||||
return oldV
|
||||
}
|
||||
|
||||
// Has: implements cache.Cache's Has().
|
||||
func (c *Cache[K, V]) Has(key K) (ok bool) {
|
||||
c.locked(func() {
|
||||
ok = c.Cache.Has(key)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Invalidate: implements cache.Cache's Invalidate().
|
||||
func (c *Cache[K, V]) Invalidate(key K) (ok bool) {
|
||||
var (
|
||||
// old value.
|
||||
oldV V
|
||||
|
||||
// hook func ptrs.
|
||||
invalid func(K, V)
|
||||
)
|
||||
|
||||
c.locked(func() {
|
||||
var item *Entry
|
||||
|
||||
// Check for item in cache
|
||||
item, ok = c.Cache.Get(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Set old value.
|
||||
oldV = item.Value.(V)
|
||||
|
||||
// Remove from cache map
|
||||
_ = c.Cache.Delete(key)
|
||||
|
||||
// Free entry
|
||||
PutEntry(item)
|
||||
|
||||
// Set hook func ptrs.
|
||||
invalid = c.Invalid
|
||||
})
|
||||
|
||||
if ok && invalid != nil {
|
||||
// Pass to invalidate hook.
|
||||
invalid(key, oldV)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// InvalidateAll: implements cache.Cache's InvalidateAll().
|
||||
func (c *Cache[K, V]) InvalidateAll(keys ...K) (ok bool) {
|
||||
var (
|
||||
// deleted items.
|
||||
items []*Entry
|
||||
|
||||
// hook func ptrs.
|
||||
invalid func(K, V)
|
||||
)
|
||||
|
||||
// Allocate a slice for invalidated.
|
||||
items = make([]*Entry, 0, len(keys))
|
||||
|
||||
c.locked(func() {
|
||||
for x := range keys {
|
||||
var item *Entry
|
||||
|
||||
// Check for item in cache
|
||||
item, ok = c.Cache.Get(keys[x])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Append this old value.
|
||||
items = append(items, item)
|
||||
|
||||
// Remove from cache map
|
||||
_ = c.Cache.Delete(keys[x])
|
||||
}
|
||||
|
||||
// Set hook func ptrs.
|
||||
invalid = c.Invalid
|
||||
})
|
||||
|
||||
if invalid != nil {
|
||||
for x := range items {
|
||||
// Pass to invalidate hook.
|
||||
k := items[x].Key.(K)
|
||||
v := items[x].Value.(V)
|
||||
invalid(k, v)
|
||||
|
||||
// Free this entry.
|
||||
PutEntry(items[x])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Clear: implements cache.Cache's Clear().
|
||||
func (c *Cache[K, V]) Clear() { c.Trim(100) }
|
||||
|
||||
// Trim will truncate the cache to ensure it stays within given percentage of total capacity.
|
||||
func (c *Cache[K, V]) Trim(perc float64) {
|
||||
var (
|
||||
// deleted items
|
||||
items []*Entry
|
||||
|
||||
// hook func ptrs.
|
||||
invalid func(K, V)
|
||||
)
|
||||
|
||||
c.locked(func() {
|
||||
// Calculate number of cache items to truncate.
|
||||
max := (perc / 100) * float64(c.Cache.Cap())
|
||||
diff := c.Cache.Len() - int(max)
|
||||
if diff <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Set hook func ptr.
|
||||
invalid = c.Invalid
|
||||
|
||||
// Truncate by calculated length.
|
||||
items = c.truncate(diff, invalid)
|
||||
})
|
||||
|
||||
if invalid != nil {
|
||||
for x := range items {
|
||||
// Pass to invalidate hook.
|
||||
k := items[x].Key.(K)
|
||||
v := items[x].Value.(V)
|
||||
invalid(k, v)
|
||||
|
||||
// Free this entry.
|
||||
PutEntry(items[x])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Len: implements cache.Cache's Len().
|
||||
func (c *Cache[K, V]) Len() (l int) {
|
||||
c.locked(func() { l = c.Cache.Len() })
|
||||
return
|
||||
}
|
||||
|
||||
// Cap: implements cache.Cache's Cap().
|
||||
func (c *Cache[K, V]) Cap() (l int) {
|
||||
c.locked(func() { l = c.Cache.Cap() })
|
||||
return
|
||||
}
|
||||
|
||||
// locked performs given function within mutex lock (NOTE: UNLOCK IS NOT DEFERRED).
|
||||
func (c *Cache[K, V]) locked(fn func()) {
|
||||
c.Lock()
|
||||
fn()
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
// truncate will truncate the cache by given size, returning deleted items.
|
||||
func (c *Cache[K, V]) truncate(sz int, hook func(K, V)) []*Entry {
|
||||
if hook == nil {
|
||||
// No hook to execute, simply release all truncated entries.
|
||||
c.Cache.Truncate(sz, func(_ K, item *Entry) { PutEntry(item) })
|
||||
return nil
|
||||
}
|
||||
|
||||
// Allocate a slice for deleted.
|
||||
deleted := make([]*Entry, 0, sz)
|
||||
|
||||
// Truncate and catch all deleted k-v pairs.
|
||||
c.Cache.Truncate(sz, func(_ K, item *Entry) {
|
||||
deleted = append(deleted, item)
|
||||
})
|
||||
|
||||
return deleted
|
||||
}
|
23
vendor/codeberg.org/gruf/go-cache/v3/simple/pool.go
generated
vendored
23
vendor/codeberg.org/gruf/go-cache/v3/simple/pool.go
generated
vendored
|
@ -1,23 +0,0 @@
|
|||
package simple
|
||||
|
||||
import "sync"
|
||||
|
||||
// entryPool is a global pool for Entry
|
||||
// objects, regardless of cache type.
|
||||
var entryPool sync.Pool
|
||||
|
||||
// GetEntry fetches an Entry from pool, or allocates new.
|
||||
func GetEntry() *Entry {
|
||||
v := entryPool.Get()
|
||||
if v == nil {
|
||||
return new(Entry)
|
||||
}
|
||||
return v.(*Entry)
|
||||
}
|
||||
|
||||
// PutEntry replaces an Entry in the pool.
|
||||
func PutEntry(e *Entry) {
|
||||
e.Key = nil
|
||||
e.Value = nil
|
||||
entryPool.Put(e)
|
||||
}
|
2
vendor/codeberg.org/gruf/go-cache/v3/ttl/ttl.go
generated
vendored
2
vendor/codeberg.org/gruf/go-cache/v3/ttl/ttl.go
generated
vendored
|
@ -15,7 +15,7 @@ type Entry[Key comparable, Value any] struct {
|
|||
Expiry uint64
|
||||
}
|
||||
|
||||
// Cache is the underlying TTLCache implementation, providing both the base Cache interface and unsafe access to underlying map to allow flexibility in building your own.
|
||||
// Cache is the underlying Cache implementation, providing both the base Cache interface and unsafe access to underlying map to allow flexibility in building your own.
|
||||
type Cache[Key comparable, Value any] struct {
|
||||
// TTL is the cache item TTL.
|
||||
TTL time.Duration
|
||||
|
|
19
vendor/github.com/DmitriyVTitov/size/.gitignore
generated
vendored
19
vendor/github.com/DmitriyVTitov/size/.gitignore
generated
vendored
|
@ -1,19 +0,0 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
example
|
||||
.idea
|
||||
go.sum
|
21
vendor/github.com/DmitriyVTitov/size/LICENSE
generated
vendored
21
vendor/github.com/DmitriyVTitov/size/LICENSE
generated
vendored
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Dmitriy Titov (Дмитрий Титов)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
48
vendor/github.com/DmitriyVTitov/size/README.md
generated
vendored
48
vendor/github.com/DmitriyVTitov/size/README.md
generated
vendored
|
@ -1,48 +0,0 @@
|
|||
# size - calculates variable's memory consumption at runtime
|
||||
|
||||
### Part of the [Transflow Project](http://transflow.ru/)
|
||||
|
||||
Sometimes you may need a tool to measure the size of object in your Go program at runtime. This package makes an attempt to do so. Package based on `binary.Size()` from Go standard library.
|
||||
|
||||
Features:
|
||||
- supports non-fixed size variables and struct fields: `struct`, `int`, `slice`, `string`, `map`;
|
||||
- supports complex types including structs with non-fixed size fields;
|
||||
- supports all basic types (numbers, bool);
|
||||
- supports `chan` and `interface`;
|
||||
- supports pointers;
|
||||
- implements infinite recursion detection (i.e. pointer inside struct field references to parent struct).
|
||||
|
||||
### Usage example
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
// Use latest tag.
|
||||
"github.com/DmitriyVTitov/size"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := struct {
|
||||
a int
|
||||
b string
|
||||
c bool
|
||||
d int32
|
||||
e []byte
|
||||
f [3]int64
|
||||
}{
|
||||
a: 10, // 8 bytes
|
||||
b: "Text", // 16 (string itself) + 4 = 20 bytes
|
||||
c: true, // 1 byte
|
||||
d: 25, // 4 bytes
|
||||
e: []byte{'c', 'd', 'e'}, // 24 (slice itself) + 3 = 27 bytes
|
||||
f: [3]int64{1, 2, 3}, // 3 * 8 = 24 bytes
|
||||
} // 84 + 3 (padding) = 87 bytes
|
||||
|
||||
fmt.Println(size.Of(a))
|
||||
}
|
||||
|
||||
// Output: 87
|
||||
```
|
142
vendor/github.com/DmitriyVTitov/size/size.go
generated
vendored
142
vendor/github.com/DmitriyVTitov/size/size.go
generated
vendored
|
@ -1,142 +0,0 @@
|
|||
// Package size implements run-time calculation of size of the variable.
|
||||
// Source code is based on "binary.Size()" function from Go standard library.
|
||||
// size.Of() omits size of slices, arrays and maps containers itself (24, 24 and 8 bytes).
|
||||
// When counting maps separate calculations are done for keys and values.
|
||||
package size
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Of returns the size of 'v' in bytes.
|
||||
// If there is an error during calculation, Of returns -1.
|
||||
func Of(v interface{}) int {
|
||||
// Cache with every visited pointer so we don't count two pointers
|
||||
// to the same memory twice.
|
||||
cache := make(map[uintptr]bool)
|
||||
return sizeOf(reflect.Indirect(reflect.ValueOf(v)), cache)
|
||||
}
|
||||
|
||||
// sizeOf returns the number of bytes the actual data represented by v occupies in memory.
|
||||
// If there is an error, sizeOf returns -1.
|
||||
func sizeOf(v reflect.Value, cache map[uintptr]bool) int {
|
||||
switch v.Kind() {
|
||||
|
||||
case reflect.Array:
|
||||
sum := 0
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
s := sizeOf(v.Index(i), cache)
|
||||
if s < 0 {
|
||||
return -1
|
||||
}
|
||||
sum += s
|
||||
}
|
||||
|
||||
return sum + (v.Cap()-v.Len())*int(v.Type().Elem().Size())
|
||||
|
||||
case reflect.Slice:
|
||||
// return 0 if this node has been visited already
|
||||
if cache[v.Pointer()] {
|
||||
return 0
|
||||
}
|
||||
cache[v.Pointer()] = true
|
||||
|
||||
sum := 0
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
s := sizeOf(v.Index(i), cache)
|
||||
if s < 0 {
|
||||
return -1
|
||||
}
|
||||
sum += s
|
||||
}
|
||||
|
||||
sum += (v.Cap() - v.Len()) * int(v.Type().Elem().Size())
|
||||
|
||||
return sum + int(v.Type().Size())
|
||||
|
||||
case reflect.Struct:
|
||||
sum := 0
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
s := sizeOf(v.Field(i), cache)
|
||||
if s < 0 {
|
||||
return -1
|
||||
}
|
||||
sum += s
|
||||
}
|
||||
|
||||
// Look for struct padding.
|
||||
padding := int(v.Type().Size())
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
padding -= int(v.Field(i).Type().Size())
|
||||
}
|
||||
|
||||
return sum + padding
|
||||
|
||||
case reflect.String:
|
||||
s := v.String()
|
||||
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
if cache[hdr.Data] {
|
||||
return int(v.Type().Size())
|
||||
}
|
||||
cache[hdr.Data] = true
|
||||
return len(s) + int(v.Type().Size())
|
||||
|
||||
case reflect.Ptr:
|
||||
// return Ptr size if this node has been visited already (infinite recursion)
|
||||
if cache[v.Pointer()] {
|
||||
return int(v.Type().Size())
|
||||
}
|
||||
cache[v.Pointer()] = true
|
||||
if v.IsNil() {
|
||||
return int(reflect.New(v.Type()).Type().Size())
|
||||
}
|
||||
s := sizeOf(reflect.Indirect(v), cache)
|
||||
if s < 0 {
|
||||
return -1
|
||||
}
|
||||
return s + int(v.Type().Size())
|
||||
|
||||
case reflect.Bool,
|
||||
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Int, reflect.Uint,
|
||||
reflect.Chan,
|
||||
reflect.Uintptr,
|
||||
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128,
|
||||
reflect.Func:
|
||||
return int(v.Type().Size())
|
||||
|
||||
case reflect.Map:
|
||||
// return 0 if this node has been visited already (infinite recursion)
|
||||
if cache[v.Pointer()] {
|
||||
return 0
|
||||
}
|
||||
cache[v.Pointer()] = true
|
||||
sum := 0
|
||||
keys := v.MapKeys()
|
||||
for i := range keys {
|
||||
val := v.MapIndex(keys[i])
|
||||
// calculate size of key and value separately
|
||||
sv := sizeOf(val, cache)
|
||||
if sv < 0 {
|
||||
return -1
|
||||
}
|
||||
sum += sv
|
||||
sk := sizeOf(keys[i], cache)
|
||||
if sk < 0 {
|
||||
return -1
|
||||
}
|
||||
sum += sk
|
||||
}
|
||||
// Include overhead due to unused map buckets. 10.79 comes
|
||||
// from https://golang.org/src/runtime/map.go.
|
||||
return sum + int(v.Type().Size()) + int(float64(len(keys))*10.79)
|
||||
|
||||
case reflect.Interface:
|
||||
return sizeOf(v.Elem(), cache) + int(v.Type().Size())
|
||||
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
|
@ -13,11 +13,10 @@ codeberg.org/gruf/go-bytesize
|
|||
# codeberg.org/gruf/go-byteutil v1.1.2
|
||||
## explicit; go 1.16
|
||||
codeberg.org/gruf/go-byteutil
|
||||
# codeberg.org/gruf/go-cache/v3 v3.5.5
|
||||
# codeberg.org/gruf/go-cache/v3 v3.4.4
|
||||
## explicit; go 1.19
|
||||
codeberg.org/gruf/go-cache/v3
|
||||
codeberg.org/gruf/go-cache/v3/result
|
||||
codeberg.org/gruf/go-cache/v3/simple
|
||||
codeberg.org/gruf/go-cache/v3/ttl
|
||||
# codeberg.org/gruf/go-debug v1.3.0
|
||||
## explicit; go 1.16
|
||||
|
@ -70,9 +69,6 @@ codeberg.org/gruf/go-sched
|
|||
codeberg.org/gruf/go-store/v2/kv
|
||||
codeberg.org/gruf/go-store/v2/storage
|
||||
codeberg.org/gruf/go-store/v2/util
|
||||
# github.com/DmitriyVTitov/size v1.5.0
|
||||
## explicit; go 1.14
|
||||
github.com/DmitriyVTitov/size
|
||||
# github.com/KimMachineGun/automemlimit v0.2.6
|
||||
## explicit; go 1.19
|
||||
github.com/KimMachineGun/automemlimit
|
||||
|
|
Loading…
Reference in a new issue