1
package entitycache_test
10
jc "github.com/juju/testing/checkers"
11
gc "gopkg.in/check.v1"
13
"gopkg.in/juju/charm.v6-unstable"
14
"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
16
"gopkg.in/juju/charmstore.v5-unstable/internal/entitycache"
17
"gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
20
var _ = gc.Suite(&suite{})
24
type entityQuery struct {
27
reply chan entityReply
30
type entityReply struct {
31
entity *mongodoc.Entity
35
type baseEntityQuery struct {
38
reply chan baseEntityReply
41
type baseEntityReply struct {
42
entity *mongodoc.BaseEntity
46
func (*suite) TestEntityIssuesBaseEntityQueryConcurrently(c *gc.C) {
47
store := newChanStore()
48
cache := entitycache.New(store)
50
cache.AddBaseEntityFields(map[string]int{"name": 1})
52
entity := &mongodoc.Entity{
53
URL: charm.MustParseURL("~bob/wordpress-1"),
54
BaseURL: charm.MustParseURL("~bob/wordpress"),
58
baseEntity := &mongodoc.BaseEntity{
59
URL: charm.MustParseURL("~bob/wordpress"),
62
queryDone := make(chan struct{})
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")))
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{
81
query2.reply <- baseEntityReply{
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
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")))
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")))
99
func (*suite) TestEntityIssuesBaseEntityQuerySequentiallyForPromulgatedURL(c *gc.C) {
100
store := newChanStore()
101
cache := entitycache.New(store)
103
cache.AddBaseEntityFields(map[string]int{"name": 1})
105
entity := &mongodoc.Entity{
106
URL: charm.MustParseURL("~bob/wordpress-1"),
107
PromulgatedURL: charm.MustParseURL("wordpress-5"),
108
BaseURL: charm.MustParseURL("~bob/wordpress"),
112
baseEntity := &mongodoc.BaseEntity{
113
URL: charm.MustParseURL("~bob/wordpress"),
116
queryDone := make(chan struct{})
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")))
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{
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
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{
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
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")))
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")))
158
func (*suite) TestFetchWhenFieldsChangeBeforeQueryResult(c *gc.C) {
159
store := newChanStore()
160
cache := entitycache.New(store)
162
cache.AddBaseEntityFields(map[string]int{"name": 1})
164
entity := &mongodoc.Entity{
165
URL: charm.MustParseURL("~bob/wordpress-1"),
166
BaseURL: charm.MustParseURL("~bob/wordpress"),
169
baseEntity := &mongodoc.BaseEntity{
170
URL: charm.MustParseURL("~bob/wordpress"),
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
179
queryDone := make(chan struct{})
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()))
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"),
198
query2Done := make(chan struct{})
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")))
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{
216
// Reply to the first query and make sure that it completed.
217
query1.reply <- entityReply{
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")))
231
func (*suite) TestSecondFetchesWaitForFirst(c *gc.C) {
232
store := newChanStore()
233
cache := entitycache.New(store)
235
cache.AddBaseEntityFields(map[string]int{"name": 1})
237
entity := &mongodoc.Entity{
238
URL: charm.MustParseURL("~bob/wordpress-1"),
239
BaseURL: charm.MustParseURL("~bob/wordpress"),
242
baseEntity := &mongodoc.BaseEntity{
243
URL: charm.MustParseURL("~bob/wordpress"),
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
252
var initialRequestGroup sync.WaitGroup
253
initialRequestGroup.Add(1)
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()))
261
query1 := <-store.entityqc
262
c.Assert(query1.url, jc.DeepEquals, charm.MustParseURL("~bob/wordpress-1"))
263
c.Assert(query1.fields, jc.DeepEquals, entityFields())
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)
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()))
277
case q := <-store.entityqc:
278
c.Fatalf("unexpected store query %#v", q)
279
case <-time.After(10 * time.Millisecond):
282
entity2 := &mongodoc.Entity{
283
URL: charm.MustParseURL("~bob/wordpress-2"),
284
BaseURL: charm.MustParseURL("~bob/wordpress"),
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{})
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()))
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{
304
// Now reply to the initial store request, which should make
305
// everything complete.
306
query1.reply <- entityReply{
309
initialRequestGroup.Wait()
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) {
318
return nil, errgo.NoteMask(params.ErrNotFound, "entity", errgo.Any)
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)
325
cache := entitycache.New(store)
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)
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)
338
c.Assert(entityFetchCount, gc.Equals, 1)
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)
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)
351
c.Assert(baseEntityFetchCount, gc.Equals, 1)
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) {
360
return nil, errgo.New("entity error")
362
findBaseEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
363
baseEntityFetchCount++
364
return nil, errgo.New("base entity error")
367
cache := entitycache.New(store)
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`)
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`)
380
c.Assert(entityFetchCount, gc.Equals, 1)
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`)
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)
394
func (*suite) TestStartFetch(c *gc.C) {
395
store := newChanStore()
396
cache := entitycache.New(store)
398
url := charm.MustParseURL("~bob/wordpress-1")
399
baseURL := charm.MustParseURL("~bob/wordpress")
400
cache.StartFetch([]*charm.URL{url})
402
entity := &mongodoc.Entity{
407
baseEntity := &mongodoc.BaseEntity{
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"))
417
entityQueryDone := make(chan struct{})
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()))
424
baseEntityQueryDone := make(chan struct{})
426
defer close(baseEntityQueryDone)
427
e, err := cache.BaseEntity(baseURL, nil)
428
c.Check(err, gc.IsNil)
429
c.Check(e, jc.DeepEquals, baseEntity)
432
// Reply to the entity query.
433
// This should cause the extra entity query to complete.
434
query1.reply <- entityReply{
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{
446
<-baseEntityQueryDone
449
func (*suite) TestAddEntityFields(c *gc.C) {
450
store := newChanStore()
451
baseEntity := &mongodoc.BaseEntity{
452
URL: charm.MustParseURL("cs:~bob/wordpress"),
454
entity := &mongodoc.Entity{
455
URL: charm.MustParseURL("cs:~bob/wordpress-1"),
461
store.findBaseEntity = func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
463
if url.String() != "cs:~bob/wordpress" {
464
return nil, params.ErrNotFound
466
return baseEntity, nil
468
cache := entitycache.New(store)
469
cache.AddEntityFields(map[string]int{"blobname": 1, "size": 1})
470
queryDone := make(chan struct{})
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")))
477
// Adding existing entity fields should have no effect.
478
cache.AddEntityFields(map[string]int{"blobname": 1, "size": 1})
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")))
484
// Adding a new field should will cause the cache to be invalidated
485
// and a new fetch to take place.
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")))
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{
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{
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"),
518
baseEntity := &mongodoc.BaseEntity{
519
URL: charm.MustParseURL("~bob/wordpress"),
522
store := &callbackStore{
523
findBestEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
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
531
return baseEntity, nil
534
cache := entitycache.New(store)
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()))
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"),
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)
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")
563
cache := entitycache.New(store)
565
fakeIter := newFakeIter()
566
iter := cache.CustomIter(fakeIter, map[string]int{"size": 1, "blobsize": 1})
567
nextDone := make(chan struct{})
569
defer close(nextDone)
571
c.Assert(ok, gc.Equals, true)
573
replyc := <-fakeIter.req
574
entity := &mongodoc.Entity{
575
URL: charm.MustParseURL("~bob/wordpress-1"),
576
BaseURL: charm.MustParseURL("~bob/wordpress"),
583
// The iterator should batch up entities so make sure that
584
// it does not return the entry immediately.
587
c.Fatalf("Next returned early - no batching?")
588
case <-time.After(10 * time.Millisecond):
591
// Get the next iterator query and reply to signal that
592
// the iterator has completed.
593
replyc = <-fakeIter.req
595
err: errIterFinished,
598
// The base entity should be requested asynchronously now.
599
baseQuery := <-store.baseEntityqc
601
// ... but the initial reply shouldn't be held up by that.
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")))
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)
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"),
619
queryDone := make(chan struct{})
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()))
627
// Check that no additional base entity query is made.
630
c.Fatalf("Next returned early - no batching?")
631
case <-time.After(10 * time.Millisecond):
634
// Reply to the base entity query ...
635
baseQuery.reply <- baseEntityReply{
638
// ... which should result in the one we just made
639
// being satisfied too.
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"),
650
URL: charm.MustParseURL("~bob/wordpress-2"),
651
BaseURL: charm.MustParseURL("~bob/wordpress"),
654
URL: charm.MustParseURL("~alice/mysql-1"),
655
BaseURL: charm.MustParseURL("~alice/mysql"),
658
baseEntities: []*mongodoc.BaseEntity{{
659
URL: charm.MustParseURL("~bob/wordpress"),
661
URL: charm.MustParseURL("~alice/mysql"),
664
cache := entitycache.New(store)
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")))
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
676
iterEntity := &mongodoc.Entity{
677
URL: charm.MustParseURL("~bob/wordpress-1"),
678
BaseURL: charm.MustParseURL("~bob/wordpress"),
682
fakeIter := newFakeIter()
683
iter := cache.CustomIter(fakeIter, map[string]int{"size": 1, "blobsize": 1})
684
iterDone := make(chan struct{})
686
defer close(iterDone)
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")))
695
c.Check(ok, gc.Equals, false)
698
// Provide the iterator request with an entity that's already
700
replyc := <-fakeIter.req
705
replyc = <-fakeIter.req
707
err: errIterFinished,
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)
716
be, err = cache.BaseEntity(charm.MustParseURL("~bob/wordpress"), nil)
717
c.Assert(err, gc.IsNil)
718
c.Assert(be, gc.Equals, cachedBaseEntity)
721
func (*suite) TestIterCloseEarlyWhenBatchLimitExceeded(c *gc.C) {
722
// The iterator gets closed when the batch limit has been
725
entities := make([]*mongodoc.Entity, entitycache.BaseEntityThreshold)
726
baseEntities := make([]*mongodoc.BaseEntity, entitycache.BaseEntityThreshold)
727
for i := range entities {
728
entities[i] = &mongodoc.Entity{
731
Name: fmt.Sprintf("wordpress%d", i),
736
Name: fmt.Sprintf("wordpress%d", i),
739
BlobName: fmt.Sprintf("w%d", i),
741
baseEntities[i] = &mongodoc.BaseEntity{
742
URL: entities[i].BaseURL,
745
store := &staticStore{
746
baseEntities: baseEntities,
748
cache := entitycache.New(store)
749
fakeIter := &sliceIter{
752
iter := cache.CustomIter(fakeIter, map[string]int{"blobname": 1})
754
c.Assert(iter.Next(), gc.Equals, false)
757
func (*suite) TestIterEntityBatchLimitExceeded(c *gc.C) {
758
entities := make([]*mongodoc.Entity, entitycache.EntityThreshold)
759
for i := range entities {
760
entities[i] = &mongodoc.Entity{
767
BaseURL: charm.MustParseURL("~bob/wordpress"),
768
BlobName: fmt.Sprintf("w%d", i),
771
entities = append(entities, &mongodoc.Entity{
772
URL: charm.MustParseURL("~alice/mysql1-1"),
773
BaseURL: charm.MustParseURL("~alice/mysql1"),
775
store := newChanStore()
776
cache := entitycache.New(store)
777
fakeIter := &sliceIter{
780
iter := cache.CustomIter(fakeIter, map[string]int{"blobname": 1})
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"),
793
for i := 0; i < entitycache.EntityThreshold; i++ {
795
c.Assert(ok, gc.Equals, true)
796
c.Assert(iter.Entity(), jc.DeepEquals, entities[i])
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"),
809
c.Assert(ok, gc.Equals, true)
810
c.Assert(iter.Entity(), jc.DeepEquals, entities[entitycache.EntityThreshold])
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{
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.
831
c.Assert(err, gc.IsNil)
833
replyc := <-fakeIter.req
835
err: errgo.New("iterator error"),
839
c.Assert(ok, gc.Equals, false)
841
c.Assert(err, gc.ErrorMatches, "iterator error")
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
853
// err holds any iteration error. When the iteration is complete,
854
// errIterFinished should be sent.
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 {
863
fields map[string]int
866
// req holds a channel that is sent a value
867
// whenever the Next method is called.
868
req chan chan iterReply
871
func newFakeIter() *fakeIter {
873
req: make(chan chan iterReply, 1),
877
func (i *fakeIter) Iter(fields map[string]int) entitycache.StoreIter {
882
// Next implements mgoIter.Next. The
883
// x parameter must be a *mongodoc.Entity.
884
func (i *fakeIter) Next(x interface{}) bool {
886
panic("Next called after Close")
891
replyc := make(chan iterReply)
896
*(x.(*mongodoc.Entity)) = *selectEntityFields(reply.entity, i.fields)
897
} else if reply.entity != nil {
898
panic("entity with non-nil error")
904
var errIterFinished = errgo.New("iteration finished")
906
// Close implements mgoIter.Close.
907
func (i *fakeIter) Close() error {
909
if i.err == errIterFinished {
915
// Close implements mgoIter.Err.
916
func (i *fakeIter) Err() error {
917
if i.err == errIterFinished {
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
931
func (i *sliceIter) Iter(fields map[string]int) entitycache.StoreIter {
936
func (iter *sliceIter) Next(x interface{}) bool {
938
panic("Next called after Close")
940
if len(iter.entities) == 0 {
943
e := x.(*mongodoc.Entity)
944
*e = *selectEntityFields(iter.entities[0], iter.fields)
945
iter.entities = iter.entities[1:]
949
func (iter *sliceIter) Err() error {
953
func (iter *sliceIter) Close() error {
958
type chanStore struct {
959
entityqc chan entityQuery
960
baseEntityqc chan baseEntityQuery
964
func newChanStore() *chanStore {
965
entityqc := make(chan entityQuery, 1)
966
baseEntityqc := make(chan baseEntityQuery, 1)
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{
979
return r.entity, r.err
981
findBaseEntity: func(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
982
reply := make(chan baseEntityReply)
983
baseEntityqc <- baseEntityQuery{
989
return r.entity, r.err
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)
1000
func (s *callbackStore) FindBestEntity(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
1001
e, err := s.findBestEntity(url, fields)
1005
return selectEntityFields(e, fields), nil
1008
func (s *callbackStore) FindBaseEntity(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
1009
e, err := s.findBaseEntity(url, fields)
1013
return selectBaseEntityFields(e, fields), nil
1016
type staticStore struct {
1017
entities []*mongodoc.Entity
1018
baseEntities []*mongodoc.BaseEntity
1021
func (s *staticStore) FindBestEntity(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
1022
for _, e := range s.entities {
1024
return selectEntityFields(e, fields), nil
1027
return nil, params.ErrNotFound
1030
func (s *staticStore) FindBaseEntity(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
1031
for _, e := range s.baseEntities {
1036
return nil, params.ErrNotFound
1039
func selectEntityFields(x *mongodoc.Entity, fields map[string]int) *mongodoc.Entity {
1040
e := selectFields(x, fields).(*mongodoc.Entity)
1042
panic("url empty after selectfields")
1047
func selectBaseEntityFields(x *mongodoc.BaseEntity, fields map[string]int) *mongodoc.BaseEntity {
1048
return selectFields(x, fields).(*mongodoc.BaseEntity)
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()
1057
dv := reflect.New(xt).Elem()
1059
for i := 0; i < xt.NumField(); i++ {
1061
if _, ok := fields[bsonFieldName(f)]; ok {
1064
dv.Field(i).Set(reflect.Zero(f.Type))
1066
return dv.Addr().Interface()
1069
func bsonFieldName(f reflect.StructField) string {
1070
t := f.Tag.Get("bson")
1072
return strings.ToLower(f.Name)
1074
if i := strings.Index(t, ","); i >= 0 {
1080
return strings.ToLower(f.Name)
1083
func entityFields(fields ...string) map[string]int {
1084
return addFields(entitycache.RequiredEntityFields, fields...)
1087
func baseEntityFields(fields ...string) map[string]int {
1088
return addFields(entitycache.RequiredBaseEntityFields, fields...)
1091
func addFields(fields map[string]int, extra ...string) map[string]int {
1092
fields1 := make(map[string]int)
1093
for f := range fields {
1096
for _, f := range extra {