services/application/applicationd: TidyNow support.
This change adde a TidyNow implementation to applicationd that removes
all but the configured number (5) most recent envelopes.
Change-Id: I44f68ac80eab2850d35e2a68e2fd5eb8f3aabccf
diff --git a/services/application/applicationd/impl_test.go b/services/application/applicationd/impl_test.go
index 68b07f9..b045bbf 100644
--- a/services/application/applicationd/impl_test.go
+++ b/services/application/applicationd/impl_test.go
@@ -5,6 +5,7 @@
package main_test
import (
+ "fmt"
"io/ioutil"
"os"
"reflect"
@@ -303,3 +304,238 @@
t.Fatalf("Incorrect output: expected %v, got %v", envelopeV1, output)
}
}
+
+// TestTidyNow tests that TidyNow operates correctly.
+func TestTidyNow(t *testing.T) {
+ ctx, shutdown := test.V23Init()
+ defer shutdown()
+
+ dir, prefix := "", ""
+ store, err := ioutil.TempDir(dir, prefix)
+ if err != nil {
+ t.Fatalf("TempDir(%q, %q) failed: %v", dir, prefix, err)
+ }
+ defer os.RemoveAll(store)
+ dispatcher, err := appd.NewDispatcher(store)
+ if err != nil {
+ t.Fatalf("NewDispatcher() failed: %v", err)
+ }
+
+ server, err := xrpc.NewDispatchingServer(ctx, "", dispatcher)
+ if err != nil {
+ t.Fatalf("NewServer(%v) failed: %v", dispatcher, err)
+ }
+
+ defer func() {
+ if err := server.Stop(); err != nil {
+ t.Fatalf("Stop() failed: %v", err)
+ }
+ }()
+ endpoint := server.Status().Endpoints[0].String()
+
+ // Create client stubs for talking to the server.
+ stub := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search"))
+ stubs := make([]repository.ApplicationClientStub, 0)
+ for _, vn := range []string{"v0", "v1", "v2", "v3"} {
+ s := repository.ApplicationClient(naming.JoinAddressName(endpoint, fmt.Sprintf("search/%s", vn)))
+ stubs = append(stubs, s)
+ }
+ blessings, sig := newPublisherSignature(t, ctx, []byte("binarycontents"))
+
+ // Create example envelopes.
+ envelopeV1 := application.Envelope{
+ Args: []string{"--help"},
+ Env: []string{"DEBUG=1"},
+ Binary: application.SignedFile{
+ File: "/v23/name/of/binary",
+ Signature: sig,
+ },
+ Publisher: blessings,
+ }
+ envelopeV2 := application.Envelope{
+ Args: []string{"--verbose"},
+ Env: []string{"DEBUG=0"},
+ Binary: application.SignedFile{
+ File: "/v23/name/of/binary",
+ Signature: sig,
+ },
+ Publisher: blessings,
+ }
+ envelopeV3 := application.Envelope{
+ Args: []string{"--verbose", "--spiffynewflag"},
+ Env: []string{"DEBUG=0"},
+ Binary: application.SignedFile{
+ File: "/v23/name/of/binary",
+ Signature: sig,
+ },
+ Publisher: blessings,
+ }
+
+ stuffEnvelopes(t, ctx, stubs, []profEnvTuple{
+ {
+ &envelopeV1,
+ []string{"base", "media"},
+ },
+ })
+
+ // Verify that we have one
+ testGlob(t, ctx, endpoint, []string{
+ "",
+ "search",
+ "search/v0",
+ })
+
+ // Tidy when already tidy does not alter.
+ if err := stubs[0].TidyNow(ctx); err != nil {
+ t.Errorf("TidyNow failed: %v", err)
+ }
+ testGlob(t, ctx, endpoint, []string{
+ "",
+ "search",
+ "search/v0",
+ })
+
+ stuffEnvelopes(t, ctx, stubs, []profEnvTuple{
+ {
+ &envelopeV1,
+ []string{"base", "media"},
+ },
+ {
+ &envelopeV2,
+ []string{"media"},
+ },
+ {
+ &envelopeV3,
+ []string{"base"},
+ },
+ })
+
+ // Now there are three envelopes which is one more than the
+ // numberOfVersionsToKeep set for the test. However
+ // we need both envelopes v0 and v2 to keep two versions for
+ // profile media and envelopes v0 and v3 to keep two versions
+ // for profile base so we continue to have three versions.
+ if err := stubs[0].TidyNow(ctx); err != nil {
+ t.Errorf("TidyNow failed: %v", err)
+ }
+ testGlob(t, ctx, endpoint, []string{
+ "",
+ "search",
+ "search/v0",
+ "search/v1",
+ "search/v2",
+ })
+
+ // And the newest version for each profile differs because
+ // not every version supports all profiles.
+ output1, err := stub.Match(ctx, []string{"media"})
+ if err != nil {
+ t.Fatalf("Match(%v) failed: %v", "media", err)
+ }
+ if !reflect.DeepEqual(envelopeV2, output1) {
+ t.Fatalf("Incorrect output: expected %v, got %v", envelopeV2, output1)
+ }
+
+ output2, err := stub.Match(ctx, []string{"base"})
+ if err != nil {
+ t.Fatalf("Match(%v) failed: %v", "base", err)
+ }
+ if !reflect.DeepEqual(envelopeV3, output2) {
+ t.Fatalf("Incorrect output: expected %v, got %v", envelopeV3, output2)
+ }
+
+ // Test that we can add an envelope for v3 with profile media and after calling
+ // TidyNow(), there will be all versions still in glob but v0 will only match profile
+ // base and not have an envelope for profile media.
+ if err := stubs[3].Put(ctx, []string{"media"}, envelopeV3); err != nil {
+ t.Fatalf("Put() failed: %v", err)
+ }
+
+ if err := stubs[0].TidyNow(ctx); err != nil {
+ t.Errorf("TidyNow failed: %v", err)
+ }
+ testGlob(t, ctx, endpoint, []string{
+ "",
+ "search",
+ "search/v0",
+ "search/v1",
+ "search/v2",
+ "search/v3",
+ })
+
+ output3, err := stubs[0].Match(ctx, []string{"base"})
+ if err != nil {
+ t.Fatalf("Match(%v) failed: %v", "base", err)
+ }
+ if !reflect.DeepEqual(envelopeV3, output2) {
+ t.Fatalf("Incorrect output: expected %v, got %v", envelopeV3, output3)
+ }
+
+ output4, err := stubs[0].Match(ctx, []string{"base"})
+ if err != nil {
+ t.Fatalf("Match(%v) failed: %v", "base", err)
+ }
+ if !reflect.DeepEqual(envelopeV3, output2) {
+ t.Fatalf("Incorrect output: expected %v, got %v", envelopeV3, output4)
+ }
+
+ _, err = stubs[0].Match(ctx, []string{"media"})
+ if verror.ErrorID(err) != verror.ErrNoExist.ID {
+ t.Fatalf("got error %v, expected %v", err, verror.ErrNoExist)
+ }
+
+ stuffEnvelopes(t, ctx, stubs, []profEnvTuple{
+ {
+ &envelopeV1,
+ []string{"base", "media"},
+ },
+ {
+ &envelopeV2,
+ []string{"base", "media"},
+ },
+ {
+ &envelopeV3,
+ []string{"base", "media"},
+ },
+ {
+ &envelopeV3,
+ []string{"base", "media"},
+ },
+ })
+
+ // Now there are four versions for all profiles so tidying
+ // will remove the older versions.
+ if err := stubs[0].TidyNow(ctx); err != nil {
+ t.Errorf("TidyNow failed: %v", err)
+ }
+
+ testGlob(t, ctx, endpoint, []string{
+ "",
+ "search",
+ "search/v2",
+ "search/v3",
+ })
+}
+
+type profEnvTuple struct {
+ e *application.Envelope
+ p []string
+}
+
+func testGlob(t *testing.T, ctx *context.T, endpoint string, expected []string) {
+ matches, _, err := testutil.GlobName(ctx, naming.JoinAddressName(endpoint, ""), "...")
+ if err != nil {
+ t.Errorf("Unexpected Glob error: %v", err)
+ }
+ if !reflect.DeepEqual(matches, expected) {
+ t.Errorf("unexpected Glob results. Got %q, want %q", matches, expected)
+ }
+}
+
+func stuffEnvelopes(t *testing.T, ctx *context.T, stubs []repository.ApplicationClientStub, pets []profEnvTuple) {
+ for i, pet := range pets {
+ if err := stubs[i].Put(ctx, pet.p, *pet.e); err != nil {
+ t.Fatalf("%d: Put(%v) failed: %v", i, pet, err)
+ }
+ }
+}
diff --git a/services/application/applicationd/only_for_test.go b/services/application/applicationd/only_for_test.go
new file mode 100644
index 0000000..0b3724e
--- /dev/null
+++ b/services/application/applicationd/only_for_test.go
@@ -0,0 +1,9 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+func init() {
+ numberOfVersionsToKeep = 2
+}
diff --git a/services/application/applicationd/service.go b/services/application/applicationd/service.go
index 2fc2a5f..e11f26d 100644
--- a/services/application/applicationd/service.go
+++ b/services/application/applicationd/service.go
@@ -5,7 +5,6 @@
package main
import (
- "fmt"
"sort"
"strings"
@@ -72,7 +71,7 @@
defer i.store.Unlock()
if version == "" {
- versions, err := i.allAppVersions(name)
+ versions, err := i.allAppVersionsForProfiles(name, profiles)
if err != nil {
return empty, err
}
@@ -187,15 +186,13 @@
return apps, nil
}
-func (i *appRepoService) allAppVersions(appName string) ([]string, error) {
- profiles, err := i.store.BindObject(naming.Join("/applications", appName)).Children()
- if err != nil {
- return nil, err
- }
+func (i *appRepoService) allAppVersionsForProfiles(appName string, profiles []string) ([]string, error) {
uniqueVersions := make(map[string]struct{})
for _, profile := range profiles {
versions, err := i.store.BindObject(naming.Join("/applications", appName, profile)).Children()
- if err != nil {
+ if verror.ErrorID(err) == verror.ErrNoExist.ID {
+ continue
+ } else if err != nil {
return nil, err
}
set.String.Union(uniqueVersions, set.String.FromSlice(versions))
@@ -203,6 +200,14 @@
return set.String.ToSlice(uniqueVersions), nil
}
+func (i *appRepoService) allAppVersions(appName string) ([]string, error) {
+ profiles, err := i.store.BindObject(naming.Join("/applications", appName)).Children()
+ if err != nil {
+ return nil, err
+ }
+ return i.allAppVersionsForProfiles(appName, profiles)
+}
+
func (i *appRepoService) GlobChildren__(ctx *context.T, _ rpc.ServerCall) (<-chan string, error) {
ctx.VI(0).Infof("%v.GlobChildren__()", i.suffix)
i.store.Lock()
@@ -334,6 +339,64 @@
return nil
}
+func (i *appRepoService) tidyRemoveVersions(call rpc.ServerCall, tname, appName, profile string, versions []string) error {
+ for _, v := range versions {
+ path := naming.Join(tname, "/applications", appName, profile, v)
+ object := i.store.BindObject(path)
+ if err := object.Remove(call); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// numberOfVersionsToKeep can be set for tests.
+var numberOfVersionsToKeep = 5
+
func (i *appRepoService) TidyNow(ctx *context.T, call rpc.ServerCall) error {
- return fmt.Errorf("method not implemented")
+ ctx.VI(2).Infof("%v.TidyNow()", i.suffix)
+ i.store.Lock()
+ defer i.store.Unlock()
+
+ tname, err := i.store.BindTransactionRoot("").CreateTransaction(call)
+ if err != nil {
+ return err
+ }
+
+ apps, err := i.allApplications()
+ if err != nil {
+ return err
+ }
+
+ for _, app := range apps {
+ profiles, err := i.store.BindObject(naming.Join("/applications", app)).Children()
+ if err != nil {
+ return err
+ }
+
+ for _, profile := range profiles {
+ versions, err := i.store.BindObject(naming.Join("/applications", app, profile)).Children()
+ if err != nil {
+ return err
+ }
+
+ lv := len(versions)
+ if lv <= numberOfVersionsToKeep {
+ continue
+ }
+
+ // Per assumption in Match, version names should ascend.
+ sort.Strings(versions)
+ versionsToRemove := versions[0 : lv-numberOfVersionsToKeep]
+ if err := i.tidyRemoveVersions(call, tname, app, profile, versionsToRemove); err != nil {
+ return err
+ }
+ }
+ }
+
+ if err := i.store.BindTransaction(tname).Commit(call); err != nil {
+ return verror.New(ErrOperationFailed, ctx)
+ }
+ return nil
+
}