2021-09-28 13:01:53 +00:00
|
|
|
package cached_mysql
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-02-15 19:07:51 +00:00
|
|
|
"encoding/json"
|
2022-01-18 01:52:09 +00:00
|
|
|
"fmt"
|
2022-02-15 19:07:51 +00:00
|
|
|
"reflect"
|
2021-09-28 13:01:53 +00:00
|
|
|
"time"
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
|
2021-09-28 13:01:53 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2022-02-15 19:07:51 +00:00
|
|
|
"github.com/jinzhu/copier"
|
2021-11-29 15:51:57 +00:00
|
|
|
"github.com/patrickmn/go-cache"
|
2021-09-28 13:01:53 +00:00
|
|
|
)
|
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
const (
|
2023-10-25 22:20:27 +00:00
|
|
|
appConfigKey = "AppConfig:%s"
|
|
|
|
defaultAppConfigExpiration = 1 * time.Second
|
|
|
|
packsHostKey = "Packs:host:%d"
|
|
|
|
defaultPacksExpiration = 1 * time.Minute
|
|
|
|
scheduledQueriesKey = "ScheduledQueries:pack:%d"
|
|
|
|
defaultScheduledQueriesExpiration = 1 * time.Minute
|
|
|
|
teamAgentOptionsKey = "TeamAgentOptions:team:%d"
|
|
|
|
defaultTeamAgentOptionsExpiration = 1 * time.Minute
|
|
|
|
teamFeaturesKey = "TeamFeatures:team:%d"
|
|
|
|
defaultTeamFeaturesExpiration = 1 * time.Minute
|
|
|
|
teamMDMConfigKey = "TeamMDMConfig:team:%d"
|
|
|
|
defaultTeamMDMConfigExpiration = 1 * time.Minute
|
|
|
|
queryByNameKey = "QueryByName:team:%d:%s"
|
|
|
|
defaultQueryByNameExpiration = 1 * time.Second
|
|
|
|
queryResultsCountKey = "QueryResultsCount:%d"
|
|
|
|
defaultQueryResultsCountExpiration = 1 * time.Second
|
2022-02-15 19:07:51 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// cloner represents any type that can clone itself. Used by types to provide a more efficient clone method.
|
|
|
|
type cloner interface {
|
|
|
|
Clone() (interface{}, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
func clone(v interface{}) (interface{}, error) {
|
|
|
|
if cloner, ok := v.(cloner); ok {
|
|
|
|
return cloner.Clone()
|
|
|
|
}
|
|
|
|
|
2023-11-07 14:51:55 +00:00
|
|
|
// TODO(mna): consider making implementation of the cloner interface
|
|
|
|
// mandatory, and panic/fail loudly if not implemented. Reflection-based deep
|
|
|
|
// cloning has significant performance issues at scale (better yet - make the
|
|
|
|
// cache accept/return cloner types instead of interface{}).
|
|
|
|
|
2022-10-03 16:29:20 +00:00
|
|
|
if v == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
// Use reflection to initialize a clone of v of the same type.
|
|
|
|
vv := reflect.ValueOf(v)
|
|
|
|
|
|
|
|
// If the value is a pointer, then calling reflect.New on it will result in a double pointer.
|
|
|
|
// Instead, dereference the pointer first.
|
|
|
|
isPtr := false
|
|
|
|
if vv.Kind() == reflect.Ptr {
|
|
|
|
isPtr = true
|
2022-10-03 16:29:20 +00:00
|
|
|
if vv.IsNil() {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2022-02-15 19:07:51 +00:00
|
|
|
vv = vv.Elem()
|
|
|
|
}
|
|
|
|
|
|
|
|
clone := reflect.New(vv.Type())
|
|
|
|
|
2022-10-17 19:03:49 +00:00
|
|
|
err := copier.CopyWithOption(clone.Interface(), v, copier.Option{DeepCopy: true, IgnoreEmpty: true})
|
2022-02-15 19:07:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if isPtr {
|
|
|
|
return clone.Interface(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// The value was not a pointer. Need to dereference it before returning.
|
|
|
|
return clone.Elem().Interface(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// cloneCache wraps the in memory cache with one that clones items before returning them.
|
|
|
|
type cloneCache struct {
|
|
|
|
*cache.Cache
|
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
func (c *cloneCache) Get(ctx context.Context, k string) (interface{}, bool) {
|
|
|
|
if ctxdb.IsCachedMysqlBypassed(ctx) {
|
|
|
|
// cache miss if the caller explicitly asked to bypass the cache
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
x, found := c.Cache.Get(k)
|
|
|
|
if !found {
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
clone, err := clone(x)
|
|
|
|
if err != nil {
|
|
|
|
// Unfortunely, we can't return an error here. Return a cache miss instead of panic'ing.
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
return clone, true
|
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
func (c *cloneCache) Set(ctx context.Context, k string, x interface{}, d time.Duration) {
|
2022-02-15 19:07:51 +00:00
|
|
|
clone, err := clone(x)
|
|
|
|
if err != nil {
|
2023-11-29 15:09:37 +00:00
|
|
|
// Unfortunately, we can't return an error here. Skip caching it if clone
|
|
|
|
// fails, but ensure that we clear any existing cached item for this key,
|
|
|
|
// as the call to Set indicates the cache is now stale.
|
|
|
|
c.Cache.Delete(k)
|
2022-02-15 19:07:51 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Cache.Set(k, clone, d)
|
|
|
|
}
|
|
|
|
|
2021-09-28 13:01:53 +00:00
|
|
|
type cachedMysql struct {
|
|
|
|
fleet.Datastore
|
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
c *cloneCache
|
2022-01-18 01:52:09 +00:00
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
appConfigExp time.Duration
|
2023-10-25 22:20:27 +00:00
|
|
|
packsExp time.Duration
|
|
|
|
scheduledQueriesExp time.Duration
|
|
|
|
teamAgentOptionsExp time.Duration
|
|
|
|
teamFeaturesExp time.Duration
|
|
|
|
teamMDMConfigExp time.Duration
|
|
|
|
queryByNameExp time.Duration
|
|
|
|
queryResultsCountExp time.Duration
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
|
|
|
|
2022-01-18 01:52:09 +00:00
|
|
|
type Option func(*cachedMysql)
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
func WithAppConfigExpiration(d time.Duration) Option {
|
|
|
|
return func(o *cachedMysql) {
|
|
|
|
o.appConfigExp = d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
func WithPacksExpiration(d time.Duration) Option {
|
|
|
|
return func(o *cachedMysql) {
|
|
|
|
o.packsExp = d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithScheduledQueriesExpiration(d time.Duration) Option {
|
2022-01-18 01:52:09 +00:00
|
|
|
return func(o *cachedMysql) {
|
2022-02-15 19:07:51 +00:00
|
|
|
o.scheduledQueriesExp = d
|
2022-01-18 01:52:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
func WithTeamAgentOptionsExpiration(d time.Duration) Option {
|
2022-01-18 01:52:09 +00:00
|
|
|
return func(o *cachedMysql) {
|
2022-02-15 19:07:51 +00:00
|
|
|
o.teamAgentOptionsExp = d
|
2022-01-18 01:52:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-30 11:13:09 +00:00
|
|
|
func WithTeamFeaturesExpiration(d time.Duration) Option {
|
|
|
|
return func(o *cachedMysql) {
|
|
|
|
o.teamFeaturesExp = d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-25 20:03:40 +00:00
|
|
|
func WithTeamMDMConfigExpiration(d time.Duration) Option {
|
|
|
|
return func(o *cachedMysql) {
|
|
|
|
o.teamMDMConfigExp = d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-18 01:52:09 +00:00
|
|
|
func New(ds fleet.Datastore, opts ...Option) fleet.Datastore {
|
|
|
|
c := &cachedMysql{
|
2023-10-25 22:20:27 +00:00
|
|
|
Datastore: ds,
|
|
|
|
c: &cloneCache{cache.New(5*time.Minute, 10*time.Minute)},
|
2023-11-29 15:09:37 +00:00
|
|
|
appConfigExp: defaultAppConfigExpiration,
|
2023-10-25 22:20:27 +00:00
|
|
|
packsExp: defaultPacksExpiration,
|
|
|
|
scheduledQueriesExp: defaultScheduledQueriesExpiration,
|
|
|
|
teamAgentOptionsExp: defaultTeamAgentOptionsExpiration,
|
|
|
|
teamFeaturesExp: defaultTeamFeaturesExpiration,
|
2023-11-07 14:51:55 +00:00
|
|
|
teamMDMConfigExp: defaultTeamMDMConfigExpiration,
|
2023-10-25 22:20:27 +00:00
|
|
|
queryByNameExp: defaultQueryByNameExpiration,
|
|
|
|
queryResultsCountExp: defaultQueryResultsCountExpiration,
|
2022-01-18 01:52:09 +00:00
|
|
|
}
|
|
|
|
for _, fn := range opts {
|
|
|
|
fn(c)
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
2022-01-18 01:52:09 +00:00
|
|
|
return c
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ds *cachedMysql) NewAppConfig(ctx context.Context, info *fleet.AppConfig) (*fleet.AppConfig, error) {
|
|
|
|
ac, err := ds.Datastore.NewAppConfig(ctx, info)
|
|
|
|
if err != nil {
|
2021-11-15 14:11:38 +00:00
|
|
|
return nil, err
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, appConfigKey, ac, ds.appConfigExp)
|
2021-09-28 13:01:53 +00:00
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
return ac, nil
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ds *cachedMysql) AppConfig(ctx context.Context) (*fleet.AppConfig, error) {
|
2023-11-29 15:09:37 +00:00
|
|
|
if x, found := ds.c.Get(ctx, appConfigKey); found {
|
2022-02-15 19:07:51 +00:00
|
|
|
ac, ok := x.(*fleet.AppConfig)
|
|
|
|
if ok {
|
|
|
|
return ac, nil
|
|
|
|
}
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
|
|
|
|
2021-11-29 15:51:57 +00:00
|
|
|
ac, err := ds.Datastore.AppConfig(ctx)
|
2021-09-28 13:01:53 +00:00
|
|
|
if err != nil {
|
2021-11-15 14:11:38 +00:00
|
|
|
return nil, err
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, appConfigKey, ac, ds.appConfigExp)
|
2021-09-28 13:01:53 +00:00
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
return ac, nil
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ds *cachedMysql) SaveAppConfig(ctx context.Context, info *fleet.AppConfig) error {
|
|
|
|
err := ds.Datastore.SaveAppConfig(ctx, info)
|
|
|
|
if err != nil {
|
2021-11-15 14:11:38 +00:00
|
|
|
return err
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, appConfigKey, info, ds.appConfigExp)
|
2021-11-29 15:51:57 +00:00
|
|
|
|
|
|
|
return nil
|
2021-09-28 13:01:53 +00:00
|
|
|
}
|
2022-01-18 01:52:09 +00:00
|
|
|
|
|
|
|
func (ds *cachedMysql) ListPacksForHost(ctx context.Context, hid uint) ([]*fleet.Pack, error) {
|
2022-02-15 19:07:51 +00:00
|
|
|
key := fmt.Sprintf(packsHostKey, hid)
|
2023-11-29 15:09:37 +00:00
|
|
|
if x, found := ds.c.Get(ctx, key); found {
|
2022-02-15 19:07:51 +00:00
|
|
|
cachedPacks, ok := x.([]*fleet.Pack)
|
2022-01-18 01:52:09 +00:00
|
|
|
if ok {
|
2022-02-15 19:07:51 +00:00
|
|
|
return cachedPacks, nil
|
2022-01-18 01:52:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
packs, err := ds.Datastore.ListPacksForHost(ctx, hid)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, key, packs, ds.packsExp)
|
2022-01-18 01:52:09 +00:00
|
|
|
|
|
|
|
return packs, nil
|
|
|
|
}
|
|
|
|
|
2022-11-23 15:04:06 +00:00
|
|
|
func (ds *cachedMysql) ListScheduledQueriesInPack(ctx context.Context, packID uint) (fleet.ScheduledQueryList, error) {
|
2022-02-15 19:07:51 +00:00
|
|
|
key := fmt.Sprintf(scheduledQueriesKey, packID)
|
2023-11-29 15:09:37 +00:00
|
|
|
if x, found := ds.c.Get(ctx, key); found {
|
2022-11-23 15:04:06 +00:00
|
|
|
scheduledQueries, ok := x.(fleet.ScheduledQueryList)
|
2022-01-18 01:52:09 +00:00
|
|
|
if ok {
|
2022-02-15 19:07:51 +00:00
|
|
|
return scheduledQueries, nil
|
2022-01-18 01:52:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
scheduledQueries, err := ds.Datastore.ListScheduledQueriesInPack(ctx, packID)
|
2022-01-18 01:52:09 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, key, scheduledQueries, ds.scheduledQueriesExp)
|
2022-01-18 01:52:09 +00:00
|
|
|
|
|
|
|
return scheduledQueries, nil
|
|
|
|
}
|
2022-02-15 19:07:51 +00:00
|
|
|
|
|
|
|
func (ds *cachedMysql) TeamAgentOptions(ctx context.Context, teamID uint) (*json.RawMessage, error) {
|
|
|
|
key := fmt.Sprintf(teamAgentOptionsKey, teamID)
|
2023-11-29 15:09:37 +00:00
|
|
|
if x, found := ds.c.Get(ctx, key); found {
|
2022-02-15 19:07:51 +00:00
|
|
|
if agentOptions, ok := x.(*json.RawMessage); ok {
|
|
|
|
return agentOptions, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
agentOptions, err := ds.Datastore.TeamAgentOptions(ctx, teamID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, key, agentOptions, ds.teamAgentOptionsExp)
|
2022-02-15 19:07:51 +00:00
|
|
|
|
|
|
|
return agentOptions, nil
|
|
|
|
}
|
|
|
|
|
2022-08-30 11:13:09 +00:00
|
|
|
func (ds *cachedMysql) TeamFeatures(ctx context.Context, teamID uint) (*fleet.Features, error) {
|
|
|
|
key := fmt.Sprintf(teamFeaturesKey, teamID)
|
2023-11-29 15:09:37 +00:00
|
|
|
if x, found := ds.c.Get(ctx, key); found {
|
2022-08-30 11:13:09 +00:00
|
|
|
if features, ok := x.(*fleet.Features); ok {
|
|
|
|
return features, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
features, err := ds.Datastore.TeamFeatures(ctx, teamID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, key, features, ds.teamFeaturesExp)
|
2022-08-30 11:13:09 +00:00
|
|
|
|
|
|
|
return features, nil
|
|
|
|
}
|
|
|
|
|
2023-01-25 20:03:40 +00:00
|
|
|
func (ds *cachedMysql) TeamMDMConfig(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) {
|
|
|
|
key := fmt.Sprintf(teamMDMConfigKey, teamID)
|
2023-11-29 15:09:37 +00:00
|
|
|
if x, found := ds.c.Get(ctx, key); found {
|
2023-01-25 20:03:40 +00:00
|
|
|
if cfg, ok := x.(*fleet.TeamMDM); ok {
|
|
|
|
return cfg, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg, err := ds.Datastore.TeamMDMConfig(ctx, teamID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-11-07 14:51:55 +00:00
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, key, cfg, ds.teamMDMConfigExp)
|
2023-11-07 14:51:55 +00:00
|
|
|
|
2023-01-25 20:03:40 +00:00
|
|
|
return cfg, nil
|
|
|
|
}
|
|
|
|
|
2022-02-15 19:07:51 +00:00
|
|
|
func (ds *cachedMysql) SaveTeam(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
|
|
|
team, err := ds.Datastore.SaveTeam(ctx, team)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-08-30 11:13:09 +00:00
|
|
|
agentOptionsKey := fmt.Sprintf(teamAgentOptionsKey, team.ID)
|
|
|
|
featuresKey := fmt.Sprintf(teamFeaturesKey, team.ID)
|
2023-01-25 20:03:40 +00:00
|
|
|
mdmConfigKey := fmt.Sprintf(teamMDMConfigKey, team.ID)
|
2022-02-15 19:07:51 +00:00
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, agentOptionsKey, team.Config.AgentOptions, ds.teamAgentOptionsExp)
|
|
|
|
ds.c.Set(ctx, featuresKey, &team.Config.Features, ds.teamFeaturesExp)
|
|
|
|
ds.c.Set(ctx, mdmConfigKey, &team.Config.MDM, ds.teamMDMConfigExp)
|
2022-02-15 19:07:51 +00:00
|
|
|
|
|
|
|
return team, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ds *cachedMysql) DeleteTeam(ctx context.Context, teamID uint) error {
|
|
|
|
err := ds.Datastore.DeleteTeam(ctx, teamID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-08-30 11:13:09 +00:00
|
|
|
agentOptionsKey := fmt.Sprintf(teamAgentOptionsKey, teamID)
|
|
|
|
featuresKey := fmt.Sprintf(teamFeaturesKey, teamID)
|
2023-01-25 20:03:40 +00:00
|
|
|
mdmConfigKey := fmt.Sprintf(teamMDMConfigKey, teamID)
|
2022-02-15 19:07:51 +00:00
|
|
|
|
2022-08-30 11:13:09 +00:00
|
|
|
ds.c.Delete(agentOptionsKey)
|
|
|
|
ds.c.Delete(featuresKey)
|
2023-01-25 20:03:40 +00:00
|
|
|
ds.c.Delete(mdmConfigKey)
|
2022-02-15 19:07:51 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-25 22:20:27 +00:00
|
|
|
|
|
|
|
func (ds *cachedMysql) QueryByName(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
|
|
|
|
teamID_ := uint(0) // global team is 0
|
|
|
|
if teamID != nil {
|
|
|
|
teamID_ = *teamID
|
|
|
|
}
|
|
|
|
key := fmt.Sprintf(queryByNameKey, teamID_, name)
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
if x, found := ds.c.Get(ctx, key); found {
|
2023-10-25 22:20:27 +00:00
|
|
|
if query, ok := x.(*fleet.Query); ok {
|
|
|
|
return query, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
query, err := ds.Datastore.QueryByName(ctx, teamID, name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, key, query, ds.queryByNameExp)
|
2023-10-25 22:20:27 +00:00
|
|
|
|
|
|
|
return query, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ds *cachedMysql) ResultCountForQuery(ctx context.Context, queryID uint) (int, error) {
|
|
|
|
key := fmt.Sprintf(queryResultsCountKey, queryID)
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
if x, found := ds.c.Get(ctx, key); found {
|
2023-10-25 22:20:27 +00:00
|
|
|
if count, ok := x.(int); ok {
|
|
|
|
return count, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
count, err := ds.Datastore.ResultCountForQuery(ctx, queryID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
2023-11-29 15:09:37 +00:00
|
|
|
ds.c.Set(ctx, key, count, ds.queryResultsCountExp)
|
2023-10-25 22:20:27 +00:00
|
|
|
|
|
|
|
return count, nil
|
|
|
|
}
|