~juju-qa/ubuntu/xenial/juju/xenial-2.0-beta3

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmstore.v5-unstable/internal/entitycache/cache_test.go

  • Committer: Martin Packman
  • Date: 2016-03-30 19:31:08 UTC
  • mfrom: (1.1.41)
  • Revision ID: martin.packman@canonical.com-20160330193108-h9iz3ak334uk0z5r
Merge new upstream source 2.0~beta3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
package entitycache_test
 
2
 
 
3
import (
 
4
        "fmt"
 
5
        "reflect"
 
6
        "strings"
 
7
        "sync"
 
8
        "time"
 
9
 
 
10
        jc "github.com/juju/testing/checkers"
 
11
        gc "gopkg.in/check.v1"
 
12
        "gopkg.in/errgo.v1"
 
13
        "gopkg.in/juju/charm.v6-unstable"
 
14
        "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
 
15
 
 
16
        "gopkg.in/juju/charmstore.v5-unstable/internal/entitycache"
 
17
        "gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
 
18
)
 
19
 
 
20
var _ = gc.Suite(&suite{})
 
21
 
 
22
type suite struct{}
 
23
 
 
24
type entityQuery struct {
 
25
        url    *charm.URL
 
26
        fields map[string]int
 
27
        reply  chan entityReply
 
28
}
 
29
 
 
30
type entityReply struct {
 
31
        entity *mongodoc.Entity
 
32
        err    error
 
33
}
 
34
 
 
35
type baseEntityQuery struct {
 
36
        url    *charm.URL
 
37
        fields map[string]int
 
38
        reply  chan baseEntityReply
 
39
}
 
40
 
 
41
type baseEntityReply struct {
 
42
        entity *mongodoc.BaseEntity
 
43
        err    error
 
44
}
 
45
 
 
46
func (*suite) TestEntityIssuesBaseEntityQueryConcurrently(c *gc.C) {
 
47
        store := newChanStore()
 
48
        cache := entitycache.New(store)
 
49
        defer cache.Close()
 
50
        cache.AddBaseEntityFields(map[string]int{"name": 1})
 
51
 
 
52
        entity := &mongodoc.Entity{
 
53
                URL:      charm.MustParseURL("~bob/wordpress-1"),
 
54
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
55
                BlobName: "w1",
 
56
                Size:     99,
 
57
        }
 
58
        baseEntity := &mongodoc.BaseEntity{
 
59
                URL:  charm.MustParseURL("~bob/wordpress"),
 
60
                Name: "wordpress",
 
61
        }
 
62
        queryDone := make(chan struct{})
 
63
        go func() {
 
64
                defer close(queryDone)
 
65
                e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), map[string]int{"blobname": 1})
 
66
                c.Check(err, gc.IsNil)
 
67
                c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields("blobname")))
 
68
        }()
 
69
 
 
70
        // Acquire both the queries before replying so that we know they've been
 
71
        // issued concurrently.
 
72
        query1 := <-store.entityqc
 
73
        c.Assert(query1.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-1"))
 
74
        c.Assert(query1.fields, jc.DeepEquals, entityFields("blobname"))
 
75
        query2 := <-store.baseEntityqc
 
76
        c.Assert(query2.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress"))
 
77
        c.Assert(query2.fields, jc.DeepEquals, baseEntityFields("name"))
 
78
        query1.reply <- entityReply{
 
79
                entity: entity,
 
80
        }
 
81
        query2.reply <- baseEntityReply{
 
82
                entity: baseEntity,
 
83
        }
 
84
        <-queryDone
 
85
 
 
86
        // Accessing the same entity again and the base entity should
 
87
        // not call any method on the store - if it does, then it'll send
 
88
        // on the query channels and we won't receive it, so the test
 
89
        // will deadlock.
 
90
        e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), map[string]int{"baseurl": 1, "blobname": 1})
 
91
        c.Check(err, gc.IsNil)
 
92
        c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields("blobname")))
 
93
 
 
94
        be, err := cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), map[string]int{"name": 1})
 
95
        c.Check(err, gc.IsNil)
 
96
        c.Check(be, jc.DeepEquals, selectBaseEntityFields(baseEntity, baseEntityFields("name")))
 
97
}
 
98
 
 
99
func (*suite) TestEntityIssuesBaseEntityQuerySequentiallyForPromulgatedURL(c *gc.C) {
 
100
        store := newChanStore()
 
101
        cache := entitycache.New(store)
 
102
        defer cache.Close()
 
103
        cache.AddBaseEntityFields(map[string]int{"name": 1})
 
104
 
 
105
        entity := &mongodoc.Entity{
 
106
                URL:            charm.MustParseURL("~bob/wordpress-1"),
 
107
                PromulgatedURL: charm.MustParseURL("wordpress-5"),
 
108
                BaseURL:        charm.MustParseURL("~bob/wordpress"),
 
109
                BlobName:       "w1",
 
110
                Size:           1,
 
111
        }
 
112
        baseEntity := &mongodoc.BaseEntity{
 
113
                URL:  charm.MustParseURL("~bob/wordpress"),
 
114
                Name: "wordpress",
 
115
        }
 
116
        queryDone := make(chan struct{})
 
117
        go func() {
 
118
                defer close(queryDone)
 
119
                e, err := cache.Entity(charm.MustParseURL("wordpress-1"), map[string]int{"blobname": 1})
 
120
                c.Check(err, gc.IsNil)
 
121
                c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields("blobname")))
 
122
        }()
 
123
 
 
124
        // Acquire both the queries before replying so that we know they've been
 
125
        // issued concurrently.
 
126
        query1 := <-store.entityqc
 
127
        c.Assert(query1.url, jc.DeepEquals, charm.MustParseURL("wordpress-1"))
 
128
        c.Assert(query1.fields, jc.DeepEquals, entityFields("blobname"))
 
129
        query1.reply <- entityReply{
 
130
                entity: entity,
 
131
        }
 
132
        <-queryDone
 
133
 
 
134
        // The base entity query is only issued when the original entity
 
135
        // is received. We can tell this because the URL in the query
 
136
        // contains the ~bob user which can't be inferred from the
 
137
        // original URL.
 
138
        query2 := <-store.baseEntityqc
 
139
        c.Assert(query2.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress"))
 
140
        c.Assert(query2.fields, jc.DeepEquals, baseEntityFields("name"))
 
141
        query2.reply <- baseEntityReply{
 
142
                entity: baseEntity,
 
143
        }
 
144
 
 
145
        // Accessing the same entity again and the base entity should
 
146
        // not call any method on the store - if it does, then it'll send
 
147
        // on the query channels and we won't receive it, so the test
 
148
        // will deadlock.
 
149
        e, err := cache.Entity(charm.MustParseURL("wordpress-1"), map[string]int{"baseurl": 1, "blobname": 1})
 
150
        c.Check(err, gc.IsNil)
 
151
        c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields("blobname")))
 
152
 
 
153
        be, err := cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), map[string]int{"name": 1})
 
154
        c.Check(err, gc.IsNil)
 
155
        c.Check(be, jc.DeepEquals, selectBaseEntityFields(baseEntity, baseEntityFields("name")))
 
156
}
 
157
 
 
158
func (*suite) TestFetchWhenFieldsChangeBeforeQueryResult(c *gc.C) {
 
159
        store := newChanStore()
 
160
        cache := entitycache.New(store)
 
161
        defer cache.Close()
 
162
        cache.AddBaseEntityFields(map[string]int{"name": 1})
 
163
 
 
164
        entity := &mongodoc.Entity{
 
165
                URL:      charm.MustParseURL("~bob/wordpress-1"),
 
166
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
167
                BlobName: "w1",
 
168
        }
 
169
        baseEntity := &mongodoc.BaseEntity{
 
170
                URL:  charm.MustParseURL("~bob/wordpress"),
 
171
                Name: "wordpress",
 
172
        }
 
173
        store.findBaseEntity = func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
174
                c.Check(url, jc.DeepEquals, baseEntity.URL)
 
175
                c.Check(fields, jc.DeepEquals, baseEntityFields("name"))
 
176
                return baseEntity, nil
 
177
        }
 
178
 
 
179
        queryDone := make(chan struct{})
 
180
        go func() {
 
181
                defer close(queryDone)
 
182
                e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
183
                c.Check(err, gc.IsNil)
 
184
                c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields()))
 
185
        }()
 
186
 
 
187
        query1 := <-store.entityqc
 
188
        c.Assert(query1.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-1"))
 
189
        c.Assert(query1.fields, jc.DeepEquals, entityFields())
 
190
        // Before we send the reply, make another query with different fields,
 
191
        // so the version changes.
 
192
        entity2 := &mongodoc.Entity{
 
193
                URL:      charm.MustParseURL("~bob/wordpress-1"),
 
194
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
195
                BlobName: "w1",
 
196
                Size:     99,
 
197
        }
 
198
        query2Done := make(chan struct{})
 
199
        go func() {
 
200
                defer close(query2Done)
 
201
                // Note the extra "size" field.
 
202
                e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), map[string]int{"size": 1})
 
203
                c.Check(err, gc.IsNil)
 
204
                c.Check(e, jc.DeepEquals, selectEntityFields(entity2, entityFields("size")))
 
205
        }()
 
206
        // The second query should be sent immediately without waiting
 
207
        // for the first because it invalidates the cache..
 
208
        query2 := <-store.entityqc
 
209
        c.Assert(query2.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-1"))
 
210
        c.Assert(query2.fields, jc.DeepEquals, entityFields("size"))
 
211
        query2.reply <- entityReply{
 
212
                entity: entity2,
 
213
        }
 
214
        <-query2Done
 
215
 
 
216
        // Reply to the first query and make sure that it completed.
 
217
        query1.reply <- entityReply{
 
218
                entity: entity,
 
219
        }
 
220
        <-queryDone
 
221
 
 
222
        // Accessing the same entity again not call any method on the store, so close the query channels
 
223
        // to ensure it doesn't.
 
224
        close(store.entityqc)
 
225
        close(store.baseEntityqc)
 
226
        e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
227
        c.Check(err, gc.IsNil)
 
228
        c.Check(e, jc.DeepEquals, selectEntityFields(entity2, entityFields("size")))
 
229
}
 
230
 
 
231
func (*suite) TestSecondFetchesWaitForFirst(c *gc.C) {
 
232
        store := newChanStore()
 
233
        cache := entitycache.New(store)
 
234
        defer cache.Close()
 
235
        cache.AddBaseEntityFields(map[string]int{"name": 1})
 
236
 
 
237
        entity := &mongodoc.Entity{
 
238
                URL:      charm.MustParseURL("~bob/wordpress-1"),
 
239
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
240
                BlobName: "w1",
 
241
        }
 
242
        baseEntity := &mongodoc.BaseEntity{
 
243
                URL:  charm.MustParseURL("~bob/wordpress"),
 
244
                Name: "wordpress",
 
245
        }
 
246
        store.findBaseEntity = func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
247
                c.Check(url, jc.DeepEquals, baseEntity.URL)
 
248
                c.Check(fields, jc.DeepEquals, baseEntityFields("name"))
 
249
                return baseEntity, nil
 
250
        }
 
251
 
 
252
        var initialRequestGroup sync.WaitGroup
 
253
        initialRequestGroup.Add(1)
 
254
        go func() {
 
255
                defer initialRequestGroup.Done()
 
256
                e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
257
                c.Check(err, gc.IsNil)
 
258
                c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields()))
 
259
        }()
 
260
 
 
261
        query1 := <-store.entityqc
 
262
        c.Assert(query1.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-1"))
 
263
        c.Assert(query1.fields, jc.DeepEquals, entityFields())
 
264
 
 
265
        // Send some more queries for the same charm. These should not send a
 
266
        // store request but instead wait for the first one.
 
267
        for i := 0; i < 5; i++ {
 
268
                initialRequestGroup.Add(1)
 
269
                go func() {
 
270
                        defer initialRequestGroup.Done()
 
271
                        e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
272
                        c.Check(err, gc.IsNil)
 
273
                        c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields()))
 
274
                }()
 
275
        }
 
276
        select {
 
277
        case q := <-store.entityqc:
 
278
                c.Fatalf("unexpected store query %#v", q)
 
279
        case <-time.After(10 * time.Millisecond):
 
280
        }
 
281
 
 
282
        entity2 := &mongodoc.Entity{
 
283
                URL:      charm.MustParseURL("~bob/wordpress-2"),
 
284
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
285
                BlobName: "w2",
 
286
        }
 
287
        // Send another query for a different charm. This will cause the
 
288
        // waiting goroutines to be woken up but go back to sleep again
 
289
        // because their entry isn't yet available.
 
290
        otherRequestDone := make(chan struct{})
 
291
        go func() {
 
292
                defer close(otherRequestDone)
 
293
                e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-2"), nil)
 
294
                c.Check(err, gc.IsNil)
 
295
                c.Check(e, jc.DeepEquals, selectEntityFields(entity2, entityFields()))
 
296
        }()
 
297
        query2 := <-store.entityqc
 
298
        c.Assert(query2.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-2"))
 
299
        c.Assert(query2.fields, jc.DeepEquals, entityFields())
 
300
        query2.reply <- entityReply{
 
301
                entity: entity2,
 
302
        }
 
303
 
 
304
        // Now reply to the initial store request, which should make
 
305
        // everything complete.
 
306
        query1.reply <- entityReply{
 
307
                entity: entity,
 
308
        }
 
309
        initialRequestGroup.Wait()
 
310
}
 
311
 
 
312
func (*suite) TestGetEntityNotFound(c *gc.C) {
 
313
        entityFetchCount := 0
 
314
        baseEntityFetchCount := 0
 
315
        store := &callbackStore{
 
316
                findBestEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
 
317
                        entityFetchCount++
 
318
                        return nil, errgo.NoteMask(params.ErrNotFound, "entity", errgo.Any)
 
319
                },
 
320
                findBaseEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
321
                        baseEntityFetchCount++
 
322
                        return nil, errgo.NoteMask(params.ErrNotFound, "base entity", errgo.Any)
 
323
                },
 
324
        }
 
325
        cache := entitycache.New(store)
 
326
        defer cache.Close()
 
327
        e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
328
        c.Assert(e, gc.IsNil)
 
329
        c.Assert(err, gc.ErrorMatches, "entity: not found")
 
330
        c.Assert(errgo.Cause(err), gc.Equals, params.ErrNotFound)
 
331
 
 
332
        // Make sure that the not-found result has been cached.
 
333
        e, err = cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
334
        c.Assert(e, gc.IsNil)
 
335
        c.Assert(err, gc.ErrorMatches, "entity: not found")
 
336
        c.Assert(errgo.Cause(err), gc.Equals, params.ErrNotFound)
 
337
 
 
338
        c.Assert(entityFetchCount, gc.Equals, 1)
 
339
 
 
340
        // Make sure fetching the base entity works the same way.
 
341
        be, err := cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), nil)
 
342
        c.Assert(be, gc.IsNil)
 
343
        c.Assert(err, gc.ErrorMatches, "base entity: not found")
 
344
        c.Assert(errgo.Cause(err), gc.Equals, params.ErrNotFound)
 
345
 
 
346
        be, err = cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), nil)
 
347
        c.Assert(be, gc.IsNil)
 
348
        c.Assert(err, gc.ErrorMatches, "base entity: not found")
 
349
        c.Assert(errgo.Cause(err), gc.Equals, params.ErrNotFound)
 
350
 
 
351
        c.Assert(baseEntityFetchCount, gc.Equals, 1)
 
352
}
 
353
 
 
354
func (*suite) TestFetchError(c *gc.C) {
 
355
        entityFetchCount := 0
 
356
        baseEntityFetchCount := 0
 
357
        store := &callbackStore{
 
358
                findBestEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
 
359
                        entityFetchCount++
 
360
                        return nil, errgo.New("entity error")
 
361
                },
 
362
                findBaseEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
363
                        baseEntityFetchCount++
 
364
                        return nil, errgo.New("base entity error")
 
365
                },
 
366
        }
 
367
        cache := entitycache.New(store)
 
368
        defer cache.Close()
 
369
 
 
370
        // Check that we get the entity fetch error from cache.Entity.
 
371
        e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
372
        c.Assert(e, gc.IsNil)
 
373
        c.Assert(err, gc.ErrorMatches, `cannot fetch "cs:~bob/wordpress-1": entity error`)
 
374
 
 
375
        // Check that the error is cached.
 
376
        e, err = cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
377
        c.Assert(e, gc.IsNil)
 
378
        c.Assert(err, gc.ErrorMatches, `cannot fetch "cs:~bob/wordpress-1": entity error`)
 
379
 
 
380
        c.Assert(entityFetchCount, gc.Equals, 1)
 
381
 
 
382
        // Check that we get the base-entity fetch error from cache.BaseEntity.
 
383
        be, err := cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), nil)
 
384
        c.Assert(be, gc.IsNil)
 
385
        c.Assert(err, gc.ErrorMatches, `cannot fetch "cs:~bob/wordpress": base entity error`)
 
386
 
 
387
        // Check that the error is cached.
 
388
        be, err = cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), nil)
 
389
        c.Assert(be, gc.IsNil)
 
390
        c.Assert(err, gc.ErrorMatches, `cannot fetch "cs:~bob/wordpress": base entity error`)
 
391
        c.Assert(baseEntityFetchCount, gc.Equals, 1)
 
392
}
 
393
 
 
394
func (*suite) TestStartFetch(c *gc.C) {
 
395
        store := newChanStore()
 
396
        cache := entitycache.New(store)
 
397
 
 
398
        url := charm.MustParseURL("~bob/wordpress-1")
 
399
        baseURL := charm.MustParseURL("~bob/wordpress")
 
400
        cache.StartFetch([]*charm.URL{url})
 
401
 
 
402
        entity := &mongodoc.Entity{
 
403
                URL:      url,
 
404
                BaseURL:  baseURL,
 
405
                BlobName: "foo",
 
406
        }
 
407
        baseEntity := &mongodoc.BaseEntity{
 
408
                URL: baseURL,
 
409
        }
 
410
 
 
411
        // Both queries should be issued concurrently.
 
412
        query1 := <-store.entityqc
 
413
        c.Assert(query1.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-1"))
 
414
        query2 := <-store.baseEntityqc
 
415
        c.Assert(query2.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress"))
 
416
 
 
417
        entityQueryDone := make(chan struct{})
 
418
        go func() {
 
419
                defer close(entityQueryDone)
 
420
                e, err := cache.Entity(url, nil)
 
421
                c.Check(err, gc.IsNil)
 
422
                c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields()))
 
423
        }()
 
424
        baseEntityQueryDone := make(chan struct{})
 
425
        go func() {
 
426
                defer close(baseEntityQueryDone)
 
427
                e, err := cache.BaseEntity(baseURL, nil)
 
428
                c.Check(err, gc.IsNil)
 
429
                c.Check(e, jc.DeepEquals, baseEntity)
 
430
        }()
 
431
 
 
432
        // Reply to the entity query.
 
433
        // This should cause the extra entity query to complete.
 
434
        query1.reply <- entityReply{
 
435
                entity: entity,
 
436
        }
 
437
        <-entityQueryDone
 
438
 
 
439
        // Reply to the base entity query.
 
440
        // This should cause the extra base entity query to complete.
 
441
        query2.reply <- baseEntityReply{
 
442
                entity: &mongodoc.BaseEntity{
 
443
                        URL: baseURL,
 
444
                },
 
445
        }
 
446
        <-baseEntityQueryDone
 
447
}
 
448
 
 
449
func (*suite) TestAddEntityFields(c *gc.C) {
 
450
        store := newChanStore()
 
451
        baseEntity := &mongodoc.BaseEntity{
 
452
                URL: charm.MustParseURL("cs:~bob/wordpress"),
 
453
        }
 
454
        entity := &mongodoc.Entity{
 
455
                URL:      charm.MustParseURL("cs:~bob/wordpress-1"),
 
456
                BlobName: "foo",
 
457
                Size:     999,
 
458
                BlobHash: "ffff",
 
459
        }
 
460
        baseEntityCount := 0
 
461
        store.findBaseEntity = func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
462
                baseEntityCount++
 
463
                if url.String() != "cs:~bob/wordpress" {
 
464
                        return nil, params.ErrNotFound
 
465
                }
 
466
                return baseEntity, nil
 
467
        }
 
468
        cache := entitycache.New(store)
 
469
        cache.AddEntityFields(map[string]int{"blobname": 1, "size": 1})
 
470
        queryDone := make(chan struct{})
 
471
        go func() {
 
472
                defer close(queryDone)
 
473
                e, err := cache.Entity(charm.MustParseURL("cs:~bob/wordpress-1"), map[string]int{"blobname": 1})
 
474
                c.Check(err, gc.IsNil)
 
475
                c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields("blobname", "size")))
 
476
 
 
477
                // Adding existing entity fields should have no effect.
 
478
                cache.AddEntityFields(map[string]int{"blobname": 1, "size": 1})
 
479
 
 
480
                e, err = cache.Entity(charm.MustParseURL("cs:~bob/wordpress-1"), map[string]int{"size": 1})
 
481
                c.Check(err, gc.IsNil)
 
482
                c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields("blobname", "size")))
 
483
 
 
484
                // Adding a new field should will cause the cache to be invalidated
 
485
                // and a new fetch to take place.
 
486
 
 
487
                cache.AddEntityFields(map[string]int{"blobhash": 1})
 
488
                e, err = cache.Entity(charm.MustParseURL("cs:~bob/wordpress-1"), nil)
 
489
                c.Check(err, gc.IsNil)
 
490
                c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields("blobname", "size", "blobhash")))
 
491
        }()
 
492
 
 
493
        query1 := <-store.entityqc
 
494
        c.Assert(query1.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-1"))
 
495
        c.Assert(query1.fields, jc.DeepEquals, entityFields("blobname", "size"))
 
496
        query1.reply <- entityReply{
 
497
                entity: entity,
 
498
        }
 
499
 
 
500
        // When the entity fields are added, we expect another query
 
501
        // because that invalidates the cache.
 
502
        query2 := <-store.entityqc
 
503
        c.Assert(query2.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-1"))
 
504
        c.Assert(query2.fields, jc.DeepEquals, entityFields("blobhash", "blobname", "size"))
 
505
        query2.reply <- entityReply{
 
506
                entity: entity,
 
507
        }
 
508
        <-queryDone
 
509
}
 
510
 
 
511
func (*suite) TestLookupByDifferentKey(c *gc.C) {
 
512
        entityFetchCount := 0
 
513
        entity := &mongodoc.Entity{
 
514
                URL:      charm.MustParseURL("~bob/wordpress-1"),
 
515
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
516
                BlobName: "w1",
 
517
        }
 
518
        baseEntity := &mongodoc.BaseEntity{
 
519
                URL:  charm.MustParseURL("~bob/wordpress"),
 
520
                Name: "wordpress",
 
521
        }
 
522
        store := &callbackStore{
 
523
                findBestEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
 
524
                        entityFetchCount++
 
525
                        return entity, nil
 
526
                },
 
527
                findBaseEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
528
                        if url.String() != "cs:~bob/wordpress" {
 
529
                                return nil, params.ErrNotFound
 
530
                        }
 
531
                        return baseEntity, nil
 
532
                },
 
533
        }
 
534
        cache := entitycache.New(store)
 
535
        defer cache.Close()
 
536
        e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
537
        c.Assert(err, gc.IsNil)
 
538
        c.Check(e, jc.DeepEquals, selectEntityFields(entity, entityFields()))
 
539
 
 
540
        oldEntity := e
 
541
 
 
542
        // The second fetch will trigger another query because
 
543
        // we can't tell whether it's the same entity or not,
 
544
        // but it should return the cached entity anyway.
 
545
        entity = &mongodoc.Entity{
 
546
                URL:      charm.MustParseURL("~bob/wordpress-1"),
 
547
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
548
                BlobName: "w2",
 
549
        }
 
550
        e, err = cache.Entity(charm.MustParseURL("~bob/wordpress"), nil)
 
551
        c.Assert(err, gc.IsNil)
 
552
        c.Logf("got %p; old entity %p; new entity %p", e, oldEntity, entity)
 
553
        c.Assert(e, gc.Equals, oldEntity)
 
554
        c.Assert(entityFetchCount, gc.Equals, 2)
 
555
}
 
556
 
 
557
func (s *suite) TestIterSingle(c *gc.C) {
 
558
        store := newChanStore()
 
559
        store.findBestEntity = func(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
 
560
                c.Errorf("store query made unexpectedly")
 
561
                return nil, errgo.New("no queries expected during iteration")
 
562
        }
 
563
        cache := entitycache.New(store)
 
564
        defer cache.Close()
 
565
        fakeIter := newFakeIter()
 
566
        iter := cache.CustomIter(fakeIter, map[string]int{"size": 1, "blobsize": 1})
 
567
        nextDone := make(chan struct{})
 
568
        go func() {
 
569
                defer close(nextDone)
 
570
                ok := iter.Next()
 
571
                c.Assert(ok, gc.Equals, true)
 
572
        }()
 
573
        replyc := <-fakeIter.req
 
574
        entity := &mongodoc.Entity{
 
575
                URL:      charm.MustParseURL("~bob/wordpress-1"),
 
576
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
577
                BlobName: "w1",
 
578
        }
 
579
        replyc <- iterReply{
 
580
                entity: entity,
 
581
        }
 
582
 
 
583
        // The iterator should batch up entities so make sure that
 
584
        // it does not return the entry immediately.
 
585
        select {
 
586
        case <-nextDone:
 
587
                c.Fatalf("Next returned early - no batching?")
 
588
        case <-time.After(10 * time.Millisecond):
 
589
        }
 
590
 
 
591
        // Get the next iterator query and reply to signal that
 
592
        // the iterator has completed.
 
593
        replyc = <-fakeIter.req
 
594
        replyc <- iterReply{
 
595
                err: errIterFinished,
 
596
        }
 
597
 
 
598
        // The base entity should be requested asynchronously now.
 
599
        baseQuery := <-store.baseEntityqc
 
600
 
 
601
        // ... but the initial reply shouldn't be held up by that.
 
602
        <-nextDone
 
603
 
 
604
        // Check that the entity is the one we expect.
 
605
        cachedEntity := iter.Entity()
 
606
        c.Assert(cachedEntity, jc.DeepEquals, selectEntityFields(entity, entityFields("size", "blobsize")))
 
607
 
 
608
        // Check that the entity can now be fetched from the cache.
 
609
        e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
610
        c.Assert(err, gc.IsNil)
 
611
        c.Assert(e, gc.Equals, cachedEntity)
 
612
 
 
613
        // A request for the base entity should now block
 
614
        // until the initial base entity request has been satisfied.
 
615
        baseEntity := &mongodoc.BaseEntity{
 
616
                URL:  charm.MustParseURL("~bob/wordpress"),
 
617
                Name: "wordpress",
 
618
        }
 
619
        queryDone := make(chan struct{})
 
620
        go func() {
 
621
                defer close(queryDone)
 
622
                e, err := cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), nil)
 
623
                c.Check(err, gc.IsNil)
 
624
                c.Check(e, jc.DeepEquals, selectBaseEntityFields(baseEntity, baseEntityFields()))
 
625
        }()
 
626
 
 
627
        // Check that no additional base entity query is made.
 
628
        select {
 
629
        case <-queryDone:
 
630
                c.Fatalf("Next returned early - no batching?")
 
631
        case <-time.After(10 * time.Millisecond):
 
632
        }
 
633
 
 
634
        // Reply to the base entity query ...
 
635
        baseQuery.reply <- baseEntityReply{
 
636
                entity: baseEntity,
 
637
        }
 
638
        // ... which should result in the one we just made
 
639
        // being satisfied too.
 
640
        <-queryDone
 
641
}
 
642
 
 
643
func (*suite) TestIterWithEntryAlreadyInCache(c *gc.C) {
 
644
        store := &staticStore{
 
645
                entities: []*mongodoc.Entity{{
 
646
                        URL:      charm.MustParseURL("~bob/wordpress-1"),
 
647
                        BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
648
                        BlobName: "w1",
 
649
                }, {
 
650
                        URL:      charm.MustParseURL("~bob/wordpress-2"),
 
651
                        BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
652
                        BlobName: "w2",
 
653
                }, {
 
654
                        URL:      charm.MustParseURL("~alice/mysql-1"),
 
655
                        BaseURL:  charm.MustParseURL("~alice/mysql"),
 
656
                        BlobName: "a1",
 
657
                }},
 
658
                baseEntities: []*mongodoc.BaseEntity{{
 
659
                        URL: charm.MustParseURL("~bob/wordpress"),
 
660
                }, {
 
661
                        URL: charm.MustParseURL("~alice/mysql"),
 
662
                }},
 
663
        }
 
664
        cache := entitycache.New(store)
 
665
        defer cache.Close()
 
666
        e, err := cache.Entity(charm.MustParseURL("~bob/wordpress-1"), map[string]int{"size": 1, "blobsize": 1})
 
667
        c.Assert(err, gc.IsNil)
 
668
        c.Check(e, jc.DeepEquals, selectEntityFields(store.entities[0], entityFields("size", "blobsize")))
 
669
        cachedEntity := e
 
670
 
 
671
        be, err := cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), nil)
 
672
        c.Assert(err, gc.IsNil)
 
673
        c.Check(be, jc.DeepEquals, selectBaseEntityFields(store.baseEntities[0], baseEntityFields()))
 
674
        cachedBaseEntity := be
 
675
 
 
676
        iterEntity := &mongodoc.Entity{
 
677
                URL:      charm.MustParseURL("~bob/wordpress-1"),
 
678
                BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
679
                BlobName: "w2",
 
680
        }
 
681
 
 
682
        fakeIter := newFakeIter()
 
683
        iter := cache.CustomIter(fakeIter, map[string]int{"size": 1, "blobsize": 1})
 
684
        iterDone := make(chan struct{})
 
685
        go func() {
 
686
                defer close(iterDone)
 
687
                ok := iter.Next()
 
688
                c.Check(ok, gc.Equals, true)
 
689
                // Even though the entity is in the cache, we still
 
690
                // receive the entity returned from the iterator.
 
691
                // We can't actually tell this though.
 
692
                c.Check(iter.Entity(), jc.DeepEquals, selectEntityFields(iterEntity, entityFields("size", "blobsize")))
 
693
 
 
694
                ok = iter.Next()
 
695
                c.Check(ok, gc.Equals, false)
 
696
        }()
 
697
 
 
698
        // Provide the iterator request with an entity that's already
 
699
        // in the cache.
 
700
        replyc := <-fakeIter.req
 
701
        replyc <- iterReply{
 
702
                entity: iterEntity,
 
703
        }
 
704
 
 
705
        replyc = <-fakeIter.req
 
706
        replyc <- iterReply{
 
707
                err: errIterFinished,
 
708
        }
 
709
        <-iterDone
 
710
 
 
711
        // The original cached entities should still be there.
 
712
        e, err = cache.Entity(charm.MustParseURL("~bob/wordpress-1"), nil)
 
713
        c.Assert(err, gc.IsNil)
 
714
        c.Assert(e, gc.Equals, cachedEntity)
 
715
 
 
716
        be, err = cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), nil)
 
717
        c.Assert(err, gc.IsNil)
 
718
        c.Assert(be, gc.Equals, cachedBaseEntity)
 
719
}
 
720
 
 
721
func (*suite) TestIterCloseEarlyWhenBatchLimitExceeded(c *gc.C) {
 
722
        // The iterator gets closed when the batch limit has been
 
723
        // exceeded.
 
724
 
 
725
        entities := make([]*mongodoc.Entity, entitycache.BaseEntityThreshold)
 
726
        baseEntities := make([]*mongodoc.BaseEntity, entitycache.BaseEntityThreshold)
 
727
        for i := range entities {
 
728
                entities[i] = &mongodoc.Entity{
 
729
                        URL: &charm.URL{
 
730
                                Schema:   "cs",
 
731
                                Name:     fmt.Sprintf("wordpress%d", i),
 
732
                                User:     "bob",
 
733
                                Revision: i,
 
734
                        },
 
735
                        BaseURL: &charm.URL{
 
736
                                Name: fmt.Sprintf("wordpress%d", i),
 
737
                                User: "bob",
 
738
                        },
 
739
                        BlobName: fmt.Sprintf("w%d", i),
 
740
                }
 
741
                baseEntities[i] = &mongodoc.BaseEntity{
 
742
                        URL: entities[i].BaseURL,
 
743
                }
 
744
        }
 
745
        store := &staticStore{
 
746
                baseEntities: baseEntities,
 
747
        }
 
748
        cache := entitycache.New(store)
 
749
        fakeIter := &sliceIter{
 
750
                entities: entities,
 
751
        }
 
752
        iter := cache.CustomIter(fakeIter, map[string]int{"blobname": 1})
 
753
        iter.Close()
 
754
        c.Assert(iter.Next(), gc.Equals, false)
 
755
}
 
756
 
 
757
func (*suite) TestIterEntityBatchLimitExceeded(c *gc.C) {
 
758
        entities := make([]*mongodoc.Entity, entitycache.EntityThreshold)
 
759
        for i := range entities {
 
760
                entities[i] = &mongodoc.Entity{
 
761
                        URL: &charm.URL{
 
762
                                Schema:   "cs",
 
763
                                Name:     "wordpress",
 
764
                                User:     "bob",
 
765
                                Revision: i,
 
766
                        },
 
767
                        BaseURL:  charm.MustParseURL("~bob/wordpress"),
 
768
                        BlobName: fmt.Sprintf("w%d", i),
 
769
                }
 
770
        }
 
771
        entities = append(entities, &mongodoc.Entity{
 
772
                URL:     charm.MustParseURL("~alice/mysql1-1"),
 
773
                BaseURL: charm.MustParseURL("~alice/mysql1"),
 
774
        })
 
775
        store := newChanStore()
 
776
        cache := entitycache.New(store)
 
777
        fakeIter := &sliceIter{
 
778
                entities: entities,
 
779
        }
 
780
        iter := cache.CustomIter(fakeIter, map[string]int{"blobname": 1})
 
781
 
 
782
        // The iterator should fetch up to entityThreshold entities
 
783
        // from the underlying iterator before sending
 
784
        // the batched base-entity request, then it
 
785
        // will make all those entries available.
 
786
        query := <-store.baseEntityqc
 
787
        c.Assert(query.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress"))
 
788
        query.reply <- baseEntityReply{
 
789
                entity: &mongodoc.BaseEntity{
 
790
                        URL: charm.MustParseURL("~bob/wordpress"),
 
791
                },
 
792
        }
 
793
        for i := 0; i < entitycache.EntityThreshold; i++ {
 
794
                ok := iter.Next()
 
795
                c.Assert(ok, gc.Equals, true)
 
796
                c.Assert(iter.Entity(), jc.DeepEquals, entities[i])
 
797
        }
 
798
        // When the iterator reaches its end, the
 
799
        // remaining entity and base entity are fetched.
 
800
        query = <-store.baseEntityqc
 
801
        c.Assert(query.url, jc.DeepEquals, charm.MustParseURL("~alice/mysql1"))
 
802
        query.reply <- baseEntityReply{
 
803
                entity: &mongodoc.BaseEntity{
 
804
                        URL: charm.MustParseURL("~alice/mysql1"),
 
805
                },
 
806
        }
 
807
 
 
808
        ok := iter.Next()
 
809
        c.Assert(ok, gc.Equals, true)
 
810
        c.Assert(iter.Entity(), jc.DeepEquals, entities[entitycache.EntityThreshold])
 
811
 
 
812
        // Check that all the entities and base entities are in fact cached.
 
813
        for _, want := range entities {
 
814
                got, err := cache.Entity(want.URL, nil)
 
815
                c.Assert(err, gc.IsNil)
 
816
                c.Assert(got, jc.DeepEquals, want)
 
817
                gotBase, err := cache.BaseEntity(want.URL, nil)
 
818
                c.Assert(err, gc.IsNil)
 
819
                c.Assert(gotBase, jc.DeepEquals, &mongodoc.BaseEntity{
 
820
                        URL: want.BaseURL,
 
821
                })
 
822
        }
 
823
}
 
824
 
 
825
func (*suite) TestIterError(c *gc.C) {
 
826
        cache := entitycache.New(&staticStore{})
 
827
        fakeIter := newFakeIter()
 
828
        iter := cache.CustomIter(fakeIter, nil)
 
829
        // Err returns nil while the iteration is in progress.
 
830
        err := iter.Err()
 
831
        c.Assert(err, gc.IsNil)
 
832
 
 
833
        replyc := <-fakeIter.req
 
834
        replyc <- iterReply{
 
835
                err: errgo.New("iterator error"),
 
836
        }
 
837
 
 
838
        ok := iter.Next()
 
839
        c.Assert(ok, gc.Equals, false)
 
840
        err = iter.Err()
 
841
        c.Assert(err, gc.ErrorMatches, "iterator error")
 
842
}
 
843
 
 
844
// iterReply holds a reply from a request from a fakeIter
 
845
// for the next item.
 
846
type iterReply struct {
 
847
        // entity holds the entity to be replied with.
 
848
        // Any fields not specified when creating the
 
849
        // iterator will be omitted from the result
 
850
        // sent to the entitycache code.
 
851
        entity *mongodoc.Entity
 
852
 
 
853
        // err holds any iteration error. When the iteration is complete,
 
854
        // errIterFinished should be sent.
 
855
        err error
 
856
}
 
857
 
 
858
// fakeIter provides a mock iterator implementation
 
859
// that sends each request for an entity to
 
860
// another goroutine for a result.
 
861
type fakeIter struct {
 
862
        closed bool
 
863
        fields map[string]int
 
864
        err    error
 
865
 
 
866
        // req holds a channel that is sent a value
 
867
        // whenever the Next method is called.
 
868
        req chan chan iterReply
 
869
}
 
870
 
 
871
func newFakeIter() *fakeIter {
 
872
        return &fakeIter{
 
873
                req: make(chan chan iterReply, 1),
 
874
        }
 
875
}
 
876
 
 
877
func (i *fakeIter) Iter(fields map[string]int) entitycache.StoreIter {
 
878
        i.fields = fields
 
879
        return i
 
880
}
 
881
 
 
882
// Next implements mgoIter.Next. The
 
883
// x parameter must be a *mongodoc.Entity.
 
884
func (i *fakeIter) Next(x interface{}) bool {
 
885
        if i.closed {
 
886
                panic("Next called after Close")
 
887
        }
 
888
        if i.err != nil {
 
889
                return false
 
890
        }
 
891
        replyc := make(chan iterReply)
 
892
        i.req <- replyc
 
893
        reply := <-replyc
 
894
        i.err = reply.err
 
895
        if i.err == nil {
 
896
                *(x.(*mongodoc.Entity)) = *selectEntityFields(reply.entity, i.fields)
 
897
        } else if reply.entity != nil {
 
898
                panic("entity with non-nil error")
 
899
        }
 
900
 
 
901
        return i.err == nil
 
902
}
 
903
 
 
904
var errIterFinished = errgo.New("iteration finished")
 
905
 
 
906
// Close implements mgoIter.Close.
 
907
func (i *fakeIter) Close() error {
 
908
        i.closed = true
 
909
        if i.err == errIterFinished {
 
910
                return nil
 
911
        }
 
912
        return i.err
 
913
}
 
914
 
 
915
// Close implements mgoIter.Err.
 
916
func (i *fakeIter) Err() error {
 
917
        if i.err == errIterFinished {
 
918
                return nil
 
919
        }
 
920
        return i.err
 
921
}
 
922
 
 
923
// sliceIter implements mgoIter over a slice of entities,
 
924
// returning each one in turn.
 
925
type sliceIter struct {
 
926
        fields   map[string]int
 
927
        entities []*mongodoc.Entity
 
928
        closed   bool
 
929
}
 
930
 
 
931
func (i *sliceIter) Iter(fields map[string]int) entitycache.StoreIter {
 
932
        i.fields = fields
 
933
        return i
 
934
}
 
935
 
 
936
func (iter *sliceIter) Next(x interface{}) bool {
 
937
        if iter.closed {
 
938
                panic("Next called after Close")
 
939
        }
 
940
        if len(iter.entities) == 0 {
 
941
                return false
 
942
        }
 
943
        e := x.(*mongodoc.Entity)
 
944
        *e = *selectEntityFields(iter.entities[0], iter.fields)
 
945
        iter.entities = iter.entities[1:]
 
946
        return true
 
947
}
 
948
 
 
949
func (iter *sliceIter) Err() error {
 
950
        return nil
 
951
}
 
952
 
 
953
func (iter *sliceIter) Close() error {
 
954
        iter.closed = true
 
955
        return nil
 
956
}
 
957
 
 
958
type chanStore struct {
 
959
        entityqc     chan entityQuery
 
960
        baseEntityqc chan baseEntityQuery
 
961
        *callbackStore
 
962
}
 
963
 
 
964
func newChanStore() *chanStore {
 
965
        entityqc := make(chan entityQuery, 1)
 
966
        baseEntityqc := make(chan baseEntityQuery, 1)
 
967
        return &chanStore{
 
968
                entityqc:     entityqc,
 
969
                baseEntityqc: baseEntityqc,
 
970
                callbackStore: &callbackStore{
 
971
                        findBestEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
 
972
                                reply := make(chan entityReply)
 
973
                                entityqc <- entityQuery{
 
974
                                        url:    url,
 
975
                                        fields: fields,
 
976
                                        reply:  reply,
 
977
                                }
 
978
                                r := <-reply
 
979
                                return r.entity, r.err
 
980
                        },
 
981
                        findBaseEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
982
                                reply := make(chan baseEntityReply)
 
983
                                baseEntityqc <- baseEntityQuery{
 
984
                                        url:    url,
 
985
                                        fields: fields,
 
986
                                        reply:  reply,
 
987
                                }
 
988
                                r := <-reply
 
989
                                return r.entity, r.err
 
990
                        },
 
991
                },
 
992
        }
 
993
}
 
994
 
 
995
type callbackStore struct {
 
996
        findBestEntity func(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error)
 
997
        findBaseEntity func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error)
 
998
}
 
999
 
 
1000
func (s *callbackStore) FindBestEntity(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
 
1001
        e, err := s.findBestEntity(url, fields)
 
1002
        if err != nil {
 
1003
                return nil, err
 
1004
        }
 
1005
        return selectEntityFields(e, fields), nil
 
1006
}
 
1007
 
 
1008
func (s *callbackStore) FindBaseEntity(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
1009
        e, err := s.findBaseEntity(url, fields)
 
1010
        if err != nil {
 
1011
                return nil, err
 
1012
        }
 
1013
        return selectBaseEntityFields(e, fields), nil
 
1014
}
 
1015
 
 
1016
type staticStore struct {
 
1017
        entities     []*mongodoc.Entity
 
1018
        baseEntities []*mongodoc.BaseEntity
 
1019
}
 
1020
 
 
1021
func (s *staticStore) FindBestEntity(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
 
1022
        for _, e := range s.entities {
 
1023
                if *url == *e.URL {
 
1024
                        return selectEntityFields(e, fields), nil
 
1025
                }
 
1026
        }
 
1027
        return nil, params.ErrNotFound
 
1028
}
 
1029
 
 
1030
func (s *staticStore) FindBaseEntity(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
 
1031
        for _, e := range s.baseEntities {
 
1032
                if *url == *e.URL {
 
1033
                        return e, nil
 
1034
                }
 
1035
        }
 
1036
        return nil, params.ErrNotFound
 
1037
}
 
1038
 
 
1039
func selectEntityFields(x *mongodoc.Entity, fields map[string]int) *mongodoc.Entity {
 
1040
        e := selectFields(x, fields).(*mongodoc.Entity)
 
1041
        if e.URL == nil {
 
1042
                panic("url empty after selectfields")
 
1043
        }
 
1044
        return e
 
1045
}
 
1046
 
 
1047
func selectBaseEntityFields(x *mongodoc.BaseEntity, fields map[string]int) *mongodoc.BaseEntity {
 
1048
        return selectFields(x, fields).(*mongodoc.BaseEntity)
 
1049
}
 
1050
 
 
1051
// selectFields returns a copy of x (which must
 
1052
// be a pointer to struct) with all fields zeroed
 
1053
// except those mentioned in fields.
 
1054
func selectFields(x interface{}, fields map[string]int) interface{} {
 
1055
        xv := reflect.ValueOf(x).Elem()
 
1056
        xt := xv.Type()
 
1057
        dv := reflect.New(xt).Elem()
 
1058
        dv.Set(xv)
 
1059
        for i := 0; i < xt.NumField(); i++ {
 
1060
                f := xt.Field(i)
 
1061
                if _, ok := fields[bsonFieldName(f)]; ok {
 
1062
                        continue
 
1063
                }
 
1064
                dv.Field(i).Set(reflect.Zero(f.Type))
 
1065
        }
 
1066
        return dv.Addr().Interface()
 
1067
}
 
1068
 
 
1069
func bsonFieldName(f reflect.StructField) string {
 
1070
        t := f.Tag.Get("bson")
 
1071
        if t == "" {
 
1072
                return strings.ToLower(f.Name)
 
1073
        }
 
1074
        if i := strings.Index(t, ","); i >= 0 {
 
1075
                t = t[0:i]
 
1076
        }
 
1077
        if t != "" {
 
1078
                return t
 
1079
        }
 
1080
        return strings.ToLower(f.Name)
 
1081
}
 
1082
 
 
1083
func entityFields(fields ...string) map[string]int {
 
1084
        return addFields(entitycache.RequiredEntityFields, fields...)
 
1085
}
 
1086
 
 
1087
func baseEntityFields(fields ...string) map[string]int {
 
1088
        return addFields(entitycache.RequiredBaseEntityFields, fields...)
 
1089
}
 
1090
 
 
1091
func addFields(fields map[string]int, extra ...string) map[string]int {
 
1092
        fields1 := make(map[string]int)
 
1093
        for f := range fields {
 
1094
                fields1[f] = 1
 
1095
        }
 
1096
        for _, f := range extra {
 
1097
                fields1[f] = 1
 
1098
        }
 
1099
        return fields1
 
1100
}