-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathexperimentController.go
165 lines (152 loc) · 5.2 KB
/
experimentController.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package choices
import (
"context"
"fmt"
"log"
"github.com/Nordstrom/choices/util"
"github.com/foolusion/elwinprotos/storage"
"github.com/pkg/errors"
)
// CreateExperiment will create a new experiment and namespace based on the
// input that it receives.
func CreateExperiment(
ctx context.Context,
expcontroller experimentController,
sExperiment *storage.Experiment,
sNamespace *storage.Namespace,
nsNumSegments int,
expNumSegments int,
) (*storage.Experiment, error) {
if sExperiment == nil {
return nil, errors.New("experiment is nil")
} else if len(sExperiment.Labels) == 0 {
return nil, errors.New("experiment labels are empty")
}
exp := FromExperiment(sExperiment)
var ns *Namespace
if sNamespace == nil {
ns = newNamespace(exp.Namespace, nsNumSegments)
} else {
ns = FromNamespace(sNamespace)
}
exp.Namespace = ns.Name
// sample the namespaces segments
seg := ns.Segments.sample(expNumSegments)
exp.Segments = &segments{b: seg, len: ns.Segments.len}
if exp.Name == "" {
exp.Name = util.BasicNameGenerator.GenerateName("")
}
exp.ID = util.BasicNameGenerator.GenerateName(fmt.Sprintf("exp-%s-", exp.Name))
if err := expcontroller.SetNamespace(ctx, ns.ToNamespace(sNamespace)); err != nil {
return nil, errors.Wrap(err, "could not save namespace")
}
if err := expcontroller.SetExperiment(ctx, exp.ToExperiment(sExperiment)); err != nil {
return nil, errors.Wrap(err, "could not save experiment")
}
// TODO: do this in a better way
out := exp.ToExperiment(sExperiment)
out.DetailName = sExperiment.DetailName
return out, nil
}
// BadSegments implements an Error interface. It is used when then
// experiments claimed segments do not match the namespaces claimed
// segments.
type BadSegments struct {
NamespaceSegments *segments
*Experiment
Err error
}
func (bs *BadSegments) Error() string {
if bs.Err == nil {
return fmt.Sprintf("namespace %s segments %x don't match experiment %s segments %x", bs.Namespace, bs.NamespaceSegments, bs.ID, bs.Segments)
}
return fmt.Sprintf("namespace %s segments %x don't match experiment %s segments %x: %v", bs.Namespace, bs.NamespaceSegments, bs.ID, bs.Segments, bs.Err)
}
// NamespaceDoesNotExist is an error thrown when an experiment has
// a namespace listed that is not in storage.
type NamespaceDoesNotExist struct {
*Experiment
}
func (n *NamespaceDoesNotExist) Error() string {
return fmt.Sprintf("namespace %s does not exist", n.Namespace)
}
// ValidateNamespaces checks whether all exeriments have namespaces
// and if the segments they claimed have also been claimed from the
// namespace. If everything is OK it ValidateNamespaces will return
// nil otherwise it will return an error. If the error is
// ErrNamespaceDoesNotExists you can fix this by creating a namespace
// to match the experiment.
func ValidateNamespaces(ctx context.Context, e experimentController) error {
namespaces, err := e.AllNamespaces(ctx)
nsSet := make(map[string]*segments, len(namespaces))
for _, ns := range namespaces {
nsSet[ns.Name] = FromSegments(ns.Segments)
}
log.Println(nsSet)
experiments, err := e.AllExperiments(ctx)
if err != nil {
return errors.Wrap(err, "could not get all experiments from storage")
}
expSet := make(map[string]*segments, len(namespaces))
for _, exp := range experiments {
if s, ok := expSet[exp.Namespace]; !ok {
expSet[exp.Namespace] = FromSegments(exp.Segments)
} else {
// check for overlapping experiments
out, err := s.Claim(FromSegments(exp.Segments))
if err != nil {
return &BadSegments{
NamespaceSegments: s,
Experiment: FromExperiment(exp),
Err: err,
}
}
s.b = out
}
// check all namespace segments are claimed
if s, ok := nsSet[exp.Namespace]; !ok {
return &NamespaceDoesNotExist{FromExperiment(exp)}
} else if !s.contains(FromSegments(exp.Segments)) {
return &BadSegments{NamespaceSegments: s, Experiment: FromExperiment(exp)}
}
}
return nil
}
// AutoFix will attempt to add namespaces for experiments
// that are missing a namespace. In the future we could potentially
// add more autofixes here.
func AutoFix(ctx context.Context, e experimentController) error {
err := ValidateNamespaces(ctx, e)
if err != nil {
switch err := err.(type) {
case *BadSegments:
return errors.Wrap(err, "could not fix bad segments")
case *NamespaceDoesNotExist:
if err := e.SetNamespace(ctx, &storage.Namespace{
Name: err.Namespace,
NumSegments: int64(err.Segments.len),
Segments: err.Segments.ToSegments(nil),
}); err != nil {
return errors.Wrap(err, "could not add namespace")
}
return AutoFix(ctx, e)
default:
return err
}
}
return nil
}
var (
// ErrNotFound is the error that should be returnned when calls to
// Namespace and Experiment fail because they don't exist in
// storage.
ErrNotFound = errors.New("not found")
)
type experimentController interface {
SetNamespace(context.Context, *storage.Namespace) error
Namespace(context.Context, string) (*storage.Namespace, error)
AllNamespaces(context.Context) ([]*storage.Namespace, error)
SetExperiment(context.Context, *storage.Experiment) error
Experiment(context.Context, string) (*storage.Experiment, error)
AllExperiments(context.Context) ([]*storage.Experiment, error)
}