Merge lp:~allenap/juju-core/break-out-juju-store into lp:~juju/juju-core/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: William Reade
Approved revision: no longer in the source branch.
Merged at revision: 886
Proposed branch: lp:~allenap/juju-core/break-out-juju-store
Merge into: lp:~juju/juju-core/trunk
Diff against target: 2667 lines (+0/-2600)
13 files modified
cmd/charmd/config.yaml (+0/-2)
cmd/charmd/main.go (+0/-74)
cmd/charmload/config.yaml (+0/-1)
cmd/charmload/main.go (+0/-75)
store/branch.go (+0/-152)
store/branch_test.go (+0/-238)
store/lpad.go (+0/-113)
store/lpad_test.go (+0/-68)
store/mgo_test.go (+0/-95)
store/server.go (+0/-191)
store/server_test.go (+0/-209)
store/store.go (+0/-774)
store/store_test.go (+0/-608)
To merge this branch: bzr merge lp:~allenap/juju-core/break-out-juju-store
Reviewer Review Type Date Requested Status
The Go Language Gophers Pending
Review via email: mp+142564@code.launchpad.net

Description of the change

Break out store and cmd/charm* into lp:juju-store

Red Squad are going to be working on the charm store, and it was
suggested that an early task would be to split the charm store out
into a separate project. That work has already been done - see
lp:juju-store - and this is the clean-up job.

mgz helped me a lot in doing both these tasks.

Fwiw, juju-store has not been advertised, so feel free to suggest a
different name.

https://codereview.appspot.com/7058063/

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Reviewers: mp+142564_code.launchpad.net,

Message:
Please take a look.

Description:
Break out store and cmd/charm* into lp:juju-store

Red Squad are going to be working on the charm store, and it was
suggested that an early task would be to split the charm store out
into a separate project. That work has already been done - see
lp:juju-store - and this is the clean-up job.

mgz helped me a lot in doing both these tasks.

Fwiw, juju-store has not been advertised, so feel free to suggest a
different name.

https://code.launchpad.net/~allenap/juju-core/break-out-juju-store/+merge/142564

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/7058063/

Affected files:
   A [revision details]
   D cmd/charmd/config.yaml
   D cmd/charmd/main.go
   D cmd/charmload/config.yaml
   D cmd/charmload/main.go
   D store/branch.go
   D store/branch_test.go
   D store/lpad.go
   D store/lpad_test.go
   D store/mgo_test.go
   D store/server.go
   D store/server_test.go
   D store/store.go
   D store/store_test.go

Revision history for this message
William Reade (fwereade) wrote :

On 2013/01/09 17:59:58, allenap wrote:
> Please take a look.

LGTM; I'm +1 on juju-store myself.

https://codereview.appspot.com/7058063/

Revision history for this message
William Reade (fwereade) wrote :

Note that https://code.launchpad.net/~dave-cheney/juju-core/068-CONTRIBUTING/+merge/143057 merges a bug fix; please ensure it makes it across to lp:juju-store.

Revision history for this message
Gavin Panella (allenap) wrote :

Reopening. This consensus from Austin is that the store should be broken out into a separate project.

Revision history for this message
William Reade (fwereade) wrote :

As long as the latest version of store is taken, and any bugs are moved, I'm +1 on just going ahead and doing this.

Revision history for this message
Gavin Panella (allenap) wrote :

William, would you be able to land this please? I don't have the necessary fu. However, before you do, do you know if this will break any service deployment scripts for store.juju.ubuntu.com (if I remember the hostname correctly)?

Revision history for this message
Gavin Panella (allenap) wrote :

I forgot to mention: lp:juju-store has the latest store code from lp:juju-core, and I'm happy to find and move all the bugs over when this lands.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed directory 'cmd/charmd'
2=== removed file 'cmd/charmd/config.yaml'
3--- cmd/charmd/config.yaml 2012-04-02 15:05:40 +0000
4+++ cmd/charmd/config.yaml 1970-01-01 00:00:00 +0000
5@@ -1,2 +0,0 @@
6-mongo-url: localhost:60017
7-api-addr: localhost:8080
8
9=== removed file 'cmd/charmd/main.go'
10--- cmd/charmd/main.go 2012-06-21 20:40:39 +0000
11+++ cmd/charmd/main.go 1970-01-01 00:00:00 +0000
12@@ -1,74 +0,0 @@
13-package main
14-
15-import (
16- "fmt"
17- "io/ioutil"
18- "launchpad.net/goyaml"
19- "launchpad.net/juju-core/log"
20- "launchpad.net/juju-core/store"
21- stdlog "log"
22- "net/http"
23- "os"
24- "path/filepath"
25-)
26-
27-func main() {
28- log.Target = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
29- err := serve()
30- if err != nil {
31- fmt.Fprintf(os.Stderr, "%v\n", err)
32- os.Exit(1)
33- }
34-}
35-
36-type config struct {
37- MongoURL string `yaml:"mongo-url"`
38- APIAddr string `yaml:"api-addr"`
39-}
40-
41-func readConfig(path string, conf interface{}) error {
42- f, err := os.Open(path)
43- if err != nil {
44- return fmt.Errorf("opening config file: %v", err)
45- }
46- data, err := ioutil.ReadAll(f)
47- f.Close()
48- if err != nil {
49- return fmt.Errorf("reading config file: %v", err)
50- }
51- err = goyaml.Unmarshal(data, conf)
52- if err != nil {
53- return fmt.Errorf("processing config file: %v", err)
54- }
55- return nil
56-}
57-
58-func serve() error {
59- var confPath string
60- if len(os.Args) == 2 {
61- if _, err := os.Stat(os.Args[1]); err == nil {
62- confPath = os.Args[1]
63- }
64- }
65- if confPath == "" {
66- return fmt.Errorf("usage: %s <config path>", filepath.Base(os.Args[0]))
67- }
68- var conf config
69- err := readConfig(confPath, &conf)
70- if err != nil {
71- return err
72- }
73- if conf.MongoURL == "" || conf.APIAddr == "" {
74- return fmt.Errorf("missing mongo-url or api-addr in config file")
75- }
76- s, err := store.Open(conf.MongoURL)
77- if err != nil {
78- return err
79- }
80- defer s.Close()
81- server, err := store.NewServer(s)
82- if err != nil {
83- return err
84- }
85- return http.ListenAndServe(conf.APIAddr, server)
86-}
87
88=== removed directory 'cmd/charmload'
89=== removed file 'cmd/charmload/config.yaml'
90--- cmd/charmload/config.yaml 2012-04-02 15:05:40 +0000
91+++ cmd/charmload/config.yaml 1970-01-01 00:00:00 +0000
92@@ -1,1 +0,0 @@
93-mongo-url: localhost:60017
94
95=== removed file 'cmd/charmload/main.go'
96--- cmd/charmload/main.go 2012-06-21 20:40:39 +0000
97+++ cmd/charmload/main.go 1970-01-01 00:00:00 +0000
98@@ -1,75 +0,0 @@
99-package main
100-
101-import (
102- "fmt"
103- "io/ioutil"
104- "launchpad.net/goyaml"
105- "launchpad.net/juju-core/log"
106- "launchpad.net/juju-core/store"
107- "launchpad.net/lpad"
108- stdlog "log"
109- "os"
110- "path/filepath"
111-)
112-
113-func main() {
114- log.Target = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
115- err := load()
116- if err != nil {
117- fmt.Fprintf(os.Stderr, "%v\n", err)
118- os.Exit(1)
119- }
120-}
121-
122-type config struct {
123- MongoURL string `yaml:"mongo-url"`
124-}
125-
126-func readConfig(path string, conf interface{}) error {
127- f, err := os.Open(path)
128- if err != nil {
129- return fmt.Errorf("opening config file: %v", err)
130- }
131- data, err := ioutil.ReadAll(f)
132- f.Close()
133- if err != nil {
134- return fmt.Errorf("reading config file: %v", err)
135- }
136- err = goyaml.Unmarshal(data, conf)
137- if err != nil {
138- return fmt.Errorf("processing config file: %v", err)
139- }
140- return nil
141-}
142-
143-func load() error {
144- var confPath string
145- if len(os.Args) == 2 {
146- if _, err := os.Stat(os.Args[1]); err == nil {
147- confPath = os.Args[1]
148- }
149- }
150- if confPath == "" {
151- return fmt.Errorf("usage: %s <config path>", filepath.Base(os.Args[0]))
152- }
153- var conf config
154- err := readConfig(confPath, &conf)
155- if err != nil {
156- return err
157- }
158- if conf.MongoURL == "" {
159- return fmt.Errorf("missing mongo-url in config file")
160- }
161- s, err := store.Open(conf.MongoURL)
162- if err != nil {
163- return err
164- }
165- defer s.Close()
166- err = store.PublishCharmsDistro(s, lpad.Production)
167- if _, ok := err.(store.PublishBranchErrors); ok {
168- // Ignore branch errors since they're commonplace here.
169- // They're logged, though.
170- return nil
171- }
172- return err
173-}
174
175=== removed directory 'store'
176=== removed file 'store/branch.go'
177--- store/branch.go 2012-07-24 09:36:51 +0000
178+++ store/branch.go 1970-01-01 00:00:00 +0000
179@@ -1,152 +0,0 @@
180-package store
181-
182-import (
183- "bytes"
184- "fmt"
185- "io/ioutil"
186- "launchpad.net/juju-core/charm"
187- "os"
188- "os/exec"
189- "path/filepath"
190- "strings"
191-)
192-
193-// PublishBazaarBranch checks out the Bazaar branch from burl and
194-// publishes its latest revision at urls in the given store.
195-// The digest parameter must be the most recent known Bazaar
196-// revision id for the branch tip. If publishing this specific digest
197-// for these URLs has been attempted already, the publishing
198-// procedure may abort early. The published digest is the Bazaar
199-// revision id of the checked out branch's tip, though, which may
200-// differ from the digest parameter.
201-func PublishBazaarBranch(store *Store, urls []*charm.URL, burl string, digest string) error {
202-
203- // Prevent other publishers from updating these specific URLs
204- // concurrently.
205- lock, err := store.LockUpdates(urls)
206- if err != nil {
207- return err
208- }
209- defer lock.Unlock()
210-
211- var branchDir string
212-NewTip:
213- // Prepare the charm publisher. This will compute the revision
214- // to be assigned to the charm, and it will also fail if the
215- // operation is unnecessary because charms are up-to-date.
216- pub, err := store.CharmPublisher(urls, digest)
217- if err != nil {
218- return err
219- }
220-
221- // Figure if publishing this charm was already attempted before and
222- // failed. We won't try again endlessly if so. In the future we may
223- // retry automatically in certain circumstances.
224- event, err := store.CharmEvent(urls[0], digest)
225- if err == nil && event.Kind != EventPublished {
226- return fmt.Errorf("charm publishing previously failed: %s", strings.Join(event.Errors, "; "))
227- } else if err != nil && err != ErrNotFound {
228- return err
229- }
230-
231- if branchDir == "" {
232- // Retrieve the branch with a lightweight checkout, so that it
233- // builds a working tree as cheaply as possible. History
234- // doesn't matter here.
235- tempDir, err := ioutil.TempDir("", "publish-branch-")
236- if err != nil {
237- return err
238- }
239- defer os.RemoveAll(tempDir)
240- branchDir = filepath.Join(tempDir, "branch")
241- output, err := exec.Command("bzr", "checkout", "--lightweight", burl, branchDir).CombinedOutput()
242- if err != nil {
243- return outputErr(output, err)
244- }
245-
246- // Pick actual digest from tip. Publishing the real tip
247- // revision rather than the revision for the digest provided is
248- // strictly necessary to prevent a race condition. If the
249- // provided digest was published instead, there's a chance
250- // another publisher concurrently running could have found a
251- // newer revision and published that first, and the digest
252- // parameter provided is in fact an old version that would
253- // overwrite the new version.
254- tipDigest, err := bzrRevisionId(branchDir)
255- if err != nil {
256- return err
257- }
258- if tipDigest != digest {
259- digest = tipDigest
260- goto NewTip
261- }
262- }
263-
264- ch, err := charm.ReadDir(branchDir)
265- if err == nil {
266- // Hand over the charm to the store for bundling and
267- // streaming its content into the database.
268- err = pub.Publish(ch)
269- if err == ErrUpdateConflict {
270- // A conflict may happen in edge cases if the whole
271- // locking mechanism fails due to an expiration event,
272- // and then the expired concurrent publisher revives
273- // for whatever reason and attempts to finish
274- // publishing. The state of the system is still
275- // consistent in that case, and the error isn't logged
276- // since the revision was properly published before.
277- return err
278- }
279- }
280-
281- // Publishing is done. Log failure or error.
282- event = &CharmEvent{
283- URLs: urls,
284- Digest: digest,
285- }
286- if err == nil {
287- event.Kind = EventPublished
288- event.Revision = pub.Revision()
289- } else {
290- event.Kind = EventPublishError
291- event.Errors = []string{err.Error()}
292- }
293- if logerr := store.LogCharmEvent(event); logerr != nil {
294- if err == nil {
295- err = logerr
296- } else {
297- err = fmt.Errorf("%v; %v", err, logerr)
298- }
299- }
300- return err
301-}
302-
303-// bzrRevisionId returns the Bazaar revision id for the branch in branchDir.
304-func bzrRevisionId(branchDir string) (string, error) {
305- cmd := exec.Command("bzr", "revision-info")
306- cmd.Dir = branchDir
307- stderr := &bytes.Buffer{}
308- cmd.Stderr = stderr
309- output, err := cmd.Output()
310- if err != nil {
311- output = append(output, '\n')
312- output = append(output, stderr.Bytes()...)
313- return "", outputErr(output, err)
314- }
315- pair := bytes.Fields(output)
316- if len(pair) != 2 {
317- output = append(output, '\n')
318- output = append(output, stderr.Bytes()...)
319- return "", fmt.Errorf(`invalid output from "bzr revision-info": %s`, output)
320- }
321- return string(pair[1]), nil
322-}
323-
324-// outputErr returns an error that assembles some command's output and its
325-// error, if both output and err are set, and returns only err if output is nil.
326-func outputErr(output []byte, err error) error {
327- if len(output) > 0 {
328- return fmt.Errorf("%v\n%s", err, output)
329- }
330- return err
331-}
332
333=== removed file 'store/branch_test.go'
334--- store/branch_test.go 2012-10-09 01:19:01 +0000
335+++ store/branch_test.go 1970-01-01 00:00:00 +0000
336@@ -1,238 +0,0 @@
337-package store_test
338-
339-import (
340- "bytes"
341- "fmt"
342- "io/ioutil"
343- . "launchpad.net/gocheck"
344- "launchpad.net/juju-core/charm"
345- "launchpad.net/juju-core/store"
346- "launchpad.net/juju-core/testing"
347- "os"
348- "os/exec"
349- "path/filepath"
350- "strings"
351- "time"
352-)
353-
354-func (s *StoreSuite) dummyBranch(c *C, suffix string) bzrDir {
355- tmpDir := c.MkDir()
356- if suffix != "" {
357- tmpDir = filepath.Join(tmpDir, suffix)
358- err := os.MkdirAll(tmpDir, 0755)
359- c.Assert(err, IsNil)
360- }
361- branch := bzrDir(tmpDir)
362- branch.init()
363-
364- copyCharmDir(branch.path(), testing.Charms.Dir("series", "dummy"))
365- branch.add()
366- branch.commit("Imported charm.")
367- return branch
368-}
369-
370-var urls = []*charm.URL{
371- charm.MustParseURL("cs:~joe/oneiric/dummy"),
372- charm.MustParseURL("cs:oneiric/dummy"),
373-}
374-
375-type fakePlugin struct {
376- oldEnv string
377-}
378-
379-func (p *fakePlugin) install(dir string, content string) {
380- p.oldEnv = os.Getenv("BZR_PLUGINS_AT")
381- err := ioutil.WriteFile(filepath.Join(dir, "__init__.py"), []byte(content), 0644)
382- if err != nil {
383- panic(err)
384- }
385- os.Setenv("BZR_PLUGINS_AT", "fakePlugin@"+dir)
386-}
387-
388-func (p *fakePlugin) uninstall() {
389- os.Setenv("BZR_PLUGINS_AT", p.oldEnv)
390-}
391-
392-func (s *StoreSuite) TestPublish(c *C) {
393- branch := s.dummyBranch(c, "")
394-
395- // Ensure that the streams are parsed separately by inserting
396- // garbage on stderr. The wanted information is still there.
397- plugin := fakePlugin{}
398- plugin.install(c.MkDir(), `import sys; sys.stderr.write("STDERR STUFF FROM TEST\n")`)
399- defer plugin.uninstall()
400-
401- err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
402- c.Assert(err, IsNil)
403-
404- for _, url := range urls {
405- info, rc, err := s.store.OpenCharm(url)
406- c.Assert(err, IsNil)
407- defer rc.Close()
408- c.Assert(info.Revision(), Equals, 0)
409- c.Assert(info.Meta().Name, Equals, "dummy")
410-
411- data, err := ioutil.ReadAll(rc)
412- c.Assert(err, IsNil)
413-
414- bundle, err := charm.ReadBundleBytes(data)
415- c.Assert(err, IsNil)
416- c.Assert(bundle.Revision(), Equals, 0)
417- c.Assert(bundle.Meta().Name, Equals, "dummy")
418- }
419-
420- // Attempt to publish the same content again while providing the wrong
421- // tip revision. It must pick the real revision from the branch and
422- // note this was previously published.
423- err = store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
424- c.Assert(err, Equals, store.ErrRedundantUpdate)
425-
426- // Bump the content revision and lie again about the known tip revision.
427- // This time, though, pretend it's the same as the real branch revision
428- // previously published. It must error and not publish the new revision
429- // because it will use the revision provided as a parameter to check if
430- // publishing was attempted before. This is the mechanism that enables
431- // stopping fast without having to download every single branch. Real
432- // revision is picked in the next scan.
433- digest1 := branch.digest()
434- branch.change()
435- err = store.PublishBazaarBranch(s.store, urls, branch.path(), digest1)
436- c.Assert(err, Equals, store.ErrRedundantUpdate)
437-
438- // Now allow it to publish the new content by providing an unseen revision.
439- err = store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
440- c.Assert(err, IsNil)
441- digest2 := branch.digest()
442-
443- info, err := s.store.CharmInfo(urls[0])
444- c.Assert(err, IsNil)
445- c.Assert(info.Revision(), Equals, 1)
446- c.Assert(info.Meta().Name, Equals, "dummy")
447-
448- // There are two events published, for each of the successful attempts.
449- // The failures are ignored given that they are artifacts of the
450- // publishing mechanism rather than actual problems.
451- _, err = s.store.CharmEvent(urls[0], "wrong-rev")
452- c.Assert(err, Equals, store.ErrNotFound)
453- for i, digest := range []string{digest1, digest2} {
454- event, err := s.store.CharmEvent(urls[0], digest)
455- c.Assert(err, IsNil)
456- c.Assert(event.Kind, Equals, store.EventPublished)
457- c.Assert(event.Revision, Equals, i)
458- c.Assert(event.Errors, IsNil)
459- c.Assert(event.Warnings, IsNil)
460- }
461-}
462-
463-func (s *StoreSuite) TestPublishErrorFromBzr(c *C) {
464- branch := s.dummyBranch(c, "")
465-
466- // In TestPublish we ensure that the streams are parsed
467- // separately by inserting garbage on stderr. Now make
468- // sure that stderr isn't simply trashed, as we want to
469- // know about what a real error tells us.
470- plugin := fakePlugin{}
471- plugin.install(c.MkDir(), `import sys; sys.stderr.write("STDERR STUFF FROM TEST\n"); sys.exit(1)`)
472- defer plugin.uninstall()
473-
474- err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
475- c.Assert(err, ErrorMatches, "(?s).*STDERR STUFF.*")
476-}
477-
478-func (s *StoreSuite) TestPublishErrorInCharm(c *C) {
479- branch := s.dummyBranch(c, "")
480-
481- // Corrupt the charm.
482- branch.remove("metadata.yaml")
483- branch.commit("Removed metadata.yaml.")
484-
485- // Attempt to publish the erroneous content.
486- err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
487- c.Assert(err, ErrorMatches, ".*/metadata.yaml: no such file or directory")
488-
489- // The event should be logged as well, since this was an error in the charm
490- // that won't go away and must be communicated to the author.
491- event, err := s.store.CharmEvent(urls[0], branch.digest())
492- c.Assert(err, IsNil)
493- c.Assert(event.Kind, Equals, store.EventPublishError)
494- c.Assert(event.Revision, Equals, 0)
495- c.Assert(event.Errors, NotNil)
496- c.Assert(event.Errors[0], Matches, ".*/metadata.yaml: no such file or directory")
497- c.Assert(event.Warnings, IsNil)
498-}
499-
500-type bzrDir string
501-
502-func (dir bzrDir) path(args ...string) string {
503- return filepath.Join(append([]string{string(dir)}, args...)...)
504-}
505-
506-func (dir bzrDir) run(args ...string) []byte {
507- cmd := exec.Command("bzr", args...)
508- oldemail := os.Getenv("EMAIL")
509- defer os.Setenv("EMAIL", oldemail)
510- // bzr will complain if bzr whoami has not been run previously,
511- // avoid this by passing $EMAIL into the environment.
512- os.Setenv("EMAIL", "nobody@example.com")
513- cmd.Dir = string(dir)
514- output, err := cmd.Output()
515- if err != nil {
516- panic(fmt.Sprintf("command failed: bzr %s\n%s", strings.Join(args, " "), output))
517- }
518- return output
519-}
520-
521-func (dir bzrDir) init() {
522- dir.run("init")
523-}
524-
525-func (dir bzrDir) add(paths ...string) {
526- dir.run(append([]string{"add"}, paths...)...)
527-}
528-
529-func (dir bzrDir) remove(paths ...string) {
530- dir.run(append([]string{"rm"}, paths...)...)
531-}
532-
533-func (dir bzrDir) commit(msg string) {
534- dir.run("commit", "-m", msg)
535-}
536-
537-func (dir bzrDir) write(path string, data string) {
538- err := ioutil.WriteFile(dir.path(path), []byte(data), 0644)
539- if err != nil {
540- panic(err)
541- }
542-}
543-
544-func (dir bzrDir) change() {
545- t := time.Now().String()
546- dir.write("timestamp", t)
547- dir.add("timestamp")
548- dir.commit("Revision bumped at " + t)
549-}
550-
551-func (dir bzrDir) digest() string {
552- output := dir.run("revision-info")
553- f := bytes.Fields(output)
554- if len(f) != 2 {
555- panic("revision-info returned bad output: " + string(output))
556- }
557- return string(f[1])
558-}
559-
560-func copyCharmDir(dst string, dir *charm.Dir) {
561- var b bytes.Buffer
562- err := dir.BundleTo(&b)
563- if err != nil {
564- panic(err)
565- }
566- bundle, err := charm.ReadBundleBytes(b.Bytes())
567- if err != nil {
568- panic(err)
569- }
570- err = bundle.ExpandTo(dst)
571- if err != nil {
572- panic(err)
573- }
574-}
575
576=== removed file 'store/lpad.go'
577--- store/lpad.go 2012-06-21 20:40:39 +0000
578+++ store/lpad.go 1970-01-01 00:00:00 +0000
579@@ -1,113 +0,0 @@
580-package store
581-
582-import (
583- "fmt"
584- "launchpad.net/juju-core/charm"
585- "launchpad.net/juju-core/log"
586- "launchpad.net/lpad"
587- "strings"
588- "time"
589-)
590-
591-type PublishBranchError struct {
592- URL string
593- Err error
594-}
595-
596-type PublishBranchErrors []PublishBranchError
597-
598-func (errs PublishBranchErrors) Error() string {
599- return fmt.Sprintf("%d branch(es) failed to be published", len(errs))
600-}
601-
602-// PublishCharmsDistro publishes all branch tips found in
603-// the /charms distribution in Launchpad onto store under
604-// the "cs:" scheme.
605-// apiBase specifies the Launchpad base API URL, such
606-// as lpad.Production or lpad.Staging.
607-// Errors found while processing one or more branches are
608-// all returned as a PublishBranchErrors value.
609-func PublishCharmsDistro(store *Store, apiBase lpad.APIBase) error {
610- oauth := &lpad.OAuth{Anonymous: true, Consumer: "juju"}
611- root, err := lpad.Login(apiBase, oauth)
612- if err != nil {
613- return err
614- }
615- distro, err := root.Distro("charms")
616- if err != nil {
617- return err
618- }
619- tips, err := distro.BranchTips(time.Time{})
620- if err != nil {
621- return err
622- }
623-
624- var errs PublishBranchErrors
625- for _, tip := range tips {
626- if !strings.HasSuffix(tip.UniqueName, "/trunk") {
627- continue
628- }
629- burl, curl, err := uniqueNameURLs(tip.UniqueName)
630- if err != nil {
631- errs = append(errs, PublishBranchError{tip.UniqueName, err})
632- log.Printf("error: %v\n", err)
633- continue
634- }
635- log.Printf("----- %s\n", burl)
636- if tip.Revision == "" {
637- errs = append(errs, PublishBranchError{burl, fmt.Errorf("branch has no revisions")})
638- log.Printf("error: branch has no revisions\n")
639- continue
640- }
641- // Charm is published in the personal URL and in any explicitly
642- // assigned official series.
643- urls := []*charm.URL{curl}
644- schema, name := curl.Schema, curl.Name
645- for _, series := range tip.OfficialSeries {
646- curl = &charm.URL{Schema: schema, Name: name, Series: series, Revision: -1}
647- curl.Series = series
648- curl.User = ""
649- urls = append(urls, curl)
650- }
651-
652- err = PublishBazaarBranch(store, urls, burl, tip.Revision)
653- if err == ErrRedundantUpdate {
654- continue
655- }
656- if err != nil {
657- errs = append(errs, PublishBranchError{burl, err})
658- log.Printf("error: %v\n", err)
659- }
660- }
661- if errs != nil {
662- return errs
663- }
664- return nil
665-}
666-
667-// uniqueNameURLs returns the branch URL and the charm URL for the
668-// provided Launchpad branch unique name. The unique name must be
669-// in the form:
670-//
671-// ~<user>/charms/<series>/<charm name>/trunk
672-//
673-// For testing purposes, if name has a prefix preceding a string in
674-// this format, the prefix is stripped out for computing the charm
675-// URL, and the unique name is returned unchanged as the branch URL.
676-func uniqueNameURLs(name string) (burl string, curl *charm.URL, err error) {
677- u := strings.Split(name, "/")
678- if len(u) > 5 {
679- u = u[len(u)-5:]
680- burl = name
681- } else {
682- burl = "lp:" + name
683- }
684- if len(u) < 5 || u[1] != "charms" || u[4] != "trunk" || len(u[0]) == 0 || u[0][0] != '~' {
685- return "", nil, fmt.Errorf("unwanted branch name: %s", name)
686- }
687- curl, err = charm.ParseURL(fmt.Sprintf("cs:%s/%s/%s", u[0], u[2], u[3]))
688- if err != nil {
689- return "", nil, err
690- }
691- return burl, curl, nil
692-}
693
694=== removed file 'store/lpad_test.go'
695--- store/lpad_test.go 2012-08-18 22:48:02 +0000
696+++ store/lpad_test.go 1970-01-01 00:00:00 +0000
697@@ -1,68 +0,0 @@
698-package store_test
699-
700-import (
701- "fmt"
702- . "launchpad.net/gocheck"
703- "launchpad.net/juju-core/charm"
704- "launchpad.net/juju-core/store"
705- "launchpad.net/juju-core/testing"
706- "launchpad.net/lpad"
707-)
708-
709-var jsonType = map[string]string{
710- "Content-Type": "application/json",
711-}
712-
713-func (s *StoreSuite) TestPublishCharmDistro(c *C) {
714- branch := s.dummyBranch(c, "~joe/charms/oneiric/dummy/trunk")
715-
716- // The Distro call will look for bare /charms, first.
717- testing.Server.Response(200, jsonType, []byte("{}"))
718-
719- // And then it picks up the tips.
720- data := fmt.Sprintf(`[`+
721- `["file://%s", "rev1", ["oneiric", "precise"]],`+
722- `["file://%s", "%s", []],`+
723- `["file:///non-existent/~jeff/charms/precise/bad/trunk", "rev2", []],`+
724- `["file:///non-existent/~jeff/charms/precise/bad/skip-me", "rev3", []]`+
725- `]`,
726- branch.path(), branch.path(), branch.digest())
727- testing.Server.Response(200, jsonType, []byte(data))
728-
729- apiBase := lpad.APIBase(testing.Server.URL)
730- err := store.PublishCharmsDistro(s.store, apiBase)
731-
732- // Should have a single failure from the trunk branch that doesn't
733- // exist. The redundant update with the known digest should be
734- // ignored, and skip-me isn't a supported branch name so it's
735- // ignored as well.
736- c.Assert(err, ErrorMatches, `1 branch\(es\) failed to be published`)
737- berr := err.(store.PublishBranchErrors)[0]
738- c.Assert(berr.URL, Equals, "file:///non-existent/~jeff/charms/precise/bad/trunk")
739- c.Assert(berr.Err, ErrorMatches, "(?s).*bzr: ERROR: Not a branch.*")
740-
741- for _, url := range []string{"cs:oneiric/dummy", "cs:precise/dummy-0", "cs:~joe/oneiric/dummy-0"} {
742- dummy, err := s.store.CharmInfo(charm.MustParseURL(url))
743- c.Assert(err, IsNil)
744- c.Assert(dummy.Meta().Name, Equals, "dummy")
745- }
746-
747- // The known digest should have been ignored, so revision is still at 0.
748- _, err = s.store.CharmInfo(charm.MustParseURL("cs:~joe/oneiric/dummy-1"))
749- c.Assert(err, Equals, store.ErrNotFound)
750-
751- // bare /charms lookup
752- req := testing.Server.WaitRequest()
753- c.Assert(req.Method, Equals, "GET")
754- c.Assert(req.URL.Path, Equals, "/charms")
755-
756- // tips request
757- req = testing.Server.WaitRequest()
758- c.Assert(req.Method, Equals, "GET")
759- c.Assert(req.URL.Path, Equals, "/charms")
760- c.Assert(req.Form["ws.op"], DeepEquals, []string{"getBranchTips"})
761- c.Assert(req.Form["since"], IsNil)
762-
763- // Request must be signed by juju.
764- c.Assert(req.Header.Get("Authorization"), Matches, `.*oauth_consumer_key="juju".*`)
765-}
766
767=== removed file 'store/mgo_test.go'
768--- store/mgo_test.go 2012-10-30 11:45:32 +0000
769+++ store/mgo_test.go 1970-01-01 00:00:00 +0000
770@@ -1,95 +0,0 @@
771-package store_test
772-
773-import (
774- "bytes"
775- "labix.org/v2/mgo"
776- . "launchpad.net/gocheck"
777- "os/exec"
778- "time"
779-)
780-
781-// ----------------------------------------------------------------------------
782-// The mgo test suite
783-
784-type MgoSuite struct {
785- Addr string
786- Session *mgo.Session
787- output bytes.Buffer
788- server *exec.Cmd
789-}
790-
791-func (s *MgoSuite) SetUpSuite(c *C) {
792- mgo.SetDebug(true)
793- mgo.SetStats(true)
794- dbdir := c.MkDir()
795- args := []string{
796- "--dbpath", dbdir,
797- "--bind_ip", "127.0.0.1",
798- "--port", "50017",
799- "--nssize", "1",
800- "--noprealloc",
801- "--smallfiles",
802- "--nojournal",
803- }
804- s.server = exec.Command("mongod", args...)
805- s.server.Stdout = &s.output
806- s.server.Stderr = &s.output
807- err := s.server.Start()
808- c.Assert(err, IsNil)
809-}
810-
811-func (s *MgoSuite) TearDownSuite(c *C) {
812- s.server.Process.Kill()
813- s.server.Process.Wait()
814-}
815-
816-func (s *MgoSuite) SetUpTest(c *C) {
817- err := DropAll("localhost:50017")
818- c.Assert(err, IsNil)
819- mgo.SetLogger(c)
820- mgo.ResetStats()
821- s.Addr = "127.0.0.1:50017"
822- s.Session, err = mgo.Dial(s.Addr)
823- c.Assert(err, IsNil)
824-}
825-
826-func (s *MgoSuite) TearDownTest(c *C) {
827- if s.Session != nil {
828- s.Session.Close()
829- }
830- for i := 0; ; i++ {
831- stats := mgo.GetStats()
832- if stats.SocketsInUse == 0 && stats.SocketsAlive == 0 {
833- break
834- }
835- if i == 20 {
836- c.Fatal("Test left sockets in a dirty state")
837- }
838- c.Logf("Waiting for sockets to die: %d in use, %d alive", stats.SocketsInUse, stats.SocketsAlive)
839- time.Sleep(500 * time.Millisecond)
840- }
841-}
842-
843-func DropAll(mongourl string) (err error) {
844- session, err := mgo.Dial(mongourl)
845- if err != nil {
846- return err
847- }
848- defer session.Close()
849-
850- names, err := session.DatabaseNames()
851- if err != nil {
852- return err
853- }
854- for _, name := range names {
855- switch name {
856- case "admin", "local", "config":
857- default:
858- err = session.DB(name).DropDatabase()
859- if err != nil {
860- return err
861- }
862- }
863- }
864- return nil
865-}
866
867=== removed file 'store/server.go'
868--- store/server.go 2012-10-11 14:52:21 +0000
869+++ store/server.go 1970-01-01 00:00:00 +0000
870@@ -1,191 +0,0 @@
871-package store
872-
873-import (
874- "encoding/json"
875- "io"
876- "launchpad.net/juju-core/charm"
877- "launchpad.net/juju-core/log"
878- "net/http"
879- "strconv"
880- "strings"
881-)
882-
883-// Server is an http.Handler that serves the HTTP API of juju
884-// so that juju clients can retrieve published charms.
885-type Server struct {
886- store *Store
887- mux *http.ServeMux
888-}
889-
890-// New returns a new *Server using store.
891-func NewServer(store *Store) (*Server, error) {
892- s := &Server{
893- store: store,
894- mux: http.NewServeMux(),
895- }
896- s.mux.HandleFunc("/charm-info", func(w http.ResponseWriter, r *http.Request) {
897- s.serveInfo(w, r)
898- })
899- s.mux.HandleFunc("/charm/", func(w http.ResponseWriter, r *http.Request) {
900- s.serveCharm(w, r)
901- })
902- s.mux.HandleFunc("/stats/counter/", func(w http.ResponseWriter, r *http.Request) {
903- s.serveStats(w, r)
904- })
905-
906- // This is just a validation key to allow blitz.io to run
907- // performance tests against the site.
908- s.mux.HandleFunc("/mu-35700a31-6bf320ca-a800b670-05f845ee", func(w http.ResponseWriter, r *http.Request) {
909- s.serveBlitzKey(w, r)
910- })
911- return s, nil
912-}
913-
914-// ServeHTTP serves an http request.
915-// This method turns *Server into an http.Handler.
916-func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
917- if r.URL.Path == "/" {
918- http.Redirect(w, r, "https://juju.ubuntu.com", http.StatusSeeOther)
919- return
920- }
921- s.mux.ServeHTTP(w, r)
922-}
923-
924-func statsEnabled(req *http.Request) bool {
925- // It's fine to parse the form more than once, and it avoids
926- // bugs from not parsing it.
927- req.ParseForm()
928- return req.Form.Get("stats") != "0"
929-}
930-
931-func charmStatsKey(curl *charm.URL, kind string) []string {
932- if curl.User == "" {
933- return []string{kind, curl.Series, curl.Name}
934- }
935- return []string{kind, curl.Series, curl.Name, curl.User}
936-}
937-
938-func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) {
939- if r.URL.Path != "/charm-info" {
940- w.WriteHeader(http.StatusNotFound)
941- return
942- }
943- r.ParseForm()
944- response := map[string]*charm.InfoResponse{}
945- for _, url := range r.Form["charms"] {
946- c := &charm.InfoResponse{}
947- response[url] = c
948- curl, err := charm.ParseURL(url)
949- var info *CharmInfo
950- if err == nil {
951- info, err = s.store.CharmInfo(curl)
952- }
953- var skey []string
954- if err == nil {
955- skey = charmStatsKey(curl, "charm-info")
956- c.Sha256 = info.BundleSha256()
957- c.Revision = info.Revision()
958- c.Digest = info.Digest()
959- } else {
960- if err == ErrNotFound {
961- skey = charmStatsKey(curl, "charm-missing")
962- }
963- c.Errors = append(c.Errors, err.Error())
964- }
965- if skey != nil && statsEnabled(r) {
966- go s.store.IncCounter(skey)
967- }
968- }
969- data, err := json.Marshal(response)
970- if err == nil {
971- w.Header().Set("Content-Type", "application/json")
972- _, err = w.Write(data)
973- }
974- if err != nil {
975- log.Printf("store: cannot write content: %v", err)
976- w.WriteHeader(http.StatusInternalServerError)
977- return
978- }
979-}
980-
981-func (s *Server) serveCharm(w http.ResponseWriter, r *http.Request) {
982- if !strings.HasPrefix(r.URL.Path, "/charm/") {
983- panic("serveCharm: bad url")
984- }
985- curl, err := charm.ParseURL("cs:" + r.URL.Path[len("/charm/"):])
986- if err != nil {
987- w.WriteHeader(http.StatusNotFound)
988- return
989- }
990- info, rc, err := s.store.OpenCharm(curl)
991- if err == ErrNotFound {
992- w.WriteHeader(http.StatusNotFound)
993- return
994- }
995- if err != nil {
996- w.WriteHeader(http.StatusInternalServerError)
997- log.Printf("store: cannot open charm %q: %v", curl, err)
998- return
999- }
1000- if statsEnabled(r) {
1001- go s.store.IncCounter(charmStatsKey(curl, "charm-bundle"))
1002- }
1003- defer rc.Close()
1004- w.Header().Set("Connection", "close") // No keep-alive for now.
1005- w.Header().Set("Content-Type", "application/octet-stream")
1006- w.Header().Set("Content-Length", strconv.FormatInt(info.BundleSize(), 10))
1007- _, err = io.Copy(w, rc)
1008- if err != nil {
1009- log.Printf("store: failed to stream charm %q: %v", curl, err)
1010- }
1011-}
1012-
1013-func (s *Server) serveStats(w http.ResponseWriter, r *http.Request) {
1014- // TODO: Adopt a smarter mux that simplifies this logic.
1015- const dir = "/stats/counter/"
1016- if !strings.HasPrefix(r.URL.Path, dir) {
1017- panic("bad url")
1018- }
1019- base := r.URL.Path[len(dir):]
1020- if strings.Index(base, "/") > 0 {
1021- w.WriteHeader(http.StatusNotFound)
1022- return
1023- }
1024- if base == "" {
1025- w.WriteHeader(http.StatusForbidden)
1026- return
1027- }
1028- key := strings.Split(base, ":")
1029- prefix := false
1030- if key[len(key)-1] == "*" {
1031- prefix = true
1032- key = key[:len(key)-1]
1033- if len(key) == 0 {
1034- // No point in counting something unknown.
1035- w.WriteHeader(http.StatusForbidden)
1036- return
1037- }
1038- }
1039- r.ParseForm()
1040- sum, err := s.store.SumCounter(key, prefix)
1041- if err != nil {
1042- log.Printf("store: cannot sum counter: %v", err)
1043- w.WriteHeader(http.StatusInternalServerError)
1044- return
1045- }
1046- data := []byte(strconv.FormatInt(sum, 10))
1047- w.Header().Set("Content-Type", "text/plain")
1048- w.Header().Set("Content-Length", strconv.Itoa(len(data)))
1049- _, err = w.Write(data)
1050- if err != nil {
1051- log.Printf("store: cannot write content: %v", err)
1052- w.WriteHeader(http.StatusInternalServerError)
1053- }
1054-}
1055-
1056-func (s *Server) serveBlitzKey(w http.ResponseWriter, r *http.Request) {
1057- w.Header().Set("Connection", "close")
1058- w.Header().Set("Content-Type", "text/plain")
1059- w.Header().Set("Content-Length", "2")
1060- w.Write([]byte("42"))
1061-}
1062
1063=== removed file 'store/server_test.go'
1064--- store/server_test.go 2012-09-05 21:08:26 +0000
1065+++ store/server_test.go 1970-01-01 00:00:00 +0000
1066@@ -1,209 +0,0 @@
1067-package store_test
1068-
1069-import (
1070- "encoding/json"
1071- "io/ioutil"
1072- . "launchpad.net/gocheck"
1073- "launchpad.net/juju-core/charm"
1074- "launchpad.net/juju-core/store"
1075- "net/http"
1076- "net/http/httptest"
1077- "net/url"
1078- "strconv"
1079- "time"
1080-)
1081-
1082-func (s *StoreSuite) prepareServer(c *C) (*store.Server, *charm.URL) {
1083- curl := charm.MustParseURL("cs:oneiric/wordpress")
1084- pub, err := s.store.CharmPublisher([]*charm.URL{curl}, "some-digest")
1085- c.Assert(err, IsNil)
1086- err = pub.Publish(&FakeCharmDir{})
1087- c.Assert(err, IsNil)
1088-
1089- server, err := store.NewServer(s.store)
1090- c.Assert(err, IsNil)
1091- return server, curl
1092-}
1093-
1094-func (s *StoreSuite) TestServerCharmInfo(c *C) {
1095- server, curl := s.prepareServer(c)
1096- req, err := http.NewRequest("GET", "/charm-info", nil)
1097- c.Assert(err, IsNil)
1098-
1099- var tests = []struct{ url, sha, digest, err string }{
1100- {curl.String(), fakeRevZeroSha, "some-digest", ""},
1101- {"cs:oneiric/non-existent", "", "", "entry not found"},
1102- {"cs:bad", "", "", `charm URL without series: "cs:bad"`},
1103- }
1104-
1105- for _, t := range tests {
1106- req.Form = url.Values{"charms": []string{t.url}}
1107- rec := httptest.NewRecorder()
1108- server.ServeHTTP(rec, req)
1109-
1110- expected := make(map[string]interface{})
1111- if t.sha != "" {
1112- expected[t.url] = map[string]interface{}{
1113- "revision": float64(0),
1114- "sha256": t.sha,
1115- "digest": t.digest,
1116- }
1117- } else {
1118- expected[t.url] = map[string]interface{}{
1119- "revision": float64(0),
1120- "errors": []interface{}{t.err},
1121- }
1122- }
1123- obtained := map[string]interface{}{}
1124- err = json.NewDecoder(rec.Body).Decode(&obtained)
1125- c.Assert(err, IsNil)
1126- c.Assert(obtained, DeepEquals, expected)
1127- c.Assert(rec.Header().Get("Content-Type"), Equals, "application/json")
1128- }
1129-
1130- s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 1)
1131- s.checkCounterSum(c, []string{"charm-missing", "oneiric", "non-existent"}, false, 1)
1132-}
1133-
1134-// checkCounterSum checks that statistics are properly collected.
1135-// It retries a few times as they are generally collected in background.
1136-func (s *StoreSuite) checkCounterSum(c *C, key []string, prefix bool, expected int64) {
1137- var sum int64
1138- var err error
1139- for retry := 0; retry < 10; retry++ {
1140- time.Sleep(1e8)
1141- sum, err = s.store.SumCounter(key, prefix)
1142- c.Assert(err, IsNil)
1143- if sum == expected {
1144- if expected == 0 && retry < 2 {
1145- continue // Wait a bit to make sure.
1146- }
1147- return
1148- }
1149- }
1150- c.Errorf("counter sum for %#v is %d, want %d", key, sum, expected)
1151-}
1152-
1153-func (s *StoreSuite) TestCharmStreaming(c *C) {
1154- server, curl := s.prepareServer(c)
1155-
1156- req, err := http.NewRequest("GET", "/charm/"+curl.String()[3:], nil)
1157- c.Assert(err, IsNil)
1158- rec := httptest.NewRecorder()
1159- server.ServeHTTP(rec, req)
1160-
1161- data, err := ioutil.ReadAll(rec.Body)
1162- c.Assert(string(data), Equals, "charm-revision-0")
1163-
1164- c.Assert(rec.Header().Get("Connection"), Equals, "close")
1165- c.Assert(rec.Header().Get("Content-Type"), Equals, "application/octet-stream")
1166- c.Assert(rec.Header().Get("Content-Length"), Equals, "16")
1167-
1168- // Check that it was accounted for in statistics.
1169- s.checkCounterSum(c, []string{"charm-bundle", curl.Series, curl.Name}, false, 1)
1170-}
1171-
1172-func (s *StoreSuite) TestDisableStats(c *C) {
1173- server, curl := s.prepareServer(c)
1174-
1175- req, err := http.NewRequest("GET", "/charm-info", nil)
1176- c.Assert(err, IsNil)
1177- req.Form = url.Values{"charms": []string{curl.String()}, "stats": []string{"0"}}
1178- rec := httptest.NewRecorder()
1179- server.ServeHTTP(rec, req)
1180- c.Assert(rec.Code, Equals, 200)
1181-
1182- req, err = http.NewRequest("GET", "/charm/"+curl.String()[3:], nil)
1183- c.Assert(err, IsNil)
1184- req.Form = url.Values{"stats": []string{"0"}}
1185- rec = httptest.NewRecorder()
1186- server.ServeHTTP(rec, req)
1187- c.Assert(rec.Code, Equals, 200)
1188-
1189- // No statistics should have been collected given the use of stats=0.
1190- for _, prefix := range []string{"charm-info", "charm-bundle", "charm-missing"} {
1191- s.checkCounterSum(c, []string{prefix}, true, 0)
1192- }
1193-}
1194-
1195-func (s *StoreSuite) TestServerStatus(c *C) {
1196- server, err := store.NewServer(s.store)
1197- c.Assert(err, IsNil)
1198- tests := []struct {
1199- path string
1200- code int
1201- }{
1202- {"/charm-info/any", 404},
1203- {"/charm/bad-url", 404},
1204- {"/charm/bad-series/wordpress", 404},
1205- {"/stats/counter/", 403},
1206- {"/stats/counter/*", 403},
1207- {"/stats/counter/any/", 404},
1208- {"/stats/", 404},
1209- {"/stats/any", 404},
1210- }
1211- for _, test := range tests {
1212- req, err := http.NewRequest("GET", test.path, nil)
1213- c.Assert(err, IsNil)
1214- rec := httptest.NewRecorder()
1215- server.ServeHTTP(rec, req)
1216- c.Assert(rec.Code, Equals, test.code, Commentf("Path: %s", test.path))
1217- }
1218-}
1219-
1220-func (s *StoreSuite) TestRootRedirect(c *C) {
1221- server, err := store.NewServer(s.store)
1222- c.Assert(err, IsNil)
1223- req, err := http.NewRequest("GET", "/", nil)
1224- c.Assert(err, IsNil)
1225- rec := httptest.NewRecorder()
1226- server.ServeHTTP(rec, req)
1227- c.Assert(rec.Code, Equals, 303)
1228- c.Assert(rec.Header().Get("Location"), Equals, "https://juju.ubuntu.com")
1229-}
1230-
1231-func (s *StoreSuite) TestStatsCounter(c *C) {
1232- for _, key := range [][]string{{"a", "b"}, {"a", "b"}, {"a"}} {
1233- err := s.store.IncCounter(key)
1234- c.Assert(err, IsNil)
1235- }
1236-
1237- server, _ := s.prepareServer(c)
1238-
1239- expected := map[string]string{
1240- "a:b": "2",
1241- "a:*": "3",
1242- "a": "1",
1243- }
1244-
1245- for counter, n := range expected {
1246- req, err := http.NewRequest("GET", "/stats/counter/"+counter, nil)
1247- c.Assert(err, IsNil)
1248- rec := httptest.NewRecorder()
1249- server.ServeHTTP(rec, req)
1250-
1251- data, err := ioutil.ReadAll(rec.Body)
1252- c.Assert(string(data), Equals, n)
1253-
1254- c.Assert(rec.Header().Get("Content-Type"), Equals, "text/plain")
1255- c.Assert(rec.Header().Get("Content-Length"), Equals, strconv.Itoa(len(n)))
1256- }
1257-}
1258-
1259-func (s *StoreSuite) TestBlitzKey(c *C) {
1260- server, _ := s.prepareServer(c)
1261-
1262- // This is just a validation key to allow blitz.io to run
1263- // performance tests against the site.
1264- req, err := http.NewRequest("GET", "/mu-35700a31-6bf320ca-a800b670-05f845ee", nil)
1265- c.Assert(err, IsNil)
1266- rec := httptest.NewRecorder()
1267- server.ServeHTTP(rec, req)
1268-
1269- data, err := ioutil.ReadAll(rec.Body)
1270- c.Assert(string(data), Equals, "42")
1271-
1272- c.Assert(rec.Header().Get("Connection"), Equals, "close")
1273- c.Assert(rec.Header().Get("Content-Type"), Equals, "text/plain")
1274- c.Assert(rec.Header().Get("Content-Length"), Equals, "2")
1275-}
1276
1277=== removed file 'store/store.go'
1278--- store/store.go 2012-10-11 17:40:17 +0000
1279+++ store/store.go 1970-01-01 00:00:00 +0000
1280@@ -1,774 +0,0 @@
1281-// The store package is capable of storing and updating charms in a MongoDB
1282-// database, as well as maintaining further information about them such as
1283-// the VCS revision the charm was loaded from and the URLs for the charms.
1284-package store
1285-
1286-import (
1287- "crypto/sha256"
1288- "encoding/hex"
1289- "errors"
1290- "fmt"
1291- "hash"
1292- "io"
1293- "labix.org/v2/mgo"
1294- "labix.org/v2/mgo/bson"
1295- "launchpad.net/juju-core/charm"
1296- "launchpad.net/juju-core/log"
1297- "sort"
1298- "strconv"
1299- "sync"
1300- "time"
1301-)
1302-
1303-// The following MongoDB collections are currently used:
1304-//
1305-// juju.events - Log of events relating to the lifecycle of charms
1306-// juju.charms - Information about the stored charms
1307-// juju.charmfs.* - GridFS with the charm files
1308-// juju.locks - Has unique keys with url of updating charms
1309-// juju.stat.counters - Counters for statistics
1310-// juju.stat.tokens - Tokens used in statistics counter keys
1311-
1312-var (
1313- ErrUpdateConflict = errors.New("charm update in progress")
1314- ErrRedundantUpdate = errors.New("charm is up-to-date")
1315- ErrNotFound = errors.New("entry not found")
1316-)
1317-
1318-const (
1319- UpdateTimeout = 600e9
1320-)
1321-
1322-// Store holds a connection to a charm store.
1323-type Store struct {
1324- session *storeSession
1325-
1326- // Cache for statistics key words (two generations).
1327- cacheMu sync.RWMutex
1328- statsTokenNew map[string]int
1329- statsTokenOld map[string]int
1330-}
1331-
1332-// Open creates a new session with the store. It connects to the MongoDB
1333-// server at the given address (as expected by the Mongo function in the
1334-// labix.org/v2/mgo package).
1335-func Open(mongoAddr string) (store *Store, err error) {
1336- log.Printf("store: Store opened. Connecting to: %s", mongoAddr)
1337- store = &Store{}
1338- session, err := mgo.Dial(mongoAddr)
1339- if err != nil {
1340- log.Printf("store: Error connecting to MongoDB: %v", err)
1341- return nil, err
1342- }
1343-
1344- store = &Store{session: &storeSession{session}}
1345-
1346- // Ignore error. It'll always fail after created.
1347- // TODO Check the error once mgo hands it to us.
1348- _ = store.session.DB("juju").Run(bson.D{{"create", "stat.counters"}, {"autoIndexId", false}}, nil)
1349-
1350- if err := store.ensureIndexes(); err != nil {
1351- session.Close()
1352- return nil, err
1353- }
1354-
1355- // Put the used socket back in the pool.
1356- session.Refresh()
1357- return store, nil
1358-}
1359-
1360-func (s *Store) ensureIndexes() error {
1361- session := s.session
1362- indexes := []struct {
1363- c *mgo.Collection
1364- i mgo.Index
1365- }{{
1366- session.StatCounters(),
1367- mgo.Index{Key: []string{"k", "t"}, Unique: true},
1368- }, {
1369- session.StatTokens(),
1370- mgo.Index{Key: []string{"t"}, Unique: true},
1371- }, {
1372- session.Charms(),
1373- mgo.Index{Key: []string{"urls", "revision"}, Unique: true},
1374- }, {
1375- session.Events(),
1376- mgo.Index{Key: []string{"urls", "digest"}},
1377- }}
1378- for _, idx := range indexes {
1379- err := idx.c.EnsureIndex(idx.i)
1380- if err != nil {
1381- log.Printf("store: Error ensuring stat.counters index: %v", err)
1382- return err
1383- }
1384- }
1385- return nil
1386-}
1387-
1388-// Close terminates the connection with the store.
1389-func (s *Store) Close() {
1390- s.session.Close()
1391-}
1392-
1393-// statsKey returns the compound statistics identifier that represents key.
1394-// If write is true, the identifier will be created if necessary.
1395-// Identifiers have a form similar to "ab:c:def:", where each section is a
1396-// base-32 number that represents the respective word in key. This form
1397-// allows efficiently indexing and searching for prefixes, while detaching
1398-// the key content and size from the actual words used in key.
1399-func (s *Store) statsKey(session *storeSession, key []string, write bool) (string, error) {
1400- if len(key) == 0 {
1401- return "", fmt.Errorf("store: empty statistics key")
1402- }
1403- tokens := session.StatTokens()
1404- skey := make([]byte, 0, len(key)*4)
1405- // Retry limit is mainly to prevent infinite recursion in edge cases,
1406- // such as if the database is ever run in read-only mode.
1407- // The logic below should deteministically stop in normal scenarios.
1408- var err error
1409- for i, retry := 0, 30; i < len(key) && retry > 0; retry-- {
1410- err = nil
1411- id, found := s.statsTokenId(key[i])
1412- if !found {
1413- var t struct {
1414- Id int "_id"
1415- Token string "t"
1416- }
1417- err = tokens.Find(bson.D{{"t", key[i]}}).One(&t)
1418- if err == mgo.ErrNotFound {
1419- if !write {
1420- return "", ErrNotFound
1421- }
1422- t.Id, err = tokens.Count()
1423- if err != nil {
1424- continue
1425- }
1426- t.Id++
1427- t.Token = key[i]
1428- err = tokens.Insert(&t)
1429- }
1430- if err != nil {
1431- continue
1432- }
1433- s.cacheStatsTokenId(t.Token, t.Id)
1434- id = t.Id
1435- }
1436- skey = strconv.AppendInt(skey, int64(id), 32)
1437- skey = append(skey, ':')
1438- i++
1439- }
1440- if err != nil {
1441- return "", err
1442- }
1443- return string(skey), nil
1444-}
1445-
1446-const statsTokenCacheSize = 512
1447-
1448-// cacheStatsTokenId adds the id for token into the cache.
1449-// The cache has two generations so that the least frequently used
1450-// tokens are evicted regularly.
1451-func (s *Store) cacheStatsTokenId(token string, id int) {
1452- s.cacheMu.Lock()
1453- defer s.cacheMu.Unlock()
1454- // Can't possibly be >, but reviews want it for defensiveness.
1455- if len(s.statsTokenNew) >= statsTokenCacheSize {
1456- s.statsTokenOld = s.statsTokenNew
1457- s.statsTokenNew = nil
1458- }
1459- if s.statsTokenNew == nil {
1460- s.statsTokenNew = make(map[string]int, statsTokenCacheSize)
1461- }
1462- s.statsTokenNew[token] = id
1463-}
1464-
1465-// statsTokenId returns the id for token from the cache, if found.
1466-func (s *Store) statsTokenId(token string) (id int, found bool) {
1467- s.cacheMu.RLock()
1468- id, found = s.statsTokenNew[token]
1469- if found {
1470- s.cacheMu.RUnlock()
1471- return
1472- }
1473- id, found = s.statsTokenOld[token]
1474- s.cacheMu.RUnlock()
1475- if found {
1476- s.cacheStatsTokenId(token, id)
1477- }
1478- return
1479-}
1480-
1481-var counterEpoch = time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
1482-
1483-// IncCounter increases by one the counter associated with the composed key.
1484-func (s *Store) IncCounter(key []string) error {
1485- session := s.session.Copy()
1486- defer session.Close()
1487-
1488- skey, err := s.statsKey(session, key, true)
1489- if err != nil {
1490- return err
1491- }
1492-
1493- t := time.Now().UTC()
1494- // Round to the start of the minute so we get one document per minute at most.
1495- t = t.Add(-time.Duration(t.Second()) * time.Second)
1496- counters := session.StatCounters()
1497- _, err = counters.Upsert(bson.D{{"k", skey}, {"t", int32(t.Unix() - counterEpoch)}}, bson.D{{"$inc", bson.D{{"c", 1}}}})
1498- return err
1499-}
1500-
1501-// SumCounter returns the sum of all the counters that exactly match key,
1502-// or that are prefixed by it if prefix is true.
1503-func (s *Store) SumCounter(key []string, prefix bool) (count int64, err error) {
1504- session := s.session.Copy()
1505- defer session.Close()
1506-
1507- skey, err := s.statsKey(session, key, false)
1508- if err == ErrNotFound {
1509- return 0, nil
1510- }
1511- if err != nil {
1512- return 0, err
1513- }
1514-
1515- var regex string
1516- if prefix {
1517- regex = "^" + skey
1518- } else {
1519- regex = "^" + skey + "$"
1520- }
1521-
1522- job := mgo.MapReduce{
1523- Map: "function() { emit('count', this.c); }",
1524- Reduce: "function(key, values) { return Array.sum(values); }",
1525- }
1526- var result []struct{ Value int64 }
1527- counters := session.StatCounters()
1528- _, err = counters.Find(bson.D{{"k", bson.D{{"$regex", regex}}}}).MapReduce(&job, &result)
1529- if len(result) > 0 {
1530- return result[0].Value, err
1531- }
1532- return 0, err
1533-}
1534-
1535-// A CharmPublisher is responsible for importing a charm dir onto the store.
1536-type CharmPublisher struct {
1537- revision int
1538- w *charmWriter
1539-}
1540-
1541-// Revision returns the revision that will be assigned to the published charm.
1542-func (p *CharmPublisher) Revision() int {
1543- return p.revision
1544-}
1545-
1546-// CharmDir matches the part of the interface of *charm.Dir that is necessary
1547-// to publish a charm. Using this interface rather than *charm.Dir directly
1548-// makes testing some aspects of the store possible.
1549-type CharmDir interface {
1550- Meta() *charm.Meta
1551- Config() *charm.Config
1552- SetRevision(revision int)
1553- BundleTo(w io.Writer) error
1554-}
1555-
1556-// Statically ensure that *charm.Dir is indeed a CharmDir.
1557-var _ CharmDir = (*charm.Dir)(nil)
1558-
1559-// Publish bundles charm and writes it to the store. The written charm
1560-// bundle will have its revision set to the result of Revision.
1561-// Publish must be called only once for a CharmPublisher.
1562-func (p *CharmPublisher) Publish(charm CharmDir) error {
1563- w := p.w
1564- if w == nil {
1565- panic("CharmPublisher already published a charm")
1566- }
1567- p.w = nil
1568- w.charm = charm
1569- // TODO: Refactor to BundleTo(w, revision)
1570- charm.SetRevision(p.revision)
1571- err := charm.BundleTo(w)
1572- if err == nil {
1573- err = w.finish()
1574- } else {
1575- w.abort()
1576- }
1577- return err
1578-}
1579-
1580-// CharmPublisher returns a new CharmPublisher for importing a charm that
1581-// will be made available in the store at all of the provided URLs.
1582-// The digest parameter must contain the unique identifier that
1583-// represents the charm data being imported (e.g. the VCS revision sha1).
1584-// ErrRedundantUpdate is returned if all of the provided urls are
1585-// already associated to that digest.
1586-func (s *Store) CharmPublisher(urls []*charm.URL, digest string) (p *CharmPublisher, err error) {
1587- log.Printf("store: Trying to add charms %v with key %q...", urls, digest)
1588- if err = mustLackRevision("CharmPublisher", urls...); err != nil {
1589- return
1590- }
1591- session := s.session.Copy()
1592- defer session.Close()
1593-
1594- maxRev := -1
1595- newKey := false
1596- charms := session.Charms()
1597- doc := charmDoc{}
1598- for i := range urls {
1599- urlStr := urls[i].String()
1600- err = charms.Find(bson.D{{"urls", urlStr}}).Sort("-revision").One(&doc)
1601- if err == mgo.ErrNotFound {
1602- log.Printf("store: Charm %s not yet in the store.", urls[i])
1603- newKey = true
1604- continue
1605- }
1606- if doc.Digest != digest {
1607- log.Printf("store: Charm %s is out of date with revision key %q.", urlStr, digest)
1608- newKey = true
1609- }
1610- if err != nil {
1611- log.Printf("store: Unknown error looking for charm %s: %s", urlStr, err)
1612- return
1613- }
1614- if doc.Revision > maxRev {
1615- maxRev = doc.Revision
1616- }
1617- }
1618- if !newKey {
1619- log.Printf("store: All charms have revision key %q. Nothing to update.", digest)
1620- err = ErrRedundantUpdate
1621- return
1622- }
1623- revision := maxRev + 1
1624- log.Printf("store: Preparing writer to add charms with revision %d.", revision)
1625- w := &charmWriter{
1626- store: s,
1627- urls: urls,
1628- revision: revision,
1629- digest: digest,
1630- }
1631- return &CharmPublisher{revision, w}, nil
1632-}
1633-
1634-// charmWriter is an io.Writer that writes charm bundles to the charms GridFS.
1635-type charmWriter struct {
1636- store *Store
1637- session *storeSession
1638- file *mgo.GridFile
1639- sha256 hash.Hash
1640- charm CharmDir
1641- urls []*charm.URL
1642- revision int
1643- digest string
1644-}
1645-
1646-// Write creates an entry in the charms GridFS when first called,
1647-// and streams all written data into it.
1648-func (w *charmWriter) Write(data []byte) (n int, err error) {
1649- if w.file == nil {
1650- w.session = w.store.session.Copy()
1651- w.file, err = w.session.CharmFS().Create("")
1652- if err != nil {
1653- log.Printf("store: Failed to create GridFS file: %v", err)
1654- return 0, err
1655- }
1656- w.sha256 = sha256.New()
1657- log.Printf("store: Creating GridFS file with id %q...", w.file.Id().(bson.ObjectId).Hex())
1658- }
1659- _, err = w.sha256.Write(data)
1660- if err != nil {
1661- panic("hash.Hash should never error")
1662- }
1663- return w.file.Write(data)
1664-}
1665-
1666-// abort cancels the charm writing.
1667-func (w *charmWriter) abort() {
1668- if w.file != nil {
1669- // Ignore error. Already aborting due to a preceding bad situation
1670- // elsewhere. This error is not important right now.
1671- _ = w.file.Close()
1672- w.session.Close()
1673- }
1674-}
1675-
1676-// finish completes the charm writing process and inserts the final metadata.
1677-// After it completes the charm will be available for consumption.
1678-func (w *charmWriter) finish() error {
1679- if w.file == nil {
1680- return nil
1681- }
1682- defer w.session.Close()
1683- id := w.file.Id()
1684- size := w.file.Size()
1685- err := w.file.Close()
1686- if err != nil {
1687- log.Printf("store: Failed to close GridFS file: %v", err)
1688- return err
1689- }
1690- charms := w.session.Charms()
1691- sha256 := hex.EncodeToString(w.sha256.Sum(nil))
1692- charm := charmDoc{
1693- w.urls,
1694- w.revision,
1695- w.digest,
1696- sha256,
1697- size,
1698- id.(bson.ObjectId),
1699- w.charm.Meta(),
1700- w.charm.Config(),
1701- }
1702- if err = charms.Insert(&charm); err != nil {
1703- err = maybeConflict(err)
1704- log.Printf("store: Failed to insert new revision of charm %v: %v", w.urls, err)
1705- return err
1706- }
1707- return nil
1708-}
1709-
1710-type CharmInfo struct {
1711- revision int
1712- digest string
1713- sha256 string
1714- size int64
1715- fileId bson.ObjectId
1716- meta *charm.Meta
1717- config *charm.Config
1718-}
1719-
1720-// Statically ensure CharmInfo is a charm.Charm.
1721-var _ charm.Charm = (*CharmInfo)(nil)
1722-
1723-// Revision returns the store charm's revision.
1724-func (ci *CharmInfo) Revision() int {
1725- return ci.revision
1726-}
1727-
1728-// BundleSha256 returns the sha256 checksum for the stored charm bundle.
1729-func (ci *CharmInfo) BundleSha256() string {
1730- return ci.sha256
1731-}
1732-
1733-// BundleSize returns the size for the stored charm bundle.
1734-func (ci *CharmInfo) BundleSize() int64 {
1735- return ci.size
1736-}
1737-
1738-// Digest returns the unique identifier that represents the charm
1739-// data imported. This is typically set to the VCS revision digest.
1740-func (ci *CharmInfo) Digest() string {
1741- return ci.digest
1742-}
1743-
1744-// Meta returns the charm.Meta details for the stored charm.
1745-func (ci *CharmInfo) Meta() *charm.Meta {
1746- return ci.meta
1747-}
1748-
1749-// Config returns the charm.Config details for the stored charm.
1750-func (ci *CharmInfo) Config() *charm.Config {
1751- return ci.config
1752-}
1753-
1754-// CharmInfo retrieves the CharmInfo value for the charm at url.
1755-func (s *Store) CharmInfo(url *charm.URL) (info *CharmInfo, err error) {
1756- session := s.session.Copy()
1757- defer session.Close()
1758-
1759- log.Debugf("store: Retrieving charm info for %s", url)
1760- rev := url.Revision
1761- url = url.WithRevision(-1)
1762-
1763- charms := session.Charms()
1764- var cdoc charmDoc
1765- var qdoc interface{}
1766- if rev == -1 {
1767- qdoc = bson.D{{"urls", url}}
1768- } else {
1769- qdoc = bson.D{{"urls", url}, {"revision", rev}}
1770- }
1771- err = charms.Find(qdoc).Sort("-revision").One(&cdoc)
1772- if err != nil {
1773- log.Printf("store: Failed to find charm %s: %v", url, err)
1774- return nil, ErrNotFound
1775- }
1776- info = &CharmInfo{
1777- cdoc.Revision,
1778- cdoc.Digest,
1779- cdoc.Sha256,
1780- cdoc.Size,
1781- cdoc.FileId,
1782- cdoc.Meta,
1783- cdoc.Config,
1784- }
1785- return info, nil
1786-}
1787-
1788-// OpenCharm opens for reading via rc the charm currently available at url.
1789-// rc must be closed after dealing with it or resources will leak.
1790-func (s *Store) OpenCharm(url *charm.URL) (info *CharmInfo, rc io.ReadCloser, err error) {
1791- log.Debugf("store: Opening charm %s", url)
1792- info, err = s.CharmInfo(url)
1793- if err != nil {
1794- return nil, nil, err
1795- }
1796- session := s.session.Copy()
1797- file, err := session.CharmFS().OpenId(info.fileId)
1798- if err != nil {
1799- log.Printf("store: Failed to open GridFS file for charm %s: %v", url, err)
1800- session.Close()
1801- return nil, nil, err
1802- }
1803- rc = &reader{session, file}
1804- return
1805-}
1806-
1807-type reader struct {
1808- session *storeSession
1809- file *mgo.GridFile
1810-}
1811-
1812-// Read consumes data from the opened charm.
1813-func (r *reader) Read(buf []byte) (n int, err error) {
1814- return r.file.Read(buf)
1815-}
1816-
1817-// Close closes the opened charm and frees associated resources.
1818-func (r *reader) Close() error {
1819- err := r.file.Close()
1820- r.session.Close()
1821- return err
1822-}
1823-
1824-// charmDoc represents the document stored in MongoDB for a charm.
1825-type charmDoc struct {
1826- URLs []*charm.URL
1827- Revision int
1828- Digest string
1829- Sha256 string
1830- Size int64
1831- FileId bson.ObjectId
1832- Meta *charm.Meta
1833- Config *charm.Config
1834-}
1835-
1836-// LockUpdates acquires a server-side lock for updating a single charm
1837-// that is supposed to be made available in all of the provided urls.
1838-// If the lock can't be acquired in any of the urls, an error will be
1839-// immediately returned.
1840-// In the usual case, any locking done is undone when an error happens,
1841-// or when l.Unlock is called. If something else goes wrong, the locks
1842-// will also expire after the period defined in UpdateTimeout.
1843-func (s *Store) LockUpdates(urls []*charm.URL) (l *UpdateLock, err error) {
1844- session := s.session.Copy()
1845- keys := make([]string, len(urls))
1846- for i := range urls {
1847- keys[i] = urls[i].String()
1848- }
1849- sort.Strings(keys)
1850- l = &UpdateLock{keys, session.Locks(), bson.Now()}
1851- if err = l.tryLock(); err != nil {
1852- session.Close()
1853- return nil, err
1854- }
1855- return l, nil
1856-}
1857-
1858-// UpdateLock represents an acquired update lock over a set of charm URLs.
1859-type UpdateLock struct {
1860- keys []string
1861- locks *mgo.Collection
1862- time time.Time
1863-}
1864-
1865-// Unlock removes the previously acquired server-side lock that prevents
1866-// other processes from attempting to update a set of charm URLs.
1867-func (l *UpdateLock) Unlock() {
1868- log.Debugf("store: Unlocking charms for future updates: %v", l.keys)
1869- defer l.locks.Database.Session.Close()
1870- for i := len(l.keys) - 1; i >= 0; i-- {
1871- // Using time below ensures only the proper lock is removed.
1872- // Can't do much about errors here. Locks will expire anyway.
1873- l.locks.Remove(bson.D{{"_id", l.keys[i]}, {"time", l.time}})
1874- }
1875-}
1876-
1877-// tryLock tries locking l.keys, one at a time, and succeeds only if it
1878-// can lock all of them in order. The keys should be pre-sorted so that
1879-// two-way conflicts can't happen. If any of the keys fail to be locked,
1880-// and expiring the old lock doesn't work, tryLock undoes all previous
1881-// locks and aborts with an error.
1882-func (l *UpdateLock) tryLock() error {
1883- for i, key := range l.keys {
1884- log.Debugf("store: Trying to lock charm %s for updates...", key)
1885- doc := bson.D{{"_id", key}, {"time", l.time}}
1886- err := l.locks.Insert(doc)
1887- if err == nil {
1888- log.Debugf("store: Charm %s is now locked for updates.", key)
1889- continue
1890- }
1891- if lerr, ok := err.(*mgo.LastError); ok && lerr.Code == 11000 {
1892- log.Debugf("store: Charm %s is locked. Trying to expire lock.", key)
1893- l.tryExpire(key)
1894- err = l.locks.Insert(doc)
1895- if err == nil {
1896- log.Debugf("store: Charm %s is now locked for updates.", key)
1897- continue
1898- }
1899- }
1900- // Couldn't lock everyone. Undo previous locks.
1901- for j := i - 1; j >= 0; j-- {
1902- // Using time below should be unnecessary, but it's an extra check.
1903- // Can't do anything about errors here. Lock will expire anyway.
1904- l.locks.Remove(bson.D{{"_id", l.keys[j]}, {"time", l.time}})
1905- }
1906- err = maybeConflict(err)
1907- log.Printf("store: Can't lock charms %v for updating: %v", l.keys, err)
1908- return err
1909- }
1910- return nil
1911-}
1912-
1913-// tryExpire attempts to remove outdated locks from the database.
1914-func (l *UpdateLock) tryExpire(key string) {
1915- // Ignore errors. If nothing happens the key will continue locked.
1916- l.locks.Remove(bson.D{{"_id", key}, {"time", bson.D{{"$lt", bson.Now().Add(-UpdateTimeout)}}}})
1917-}
1918-
1919-// maybeConflict returns an ErrUpdateConflict if err is a mgo
1920-// insert conflict LastError, or err itself otherwise.
1921-func maybeConflict(err error) error {
1922- if lerr, ok := err.(*mgo.LastError); ok && lerr.Code == 11000 {
1923- return ErrUpdateConflict
1924- }
1925- return err
1926-}
1927-
1928-// storeSession wraps a mgo.Session ands adds a few convenience methods.
1929-type storeSession struct {
1930- *mgo.Session
1931-}
1932-
1933-// Copy copies the storeSession and its underlying mgo session.
1934-func (s *storeSession) Copy() *storeSession {
1935- return &storeSession{s.Session.Copy()}
1936-}
1937-
1938-// Charms returns the mongo collection where charms are stored.
1939-func (s *storeSession) Charms() *mgo.Collection {
1940- return s.DB("juju").C("charms")
1941-}
1942-
1943-// Charms returns a mgo.GridFS to read and write charms.
1944-func (s *storeSession) CharmFS() *mgo.GridFS {
1945- return s.DB("juju").GridFS("charmfs")
1946-}
1947-
1948-// Events returns the mongo collection where charm events are stored.
1949-func (s *storeSession) Events() *mgo.Collection {
1950- return s.DB("juju").C("events")
1951-}
1952-
1953-// Locks returns the mongo collection where charm locks are stored.
1954-func (s *storeSession) Locks() *mgo.Collection {
1955- return s.DB("juju").C("locks")
1956-}
1957-
1958-// StatTokens returns the mongo collection for storing key tokens
1959-// for statistics collection.
1960-func (s *storeSession) StatTokens() *mgo.Collection {
1961- return s.DB("juju").C("stat.tokens")
1962-}
1963-
1964-// StatCounters returns the mongo collection for counter values.
1965-func (s *storeSession) StatCounters() *mgo.Collection {
1966- return s.DB("juju").C("stat.counters")
1967-}
1968-
1969-type CharmEventKind int
1970-
1971-const (
1972- EventPublished CharmEventKind = iota + 1
1973- EventPublishError
1974-
1975- EventKindCount
1976-)
1977-
1978-func (k CharmEventKind) String() string {
1979- switch k {
1980- case EventPublished:
1981- return "published"
1982- case EventPublishError:
1983- return "publish-error"
1984- }
1985- panic(fmt.Errorf("unknown charm event kind %d", k))
1986-}
1987-
1988-// CharmEvent is a record for an event relating to one or more charm URLs.
1989-type CharmEvent struct {
1990- Kind CharmEventKind
1991- Digest string
1992- Revision int
1993- URLs []*charm.URL
1994- Errors []string `bson:",omitempty"`
1995- Warnings []string `bson:",omitempty"`
1996- Time time.Time
1997-}
1998-
1999-// LogCharmEvent records an event related to one or more charm URLs.
2000-func (s *Store) LogCharmEvent(event *CharmEvent) (err error) {
2001- log.Printf("store: Adding charm event for %v with key %q: %s", event.URLs, event.Digest, event.Kind)
2002- if err = mustLackRevision("LogCharmEvent", event.URLs...); err != nil {
2003- return
2004- }
2005- session := s.session.Copy()
2006- defer session.Close()
2007- if event.Kind == 0 || event.Digest == "" || len(event.URLs) == 0 {
2008- return fmt.Errorf("LogCharmEvent: need valid Kind, Digest and URLs")
2009- }
2010- if event.Time.IsZero() {
2011- event.Time = time.Now()
2012- }
2013- events := session.Events()
2014- return events.Insert(event)
2015-}
2016-
2017-// CharmEvent returns the most recent event associated with url
2018-// and digest. If the specified event isn't found the error
2019-// ErrUnknownChange will be returned.
2020-func (s *Store) CharmEvent(url *charm.URL, digest string) (*CharmEvent, error) {
2021- // TODO: It'd actually make sense to find the charm event after the
2022- // revision id, but since we don't care about that now, just make sure
2023- // we don't write bad code.
2024- if err := mustLackRevision("CharmEvent", url); err != nil {
2025- return nil, err
2026- }
2027- session := s.session.Copy()
2028- defer session.Close()
2029-
2030- events := session.Events()
2031- event := &CharmEvent{Digest: digest}
2032- query := events.Find(bson.D{{"urls", url}, {"digest", digest}})
2033- query.Sort("-time")
2034- err := query.One(&event)
2035- if err == mgo.ErrNotFound {
2036- return nil, ErrNotFound
2037- }
2038- if err != nil {
2039- return nil, err
2040- }
2041- return event, nil
2042-}
2043-
2044-// mustLackRevision returns an error if any of the urls has a revision.
2045-func mustLackRevision(context string, urls ...*charm.URL) error {
2046- for _, url := range urls {
2047- if url.Revision != -1 {
2048- err := fmt.Errorf("%s: got charm URL with revision: %s", context, url)
2049- log.Printf("store: %v", err)
2050- return err
2051- }
2052- }
2053- return nil
2054-}
2055
2056=== removed file 'store/store_test.go'
2057--- store/store_test.go 2012-11-20 07:18:32 +0000
2058+++ store/store_test.go 1970-01-01 00:00:00 +0000
2059@@ -1,608 +0,0 @@
2060-package store_test
2061-
2062-import (
2063- "fmt"
2064- "io"
2065- "io/ioutil"
2066- "labix.org/v2/mgo/bson"
2067- . "launchpad.net/gocheck"
2068- "launchpad.net/juju-core/charm"
2069- "launchpad.net/juju-core/log"
2070- "launchpad.net/juju-core/store"
2071- "launchpad.net/juju-core/testing"
2072- "strconv"
2073- "sync"
2074- stdtesting "testing"
2075- "time"
2076-)
2077-
2078-func Test(t *stdtesting.T) {
2079- TestingT(t)
2080-}
2081-
2082-var _ = Suite(&StoreSuite{})
2083-var _ = Suite(&TrivialSuite{})
2084-
2085-type StoreSuite struct {
2086- MgoSuite
2087- testing.HTTPSuite
2088- store *store.Store
2089-}
2090-
2091-type TrivialSuite struct{}
2092-
2093-func (s *StoreSuite) SetUpSuite(c *C) {
2094- s.MgoSuite.SetUpSuite(c)
2095- s.HTTPSuite.SetUpSuite(c)
2096-}
2097-
2098-func (s *StoreSuite) TearDownSuite(c *C) {
2099- s.HTTPSuite.TearDownSuite(c)
2100- s.MgoSuite.TearDownSuite(c)
2101-}
2102-
2103-func (s *StoreSuite) SetUpTest(c *C) {
2104- s.MgoSuite.SetUpTest(c)
2105- var err error
2106- s.store, err = store.Open(s.Addr)
2107- c.Assert(err, IsNil)
2108- log.Target = c
2109- log.Debug = true
2110-}
2111-
2112-func (s *StoreSuite) TearDownTest(c *C) {
2113- s.HTTPSuite.TearDownTest(c)
2114- if s.store != nil {
2115- s.store.Close()
2116- }
2117- s.MgoSuite.TearDownTest(c)
2118-}
2119-
2120-// FakeCharmDir is a charm that implements the interface that the
2121-// store publisher cares about.
2122-type FakeCharmDir struct {
2123- revision interface{} // so we can tell if it's not set.
2124- error string
2125-}
2126-
2127-func (d *FakeCharmDir) Meta() *charm.Meta {
2128- return &charm.Meta{
2129- Name: "fakecharm",
2130- Summary: "Fake charm for testing purposes.",
2131- Description: "This is a fake charm for testing purposes.\n",
2132- Provides: make(map[string]charm.Relation),
2133- Requires: make(map[string]charm.Relation),
2134- Peers: make(map[string]charm.Relation),
2135- }
2136-}
2137-
2138-func (d *FakeCharmDir) Config() *charm.Config {
2139- return &charm.Config{make(map[string]charm.Option)}
2140-}
2141-
2142-func (d *FakeCharmDir) SetRevision(revision int) {
2143- d.revision = revision
2144-}
2145-
2146-func (d *FakeCharmDir) BundleTo(w io.Writer) error {
2147- if d.error == "beforeWrite" {
2148- return fmt.Errorf(d.error)
2149- }
2150- _, err := w.Write([]byte(fmt.Sprintf("charm-revision-%v", d.revision)))
2151- if d.error == "afterWrite" {
2152- return fmt.Errorf(d.error)
2153- }
2154- return err
2155-}
2156-
2157-func (s *StoreSuite) TestCharmPublisherWithRevisionedURL(c *C) {
2158- urls := []*charm.URL{charm.MustParseURL("cs:oneiric/wordpress-0")}
2159- pub, err := s.store.CharmPublisher(urls, "some-digest")
2160- c.Assert(err, ErrorMatches, "CharmPublisher: got charm URL with revision: cs:oneiric/wordpress-0")
2161- c.Assert(pub, IsNil)
2162-}
2163-
2164-func (s *StoreSuite) TestCharmPublisher(c *C) {
2165- urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
2166- urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
2167- urls := []*charm.URL{urlA, urlB}
2168-
2169- pub, err := s.store.CharmPublisher(urls, "some-digest")
2170- c.Assert(err, IsNil)
2171- c.Assert(pub.Revision(), Equals, 0)
2172-
2173- err = pub.Publish(testing.Charms.ClonedDir(c.MkDir(), "series", "dummy"))
2174- c.Assert(err, IsNil)
2175-
2176- for _, url := range urls {
2177- info, rc, err := s.store.OpenCharm(url)
2178- c.Assert(err, IsNil)
2179- c.Assert(info.Revision(), Equals, 0)
2180- c.Assert(info.Digest(), Equals, "some-digest")
2181- data, err := ioutil.ReadAll(rc)
2182- c.Check(err, IsNil)
2183- err = rc.Close()
2184- c.Assert(err, IsNil)
2185- bundle, err := charm.ReadBundleBytes(data)
2186- c.Assert(err, IsNil)
2187-
2188- // The same information must be available by reading the
2189- // full charm data...
2190- c.Assert(bundle.Meta().Name, Equals, "dummy")
2191- c.Assert(bundle.Config().Options["title"].Default, Equals, "My Title")
2192-
2193- // ... and the queriable details.
2194- c.Assert(info.Meta().Name, Equals, "dummy")
2195- c.Assert(info.Config().Options["title"].Default, Equals, "My Title")
2196-
2197- info2, err := s.store.CharmInfo(url)
2198- c.Assert(err, IsNil)
2199- c.Assert(info2, DeepEquals, info)
2200- }
2201-}
2202-
2203-func (s *StoreSuite) TestCharmPublishError(c *C) {
2204- url := charm.MustParseURL("cs:oneiric/wordpress")
2205- urls := []*charm.URL{url}
2206-
2207- // Publish one successfully to bump the revision so we can
2208- // make sure it is being correctly set below.
2209- pub, err := s.store.CharmPublisher(urls, "one-digest")
2210- c.Assert(err, IsNil)
2211- c.Assert(pub.Revision(), Equals, 0)
2212- err = pub.Publish(&FakeCharmDir{})
2213- c.Assert(err, IsNil)
2214-
2215- pub, err = s.store.CharmPublisher(urls, "another-digest")
2216- c.Assert(err, IsNil)
2217- c.Assert(pub.Revision(), Equals, 1)
2218- err = pub.Publish(&FakeCharmDir{error: "beforeWrite"})
2219- c.Assert(err, ErrorMatches, "beforeWrite")
2220-
2221- pub, err = s.store.CharmPublisher(urls, "another-digest")
2222- c.Assert(err, IsNil)
2223- c.Assert(pub.Revision(), Equals, 1)
2224- err = pub.Publish(&FakeCharmDir{error: "afterWrite"})
2225- c.Assert(err, ErrorMatches, "afterWrite")
2226-
2227- // Still at the original charm revision that succeeded first.
2228- info, err := s.store.CharmInfo(url)
2229- c.Assert(err, IsNil)
2230- c.Assert(info.Revision(), Equals, 0)
2231- c.Assert(info.Digest(), Equals, "one-digest")
2232-}
2233-
2234-func (s *StoreSuite) TestCharmInfoNotFound(c *C) {
2235- info, err := s.store.CharmInfo(charm.MustParseURL("cs:oneiric/wordpress"))
2236- c.Assert(err, Equals, store.ErrNotFound)
2237- c.Assert(info, IsNil)
2238-}
2239-
2240-func (s *StoreSuite) TestRevisioning(c *C) {
2241- urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
2242- urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
2243- urls := []*charm.URL{urlA, urlB}
2244-
2245- tests := []struct {
2246- urls []*charm.URL
2247- data string
2248- }{
2249- {urls[0:], "charm-revision-0"},
2250- {urls[1:], "charm-revision-1"},
2251- {urls[0:], "charm-revision-2"},
2252- }
2253-
2254- for i, t := range tests {
2255- pub, err := s.store.CharmPublisher(t.urls, fmt.Sprintf("digest-%d", i))
2256- c.Assert(err, IsNil)
2257- c.Assert(pub.Revision(), Equals, i)
2258-
2259- err = pub.Publish(&FakeCharmDir{})
2260- c.Assert(err, IsNil)
2261- }
2262-
2263- for i, t := range tests {
2264- for _, url := range t.urls {
2265- url = url.WithRevision(i)
2266- info, rc, err := s.store.OpenCharm(url)
2267- c.Assert(err, IsNil)
2268- data, err := ioutil.ReadAll(rc)
2269- cerr := rc.Close()
2270- c.Assert(info.Revision(), Equals, i)
2271- c.Assert(url.Revision, Equals, i) // Untouched.
2272- c.Assert(cerr, IsNil)
2273- c.Assert(string(data), Equals, string(t.data))
2274- c.Assert(err, IsNil)
2275- }
2276- }
2277-
2278- info, rc, err := s.store.OpenCharm(urlA.WithRevision(1))
2279- c.Assert(err, Equals, store.ErrNotFound)
2280- c.Assert(info, IsNil)
2281- c.Assert(rc, IsNil)
2282-}
2283-
2284-func (s *StoreSuite) TestLockUpdates(c *C) {
2285- urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
2286- urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
2287- urls := []*charm.URL{urlA, urlB}
2288-
2289- // Lock update of just B to force a partial conflict.
2290- lock1, err := s.store.LockUpdates(urls[1:])
2291- c.Assert(err, IsNil)
2292-
2293- // Partially conflicts with locked update above.
2294- lock2, err := s.store.LockUpdates(urls)
2295- c.Check(err, Equals, store.ErrUpdateConflict)
2296- c.Check(lock2, IsNil)
2297-
2298- lock1.Unlock()
2299-
2300- // Trying again should work since lock1 was released.
2301- lock3, err := s.store.LockUpdates(urls)
2302- c.Assert(err, IsNil)
2303- lock3.Unlock()
2304-}
2305-
2306-func (s *StoreSuite) TestLockUpdatesExpires(c *C) {
2307- urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
2308- urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
2309- urls := []*charm.URL{urlA, urlB}
2310-
2311- // Initiate an update of B only to force a partial conflict.
2312- lock1, err := s.store.LockUpdates(urls[1:])
2313- c.Assert(err, IsNil)
2314-
2315- // Hack time to force an expiration.
2316- locks := s.Session.DB("juju").C("locks")
2317- selector := bson.M{"_id": urlB.String()}
2318- update := bson.M{"time": bson.Now().Add(-store.UpdateTimeout - 10e9)}
2319- err = locks.Update(selector, update)
2320- c.Check(err, IsNil)
2321-
2322- // Works due to expiration of previous lock.
2323- lock2, err := s.store.LockUpdates(urls)
2324- c.Assert(err, IsNil)
2325- defer lock2.Unlock()
2326-
2327- // The expired lock was forcefully killed. Unlocking it must
2328- // not interfere with lock2 which is still alive.
2329- lock1.Unlock()
2330-
2331- // The above statement was a NOOP and lock2 is still in effect,
2332- // so attempting another lock must necessarily fail.
2333- lock3, err := s.store.LockUpdates(urls)
2334- c.Check(err == store.ErrUpdateConflict, Equals, true)
2335- c.Check(lock3, IsNil)
2336-}
2337-
2338-func (s *StoreSuite) TestConflictingUpdate(c *C) {
2339- // This test checks that if for whatever reason the locking
2340- // safety-net fails, adding two charms in parallel still
2341- // results in a sane outcome.
2342- url := charm.MustParseURL("cs:oneiric/wordpress")
2343- urls := []*charm.URL{url}
2344-
2345- pub1, err := s.store.CharmPublisher(urls, "some-digest")
2346- c.Assert(err, IsNil)
2347- c.Assert(pub1.Revision(), Equals, 0)
2348-
2349- pub2, err := s.store.CharmPublisher(urls, "some-digest")
2350- c.Assert(err, IsNil)
2351- c.Assert(pub2.Revision(), Equals, 0)
2352-
2353- // The first publishing attempt should work.
2354- err = pub2.Publish(&FakeCharmDir{})
2355- c.Assert(err, IsNil)
2356-
2357- // Attempting to finish the second attempt should break,
2358- // since it lost the race and the given revision is already
2359- // in place.
2360- err = pub1.Publish(&FakeCharmDir{})
2361- c.Assert(err, Equals, store.ErrUpdateConflict)
2362-}
2363-
2364-func (s *StoreSuite) TestRedundantUpdate(c *C) {
2365- urlA := charm.MustParseURL("cs:oneiric/wordpress-a")
2366- urlB := charm.MustParseURL("cs:oneiric/wordpress-b")
2367- urls := []*charm.URL{urlA, urlB}
2368-
2369- pub, err := s.store.CharmPublisher(urls, "digest-0")
2370- c.Assert(err, IsNil)
2371- c.Assert(pub.Revision(), Equals, 0)
2372- err = pub.Publish(&FakeCharmDir{})
2373- c.Assert(err, IsNil)
2374-
2375- // All charms are already on digest-0.
2376- pub, err = s.store.CharmPublisher(urls, "digest-0")
2377- c.Assert(err, ErrorMatches, "charm is up-to-date")
2378- c.Assert(err, Equals, store.ErrRedundantUpdate)
2379- c.Assert(pub, IsNil)
2380-
2381- // Now add a second revision just for wordpress-b.
2382- pub, err = s.store.CharmPublisher(urls[1:], "digest-1")
2383- c.Assert(err, IsNil)
2384- c.Assert(pub.Revision(), Equals, 1)
2385- err = pub.Publish(&FakeCharmDir{})
2386- c.Assert(err, IsNil)
2387-
2388- // Same digest bumps revision because one of them was old.
2389- pub, err = s.store.CharmPublisher(urls, "digest-1")
2390- c.Assert(err, IsNil)
2391- c.Assert(pub.Revision(), Equals, 2)
2392- err = pub.Publish(&FakeCharmDir{})
2393- c.Assert(err, IsNil)
2394-}
2395-
2396-const fakeRevZeroSha = "319095521ac8a62fa1e8423351973512ecca8928c9f62025e37de57c9ef07a53"
2397-
2398-func (s *StoreSuite) TestCharmBundleData(c *C) {
2399- url := charm.MustParseURL("cs:oneiric/wordpress")
2400- urls := []*charm.URL{url}
2401-
2402- pub, err := s.store.CharmPublisher(urls, "key")
2403- c.Assert(err, IsNil)
2404- c.Assert(pub.Revision(), Equals, 0)
2405-
2406- err = pub.Publish(&FakeCharmDir{})
2407- c.Assert(err, IsNil)
2408-
2409- info, rc, err := s.store.OpenCharm(url)
2410- c.Assert(err, IsNil)
2411- c.Check(info.BundleSha256(), Equals, fakeRevZeroSha)
2412- c.Check(info.BundleSize(), Equals, int64(len("charm-revision-0")))
2413- err = rc.Close()
2414- c.Check(err, IsNil)
2415-}
2416-
2417-func (s *StoreSuite) TestLogCharmEventWithRevisionedURL(c *C) {
2418- url := charm.MustParseURL("cs:oneiric/wordpress-0")
2419- event := &store.CharmEvent{
2420- Kind: store.EventPublishError,
2421- Digest: "some-digest",
2422- URLs: []*charm.URL{url},
2423- }
2424- err := s.store.LogCharmEvent(event)
2425- c.Assert(err, ErrorMatches, "LogCharmEvent: got charm URL with revision: cs:oneiric/wordpress-0")
2426-
2427- // This may work in the future, but not now.
2428- event, err = s.store.CharmEvent(url, "some-digest")
2429- c.Assert(err, ErrorMatches, "CharmEvent: got charm URL with revision: cs:oneiric/wordpress-0")
2430- c.Assert(event, IsNil)
2431-}
2432-
2433-func (s *StoreSuite) TestLogCharmEvent(c *C) {
2434- url1 := charm.MustParseURL("cs:oneiric/wordpress")
2435- url2 := charm.MustParseURL("cs:oneiric/mysql")
2436- urls := []*charm.URL{url1, url2}
2437-
2438- event1 := &store.CharmEvent{
2439- Kind: store.EventPublished,
2440- Revision: 42,
2441- Digest: "revKey1",
2442- URLs: urls,
2443- Warnings: []string{"A warning."},
2444- Time: time.Unix(1, 0),
2445- }
2446- event2 := &store.CharmEvent{
2447- Kind: store.EventPublished,
2448- Revision: 42,
2449- Digest: "revKey2",
2450- URLs: urls,
2451- Time: time.Unix(1, 0),
2452- }
2453- event3 := &store.CharmEvent{
2454- Kind: store.EventPublishError,
2455- Digest: "revKey2",
2456- Errors: []string{"An error."},
2457- URLs: urls[:1],
2458- }
2459-
2460- for _, event := range []*store.CharmEvent{event1, event2, event3} {
2461- err := s.store.LogCharmEvent(event)
2462- c.Assert(err, IsNil)
2463- }
2464-
2465- events := s.Session.DB("juju").C("events")
2466- var s1, s2 map[string]interface{}
2467-
2468- err := events.Find(bson.M{"digest": "revKey1"}).One(&s1)
2469- c.Assert(err, IsNil)
2470- c.Assert(s1["kind"], Equals, int(store.EventPublished))
2471- c.Assert(s1["urls"], DeepEquals, []interface{}{"cs:oneiric/wordpress", "cs:oneiric/mysql"})
2472- c.Assert(s1["warnings"], DeepEquals, []interface{}{"A warning."})
2473- c.Assert(s1["errors"], IsNil)
2474- c.Assert(s1["time"], DeepEquals, time.Unix(1, 0))
2475-
2476- err = events.Find(bson.M{"digest": "revKey2", "kind": store.EventPublishError}).One(&s2)
2477- c.Assert(err, IsNil)
2478- c.Assert(s2["urls"], DeepEquals, []interface{}{"cs:oneiric/wordpress"})
2479- c.Assert(s2["warnings"], IsNil)
2480- c.Assert(s2["errors"], DeepEquals, []interface{}{"An error."})
2481- c.Assert(s2["time"].(time.Time).After(bson.Now().Add(-10e9)), Equals, true)
2482-
2483- // Mongo stores timestamps in milliseconds, so chop
2484- // off the extra bits for comparison.
2485- event3.Time = time.Unix(0, event3.Time.UnixNano()/1e6*1e6)
2486-
2487- event, err := s.store.CharmEvent(urls[0], "revKey2")
2488- c.Assert(err, IsNil)
2489- c.Assert(event, DeepEquals, event3)
2490-
2491- event, err = s.store.CharmEvent(urls[1], "revKey1")
2492- c.Assert(err, IsNil)
2493- c.Assert(event, DeepEquals, event1)
2494-
2495- event, err = s.store.CharmEvent(urls[1], "revKeyX")
2496- c.Assert(err, Equals, store.ErrNotFound)
2497- c.Assert(event, IsNil)
2498-}
2499-
2500-func (s *StoreSuite) TestCounters(c *C) {
2501- sum, err := s.store.SumCounter([]string{"a"}, false)
2502- c.Assert(err, IsNil)
2503- c.Assert(sum, Equals, int64(0))
2504-
2505- for i := 0; i < 10; i++ {
2506- err := s.store.IncCounter([]string{"a", "b", "c"})
2507- c.Assert(err, IsNil)
2508- }
2509- for i := 0; i < 7; i++ {
2510- s.store.IncCounter([]string{"a", "b"})
2511- c.Assert(err, IsNil)
2512- }
2513- for i := 0; i < 3; i++ {
2514- s.store.IncCounter([]string{"a", "z", "b"})
2515- c.Assert(err, IsNil)
2516- }
2517-
2518- tests := []struct {
2519- key []string
2520- prefix bool
2521- result int64
2522- }{
2523- {[]string{"a", "b", "c"}, false, 10},
2524- {[]string{"a", "b"}, false, 7},
2525- {[]string{"a", "z", "b"}, false, 3},
2526- {[]string{"a", "b", "c"}, true, 10},
2527- {[]string{"a", "b"}, true, 17},
2528- {[]string{"a"}, true, 20},
2529- {[]string{"b"}, true, 0},
2530- }
2531-
2532- for _, t := range tests {
2533- c.Logf("Test: %#v\n", t)
2534- sum, err := s.store.SumCounter(t.key, t.prefix)
2535- c.Assert(err, IsNil)
2536- c.Assert(sum, Equals, t.result)
2537- }
2538-
2539- // High-level interface works. Now check that the data is
2540- // stored correctly.
2541- counters := s.Session.DB("juju").C("stat.counters")
2542- docs1, err := counters.Count()
2543- c.Assert(err, IsNil)
2544- if docs1 != 3 && docs1 != 4 {
2545- fmt.Errorf("Expected 3 or 4 docs in counters collection, got %d", docs1)
2546- }
2547-
2548- // Hack times so that the next operation adds another document.
2549- err = counters.Update(nil, bson.D{{"$set", bson.D{{"t", 1}}}})
2550- c.Check(err, IsNil)
2551-
2552- err = s.store.IncCounter([]string{"a", "b", "c"})
2553- c.Assert(err, IsNil)
2554-
2555- docs2, err := counters.Count()
2556- c.Assert(err, IsNil)
2557- c.Assert(docs2, Equals, docs1+1)
2558-
2559- sum, err = s.store.SumCounter([]string{"a", "b", "c"}, false)
2560- c.Assert(err, IsNil)
2561- c.Assert(sum, Equals, int64(11))
2562-
2563- sum, err = s.store.SumCounter([]string{"a"}, true)
2564- c.Assert(err, IsNil)
2565- c.Assert(sum, Equals, int64(21))
2566-}
2567-
2568-func (s *StoreSuite) TestCountersReadOnlySum(c *C) {
2569- // Summing up an unknown key shouldn't add the key to the database.
2570- sum, err := s.store.SumCounter([]string{"a", "b", "c"}, false)
2571- c.Assert(err, IsNil)
2572- c.Assert(sum, Equals, int64(0))
2573-
2574- tokens := s.Session.DB("juju").C("stat.tokens")
2575- n, err := tokens.Count()
2576- c.Assert(err, IsNil)
2577- c.Assert(n, Equals, 0)
2578-}
2579-
2580-func (s *StoreSuite) TestCountersTokenCaching(c *C) {
2581- sum, err := s.store.SumCounter([]string{"a"}, false)
2582- c.Assert(err, IsNil)
2583- c.Assert(sum, Equals, int64(0))
2584-
2585- const genSize = 512
2586-
2587- // All of these will be cached, as we have two generations
2588- // of genSize entries each.
2589- for i := 0; i < genSize*2; i++ {
2590- err := s.store.IncCounter([]string{strconv.Itoa(i)})
2591- c.Assert(err, IsNil)
2592- }
2593-
2594- // Now go behind the scenes and corrupt all the tokens.
2595- tokens := s.Session.DB("juju").C("stat.tokens")
2596- iter := tokens.Find(nil).Iter()
2597- var t struct {
2598- Id int "_id"
2599- Token string "t"
2600- }
2601- for iter.Next(&t) {
2602- err := tokens.UpdateId(t.Id, bson.M{"$set": bson.M{"t": "corrupted" + t.Token}})
2603- c.Assert(err, IsNil)
2604- }
2605- c.Assert(iter.Err(), IsNil)
2606-
2607- // We can consult the counters for the cached entries still.
2608- // First, check that the newest generation is good.
2609- for i := genSize; i < genSize*2; i++ {
2610- n, err := s.store.SumCounter([]string{strconv.Itoa(i)}, false)
2611- c.Assert(err, IsNil)
2612- c.Assert(n, Equals, int64(1))
2613- }
2614-
2615- // Now, we can still access a single entry of the older generation,
2616- // but this will cause the generations to flip and thus the rest
2617- // of the old generation will go away as the top half of the
2618- // entries is turned into the old generation.
2619- n, err := s.store.SumCounter([]string{"0"}, false)
2620- c.Assert(err, IsNil)
2621- c.Assert(n, Equals, int64(1))
2622-
2623- // Now we've lost access to the rest of the old generation.
2624- for i := 1; i < genSize; i++ {
2625- n, err := s.store.SumCounter([]string{strconv.Itoa(i)}, false)
2626- c.Assert(err, IsNil)
2627- c.Assert(n, Equals, int64(0))
2628- }
2629-
2630- // But we still have all of the top half available since it was
2631- // moved into the old generation.
2632- for i := genSize; i < genSize*2; i++ {
2633- n, err := s.store.SumCounter([]string{strconv.Itoa(i)}, false)
2634- c.Assert(err, IsNil)
2635- c.Assert(n, Equals, int64(1))
2636- }
2637-}
2638-
2639-func (s *StoreSuite) TestCounterTokenUniqueness(c *C) {
2640- var wg0, wg1 sync.WaitGroup
2641- wg0.Add(10)
2642- wg1.Add(10)
2643- for i := 0; i < 10; i++ {
2644- go func() {
2645- wg0.Done()
2646- wg0.Wait()
2647- defer wg1.Done()
2648- err := s.store.IncCounter([]string{"a"})
2649- c.Check(err, IsNil)
2650- }()
2651- }
2652- wg1.Wait()
2653-
2654- sum, err := s.store.SumCounter([]string{"a"}, false)
2655- c.Assert(err, IsNil)
2656- c.Assert(sum, Equals, int64(10))
2657-}
2658-
2659-func (s *TrivialSuite) TestEventString(c *C) {
2660- c.Assert(store.EventPublished, Matches, "published")
2661- c.Assert(store.EventPublishError, Matches, "publish-error")
2662- for kind := store.CharmEventKind(1); kind < store.EventKindCount; kind++ {
2663- // This guarantees the switch in String is properly
2664- // updated with new event kinds.
2665- c.Assert(kind.String(), Matches, "[a-z-]+")
2666- }
2667-}

Subscribers

People subscribed via source and target branches