Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(277)

Side by Side Diff: logdog/appengine/coordinator/hierarchy/hierarchy.go

Issue 2991253004: [logdog] Remove list functionality. (Closed)
Patch Set: fix test Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 The LUCI Authors.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package hierarchy
16
17 import (
18 "crypto/sha256"
19 "encoding/base64"
20 "encoding/hex"
21
22 log "github.com/luci/luci-go/common/logging"
23 "github.com/luci/luci-go/grpc/grpcutil"
24 "github.com/luci/luci-go/logdog/appengine/coordinator"
25 "github.com/luci/luci-go/logdog/common/types"
26 "github.com/luci/luci-go/luci_config/common/cfgtypes"
27
28 ds "github.com/luci/gae/service/datastore"
29
30 "golang.org/x/net/context"
31 "google.golang.org/grpc/codes"
32 )
33
34 // componentEntity is a hierarchial component that stores a specific log path
35 // component. A given stream path is broken into components, each of which is
36 // represented by a single componentEntity.
37 //
38 // A componentEntity is keyed based on its path component name and the hash of
39 // its parent component's path. This ensures that the set of child components
40 // can be obtained for any parent component path.
41 //
42 // The child component's key is chosen to be sortable with the following
43 // preferences:
44 // - Stream components sort before path components.
45 // - Stream components sort alphanumerically. Elements that are entirely
46 // numeric will be constructed to sort numerically.
47 //
48 // Storing the component's name in its key ensures that a keys query will pull
49 // the set of elements in the correct order, requiring no non-default indexes.
50 //
51 // Writing path components is inherently idempotent, since each one is defined
52 // solely by its identity. Consequently, transactions are not necessary when
53 // writing path components.
54 //
55 // All componentEntity share a common implicit ancestor, "/".
56 //
57 // This entity is created at stream and prefix registration, and is entirely
58 // centered around being queried for log stream "directory" listings. For
59 // example:
60 //
61 // foo/bar/+/baz/qux
62 //
63 // This stream would add the following name components to the datastore, keyed
64 // (parent-key, id) as:
65 // ("/", "foo")
66 // ("/foo", "bar")
67 // ("/foo/bar", "+")
68 // ("/foo/bar/+", "baz")
69 // ("/foo/bar/+/baz", "qux")
70 //
71 // A generated property, "s", is stored to indicate whether this entry is a
72 // stream or path component.
73 type componentEntity struct {
74 // _kind is the entity's kind. This is intentionally short to mitigate i ndex
75 // bloat since this will be repeated in a lot of keys.
76 _kind string `gae:"$kind,_StreamNameComponent"`
77
78 // ID is the name component of this specific stream.
79 ID componentID `gae:"$id"`
80 }
81
82 var _ ds.PropertyLoadSaver = (*componentEntity)(nil)
83
84 func (c *componentEntity) Load(pmap ds.PropertyMap) error {
85 // Delete custom elements (added in Save).
86 delete(pmap, "s")
87 delete(pmap, "p")
88
89 return ds.GetPLS(c).Load(pmap)
90 }
91
92 func (c *componentEntity) Save(withMeta bool) (ds.PropertyMap, error) {
93 pmap, err := ds.GetPLS(c).Save(withMeta)
94 if err != nil {
95 return nil, err
96 }
97 pmap["s"] = ds.MkProperty(c.ID.stream)
98 pmap["p"] = ds.MkProperty(c.ID.parent)
99 return pmap, nil
100 }
101
102 // streamPath returns the StreamPath of this component, given its parent path.
103 //
104 // This is only valid if the component is a stream component.
105 func (c *componentEntity) streamPath(parent types.StreamPath) types.StreamPath {
106 return parent.Append(c.ID.name)
107 }
108
109 func componentEntityParent(parent types.StreamPath) string {
110 hash := sha256.Sum256([]byte(parent))
111 return hex.EncodeToString(hash[:])
112 }
113
114 func mkComponentEntity(parent types.StreamPath, name string, stream bool) *compo nentEntity {
115 return &componentEntity{
116 ID: componentID{
117 parent: componentEntityParent(parent),
118 name: name,
119 stream: stream,
120 },
121 }
122 }
123
124 // Request is a listing request to execute.
125 //
126 // It describes a hierarchy listing request. For example, given the following
127 // streams in project "qux":
128 // qux/foo/+/bar
129 // qux/foo/+/bar/baz
130 // qux/foo/bar/+/baz
131 //
132 // The following queries would return values ("$" denotes streams vs. paths):
133 // Project="", Path="": ["qux"]
134 // Project="qux", Path="": ["foo"]
135 // Project="qux", Path="foo": ["+", "bar"]
136 // Project="qux", Path="foo/+": ["bar$", "bar"]
137 // Project="qux", Path="foo/bar": ["+"]
138 // Project="qux", Path="foo/bar/+": ["baz$"]
139 //
140 // If Limit is >0, it will be used to constrain the results. Otherwise, all
141 // results will be returned.
142 //
143 // If Next is not empty, it is a datastore cursor for a continued query. If
144 // supplied, it must use the same parameters as the previous queries in the
145 // sequence.
146 type Request struct {
147 // Project is the project to list. If empty, Request will perform a
148 // project-level listing.
149 Project string
150 // PathBase is the base path within Project to list.
151 PathBase string
152 // StreamOnly, if true, only returns stream path components.
153 StreamOnly bool
154
155 // Limit, if >0, is the maximum number of results to return. If more res ults
156 // are available, the returned List will have its Next field set to a cu rsor
157 // that can be used to issue iterative requests.
158 Limit int
159 // Next, if not empty, is the start cursor for this stream.
160 Next string
161 // Skip, if >0, skips past the first Skip query results.
162 Skip int
163 }
164
165 // ListComponent is a single component element in the stream path hierarchy.
166 //
167 // This can represent a path component "path" in (path/component/+/stream)
168 // or a stream component ("stream").
169 type ListComponent struct {
170 // Name is the name of this hierarchy element.
171 Name string
172 // Stream, if true, indicates that this is a stream component.
173 Stream bool
174 }
175
176 // List is a branch of the stream path tree.
177 //
178 // It may represent either the top-level project hierarchy, or the sub-project
179 // stream space hierarchy, depending on the query base.
180 type List struct {
181 // Project is the listed project name. If empty, the list refers to the
182 // project namespace.
183 Project cfgtypes.ProjectName
184 // PathBase is the stream path base.
185 PathBase types.StreamPath
186 // Comp is the set of elements in this hierarchy result.
187 Comp []*ListComponent
188
189 // Next, if not empty, is the iterative query cursor.
190 Next string
191 }
192
193 // Path returns the StreamPath of the supplied Component.
194 func (l *List) Path(c *ListComponent) types.StreamPath {
195 return l.PathBase.Append(c.Name)
196 }
197
198 // Get performs a hierarchy query based on parameters in the supplied Request
199 // and returns the resulting List.
200 //
201 // This method will set the namespace based on the request after asserting user
202 // membership.
203 //
204 // The supplied Context should not be bound to a namespace (i.e., default
205 // namespace).
206 //
207 // If a failure is encountered, a wrapped gRPC error will be returned.
208 func Get(c context.Context, r Request) (*List, error) {
209 // If our project is empty, this is a project-level query.
210 if r.Project == "" {
211 return getProjects(c, &r)
212 }
213
214 // Build our List result.
215 //
216 // We will validate our Project and PathBase types immediately afterward s.
217 l := List{
218 Project: cfgtypes.ProjectName(r.Project),
219 PathBase: types.StreamPath(r.PathBase),
220 }
221
222 // Validate our PathBase component.
223 if err := l.PathBase.ValidatePartial(); err != nil {
224 return nil, grpcutil.Errf(codes.InvalidArgument, "invalid stream path base %q: %v", l.PathBase, err)
225 }
226
227 // Enter the supplied Project namespace. This will assert the the user h as
228 // access to the project.
229 if err := coordinator.WithProjectNamespace(&c, l.Project, coordinator.Na mespaceAccessREAD); err != nil {
230 return nil, err
231 }
232
233 // Determine our ancestor component.
234 q := ds.NewQuery("_StreamNameComponent")
235 q = q.Eq("p", componentEntityParent(l.PathBase))
236 if r.StreamOnly {
237 q = q.Eq("s", true)
238 }
239 if r.Next != "" {
240 k, err := keyForCursor(c, r.Next)
241 if err != nil {
242 return nil, grpcutil.Errf(codes.InvalidArgument, "invali d cursor: %s", err)
243 }
244 q = q.Gt("__key__", k)
245 }
246 if r.Skip > 0 {
247 q = q.Offset(int32(r.Skip))
248 }
249
250 limit := r.Limit
251 if limit > 0 {
252 q = q.Limit(int32(limit))
253 }
254
255 err := ds.Run(c, q, func(e *componentEntity) error {
256 l.Comp = append(l.Comp, &ListComponent{
257 Name: e.ID.name,
258 Stream: e.ID.stream,
259 })
260
261 if limit > 0 && len(l.Comp) >= limit {
262 l.Next = cursorForKey(c, e)
263 return ds.Stop
264 }
265 return nil
266 })
267 if err != nil {
268 log.WithError(err).Errorf(c, "Failed to execute hierarhcy query. ")
269 return nil, grpcutil.Internal
270 }
271 return &l, nil
272 }
273
274 // keyForCursor returns the component key for the supplied cursor.
275 //
276 // If the cursor string is not valid, an error will be returned.
277 func keyForCursor(c context.Context, curs string) (*ds.Key, error) {
278 d, err := base64.URLEncoding.DecodeString(curs)
279 if err != nil {
280 return nil, err
281 }
282
283 return ds.NewKey(c, "_StreamNameComponent", string(d), 0, nil), nil
284 }
285
286 // cursorForKey returns a cursor for the supplied componentID. This cursor will
287 // start new queries at the component immediately following this ID.
288 func cursorForKey(c context.Context, e *componentEntity) string {
289 key := ds.KeyForObj(c, e)
290 return base64.URLEncoding.EncodeToString([]byte(key.StringID()))
291 }
OLDNEW
« no previous file with comments | « logdog/appengine/coordinator/hierarchy/component_test.go ('k') | logdog/appengine/coordinator/hierarchy/hierarchy_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698