-
Notifications
You must be signed in to change notification settings - Fork 0
/
context.go
194 lines (173 loc) · 6.18 KB
/
context.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package config abstracts access to Logdog service and project configuration.
//
// Sync(...) assumes the context has a cfgclient implementation and a read-write
// datastore. All other methods need only read-only datastore.
package config
import (
"context"
"sync"
"time"
"go.chromium.org/luci/common/data/caching/lazyslot"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/config"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/logdog/api/config/svcconfig"
)
var (
// ErrInvalidConfig is returned when the configuration exists, but is invalid.
ErrInvalidConfig = errors.New("invalid configuration")
)
// Store caches configs in memory to avoid hitting cfgclient all the time.
//
// Keep at as a global variable and install into contexts via WithStore.
type Store struct {
// NoCache disables in-process caching (useful in tests).
NoCache bool
service lazyslot.Slot // caches the main service config
m sync.RWMutex // protects 'projects'
projects map[string]*lazyslot.Slot // caches project configs
}
// projectCacheSlot returns a slot with a project config cache.
func (s *Store) projectCacheSlot(projectID string) *lazyslot.Slot {
s.m.RLock()
slot, _ := s.projects[projectID]
s.m.RUnlock()
if slot != nil {
return slot
}
s.m.Lock()
defer s.m.Unlock()
if slot, _ = s.projects[projectID]; slot != nil {
return slot
}
slot = &lazyslot.Slot{}
if s.projects == nil {
s.projects = make(map[string]*lazyslot.Slot, 1)
}
s.projects[projectID] = slot
return slot
}
var storeKey = "LogDog config.Store"
// store returns the installed store or panics if it's not installed.
func store(ctx context.Context) *Store {
s, _ := ctx.Value(&storeKey).(*Store)
if s == nil {
panic("config.Store is not in the context")
}
return s
}
// WithStore installs a store that caches configs in memory.
func WithStore(ctx context.Context, s *Store) context.Context {
return context.WithValue(ctx, &storeKey, s)
}
// Middleware returns a middleware that installs `s` into requests' context.
func Middleware(s *Store) router.Middleware {
return func(ctx *router.Context, next router.Handler) {
ctx.Context = WithStore(ctx.Context, s)
next(ctx)
}
}
// Config loads and returns the service configuration.
func Config(ctx context.Context) (*svcconfig.Config, error) {
store := store(ctx)
if store.NoCache {
return fetchServiceConfig(ctx)
}
cached, err := store.service.Get(ctx, func(prev interface{}) (val interface{}, exp time.Duration, err error) {
logging.Infof(ctx, "Cache miss for services.cfg, fetching it from datastore...")
cfg, err := fetchServiceConfig(ctx)
return cfg, time.Minute, err
})
if err != nil {
return nil, err
}
return cached.(*svcconfig.Config), nil
}
// fetchServiceConfig fetches the service config from the datastore.
func fetchServiceConfig(ctx context.Context) (*svcconfig.Config, error) {
var cfg svcconfig.Config
switch err := fromDatastore(ctx, serviceConfigKind, serviceConfigPath, &cfg); {
case transient.Tag.In(err):
return nil, err
case err == datastore.ErrNoSuchEntity:
return nil, config.ErrNoConfig
case err != nil:
logging.Errorf(ctx, "Broken service config in the datastore: %s", err)
return nil, ErrInvalidConfig
default:
return &cfg, nil
}
}
// missingProjectMarker is cached instead of *svcconfig.ProjectConfig if the
// project is missing to avoid hitting datastore all the time when accessing
// missing projects.
//
// Note: strictly speaking caching all missing projects forever in
// Store.projects introduces a DoS attack vector. But this code is scheduled for
// removal when Logdog is integrated with LUCI Realms, so it's fine to ignore
// this problem for now.
var missingProjectMarker = "missing project"
// ProjectConfig loads the project config protobuf from the config service.
//
// This function will return following errors:
// - nil, if the project exists and the configuration successfully loaded
// - config.ErrNoConfig if the project configuration was not present.
// - ErrInvalidConfig if the project configuration was present, but could not
// be loaded.
// - Some other error if an error occurred that does not fit one of the
// previous categories.
func ProjectConfig(ctx context.Context, projectID string) (*svcconfig.ProjectConfig, error) {
store := store(ctx)
if projectID == "" {
return nil, config.ErrNoConfig
}
if store.NoCache {
return fetchProjectConfig(ctx, projectID)
}
cached, err := store.projectCacheSlot(projectID).Get(ctx, func(prev interface{}) (val interface{}, exp time.Duration, err error) {
logging.Infof(ctx, "Cache miss for %q project config, fetching it...", projectID)
cfg, err := fetchProjectConfig(ctx, projectID)
if err == config.ErrNoConfig {
return &missingProjectMarker, time.Minute, nil
}
return cfg, time.Minute, err
})
if err != nil {
return nil, err
}
if cached == &missingProjectMarker {
return nil, config.ErrNoConfig
}
return cached.(*svcconfig.ProjectConfig), nil
}
// fetchProjectConfig fetches a project config from the datastore.
func fetchProjectConfig(ctx context.Context, projectID string) (*svcconfig.ProjectConfig, error) {
var cfg svcconfig.ProjectConfig
switch err := fromDatastore(ctx, projectConfigKind, projectID, &cfg); {
case transient.Tag.In(err):
return nil, err
case err == datastore.ErrNoSuchEntity:
return nil, config.ErrNoConfig
case err != nil:
logging.Errorf(ctx, "Broken project config for %q in the datastore: %s", projectID, err)
return nil, ErrInvalidConfig
default:
return &cfg, nil
}
}