Merge lp:~jameinel/juju-core/api-rpc-reflect-version into lp:~go-bot/juju-core/trunk
- api-rpc-reflect-version
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~jameinel/juju-core/api-rpc-reflect-version |
Merge into: | lp:~go-bot/juju-core/trunk |
Prerequisite: | lp:~jameinel/juju-core/typed-registry |
Diff against target: |
1306 lines (+645/-135) 12 files modified
rpc/jsoncodec/codec.go (+7/-3) rpc/reflect_test.go (+47/-19) rpc/registry/package_test.go (+14/-0) rpc/registry/registry.go (+71/-0) rpc/registry/registry_test.go (+96/-0) rpc/rpc_test.go (+266/-47) rpc/rpcreflect/type.go (+13/-6) rpc/rpcreflect/value.go (+44/-19) rpc/server.go (+60/-14) state/apiserver/root_test.go (+20/-18) utils/registry/registry.go (+6/-8) utils/registry/registry_test.go (+1/-1) |
To merge this branch: | bzr merge lp:~jameinel/juju-core/api-rpc-reflect-version |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+219653@code.launchpad.net |
Commit message
Description of the change
rpc: support a Version field
This adds support for a Version field in RPC requests, and changes the
MethodCaller interface for how and when it looks up objects.
Most of the MethodCaller changes were already reviewed in:
https:/
There are only a couple of tweaks because the lookup is now done with a
real Version instead of overriding the "id" field.
This also uses the registry.
shows one possible way that we can do a Registry with versioned lookups.
Note that the next steps don't actually make use of rpc/registry/* so
I'm willing to pull it out, or put it in as a test-suite only thing.
Alternatively it might be something tha could fit into
documentation/
This does disable one test that assumed you could statically iterate
over all the types and exposed methods. I introduce a similar function
later on once we have the factories registered.
One thing that was brought up before about always unfolding Interface(),
I didn't do that yet, but I still could if we feel it would be more
straightforward.
William Reade (fwereade) wrote : | # |
LGTM
https:/
File rpc/reflect_test.go (right):
https:/
rpc/reflect_
I still think it is :).
https:/
File rpc/registry/
https:/
rpc/registry/
pointer Elem trick
Tell me about it.
https:/
rpc/registry/
/me feels quietly smug
https:/
rpc/registry/
into CallNotImplemen
I *think* this is good as it is.
https:/
File rpc/registry/
https:/
rpc/registry/
`unknown object type "facade"`)
d
https:/
File rpc/rpc_test.go (right):
https:/
rpc/rpc_
I'm really appreciating the comments, but I'd love to see trailing full
stops.
- 2722. By John A Meinel
-
address review comments.
John A Meinel (jameinel) wrote : | # |
Please take a look.
https:/
File rpc/reflect_test.go (right):
https:/
rpc/reflect_
On 2014/05/16 09:05:27, fwereade wrote:
> I still think it is :).
I wasn't sure because the test used to assert that rtype.ObjType matched
rpcreflect.
world matched the object type you get by looking it up directly.)
Since we just asserted that the actual reflect.Type is identical, it
seemed a little strange to assert that ObjTypeOf(T), gc.Equals,
ObjTypeOf(T).
But I'm willing to leave it in.
Comment removed.
https:/
File rpc/registry/
https:/
rpc/registry/
On 2014/05/16 09:05:28, fwereade wrote:
> /me feels quietly smug
Having seen quite a bit of code with long params jumping around we do
have quite a few places that also do it Nate's way (possibly written by
him):
func (r *RootFromRegistry) MethodCaller(
rootMethodName string, version int, objId, objMethodName string,
) (rcreflect.
I think I still prefer the consistency between args in and args out.
The only case I might switch is when the arg list is still too long
because of package names, etc. I haven't quite decided between:
func (r *RootFromRegistry) MethodCaller(
rootMethodName string, version int,
objId string, objMethodName string,
) (
rpcreflect.
) {
and
func (r *RootFromRegistry) MethodCaller(
rootMethodName string,
version int,
objId string,
) (
rpcreflect.
) {
(If you *have* to wrap, should you go down to one-per-line).
Anyway, I did like your suggestion, and certainly I asked looking for a
nice way to go. :)
https:/
rpc/registry/
into CallNotImplemen
On 2014/05/16 09:05:28, fwereade wrote:
> I *think* this is good as it is.
comment removed. I could see a case for mapping obvious types, like
potentially a NotFound into a CallNotImplemented.
https:/
File rpc/registry/
https:/
rpc/registry/
`unknown object type "facade"`)
On 2014/05/16 09:05:28, fwereade wrote:
> d
Done.
https:/
File rpc/rpc_test.go (right):
https:/
rpc/rpc_
On 2014/05/16 09:05:28, fwereade wrote:
> I'm really appreciating the comments, but I'd love to see trailing
full stops.
I went through and audited the change so far, adding full stops where
appropriate (hopefully).
- 2723. By John A Meinel
-
add some fullstops for comments in another branch
- 2724. By John A Meinel
-
merge typed-registry, bringing it trunk and switch to github.
com/juju/ errors
Dimiter Naydenov (dimitern) wrote : | # |
Few comments for clarification, otherwise looks great!
https:/
File rpc/registry/
https:/
rpc/registry/
string, version int, f Factory) {
Doc comment? Also, is panicking here a good idea?
https:/
rpc/registry/
from a string into an int)
This comment is a bit confusing - we have version int and objId string -
is only the latter used for versioning?
https:/
File rpc/server.go (right):
https:/
rpc/server.go:344: // version, id, methodName) triplet.
It's a quadruplet now actually :)
- 2726. By John A Meinel
-
apply feedback from Dimiter's review.
John A Meinel (jameinel) wrote : | # |
Please take a look.
https:/
File rpc/registry/
https:/
rpc/registry/
string, version int, f Factory) {
On 2014/05/19 09:33:06, dimitern wrote:
> Doc comment? Also, is panicking here a good idea?
So this is example code, but panic() in stuff that happens during init()
is fine with me. Because then it panics early and not just end up with
things that aren't actually registered.
https:/
rpc/registry/
from a string into an int)
On 2014/05/19 09:33:06, dimitern wrote:
> This comment is a bit confusing - we have version int and objId string
- is only
> the latter used for versioning?
Just out of date docstrings. It was added when we were using objId for
versioning, but now we have the actual Version field. I updated the
comments.
https:/
File rpc/server.go (right):
https:/
rpc/server.go:344: // version, id, methodName) triplet.
On 2014/05/19 09:33:06, dimitern wrote:
> It's a quadruplet now actually :)
Done.
Unmerged revisions
- 2726. By John A Meinel
-
apply feedback from Dimiter's review.
- 2724. By John A Meinel
-
merge typed-registry, bringing it trunk and switch to github.
com/juju/ errors - 2723. By John A Meinel
-
add some fullstops for comments in another branch
- 2722. By John A Meinel
-
address review comments.
- 2721. By John A Meinel
-
merge in rpc-version.
This gives us all of the functionality for passing Version in the RPC,
and changes the MethodCaller signature so that it can take the version
and the objId.
Preview Diff
1 | === modified file 'rpc/jsoncodec/codec.go' |
2 | --- rpc/jsoncodec/codec.go 2014-03-05 19:41:34 +0000 |
3 | +++ rpc/jsoncodec/codec.go 2014-05-22 11:41:28 +0000 |
4 | @@ -64,6 +64,7 @@ |
5 | type inMsg struct { |
6 | RequestId uint64 |
7 | Type string |
8 | + Version int `json:",omitempty"` |
9 | Id string |
10 | Request string |
11 | Params json.RawMessage |
12 | @@ -76,6 +77,7 @@ |
13 | type outMsg struct { |
14 | RequestId uint64 |
15 | Type string `json:",omitempty"` |
16 | + Version int `json:",omitempty"` |
17 | Id string `json:",omitempty"` |
18 | Request string `json:",omitempty"` |
19 | Params interface{} `json:",omitempty"` |
20 | @@ -122,9 +124,10 @@ |
21 | } |
22 | hdr.RequestId = c.msg.RequestId |
23 | hdr.Request = rpc.Request{ |
24 | - Type: c.msg.Type, |
25 | - Id: c.msg.Id, |
26 | - Action: c.msg.Request, |
27 | + Type: c.msg.Type, |
28 | + Version: c.msg.Version, |
29 | + Id: c.msg.Id, |
30 | + Action: c.msg.Request, |
31 | } |
32 | hdr.Error = c.msg.Error |
33 | hdr.ErrorCode = c.msg.ErrorCode |
34 | @@ -184,6 +187,7 @@ |
35 | m.RequestId = hdr.RequestId |
36 | m.Type = hdr.Request.Type |
37 | m.Id = hdr.Request.Id |
38 | + m.Version = hdr.Request.Version |
39 | m.Request = hdr.Request.Action |
40 | m.Error = hdr.Error |
41 | m.ErrorCode = hdr.ErrorCode |
42 | |
43 | === modified file 'rpc/reflect_test.go' |
44 | --- rpc/reflect_test.go 2014-05-20 04:27:02 +0000 |
45 | +++ rpc/reflect_test.go 2014-05-22 11:41:28 +0000 |
46 | @@ -11,6 +11,7 @@ |
47 | |
48 | "launchpad.net/juju-core/rpc/rpcreflect" |
49 | "launchpad.net/juju-core/testing" |
50 | + "launchpad.net/juju-core/utils/set" |
51 | ) |
52 | |
53 | // We test rpcreflect in this package, so that the |
54 | @@ -23,28 +24,38 @@ |
55 | var _ = gc.Suite(&reflectSuite{}) |
56 | |
57 | func (*reflectSuite) TestTypeOf(c *gc.C) { |
58 | - rtype := rpcreflect.TypeOf(reflect.TypeOf(&Root{})) |
59 | + root := SimpleRoot() |
60 | + rtype := rpcreflect.TypeOf(reflect.TypeOf(root)) |
61 | c.Assert(rtype.DiscardedMethods(), gc.DeepEquals, []string{ |
62 | "Discard1", |
63 | "Discard2", |
64 | "Discard3", |
65 | }) |
66 | - expect := map[string]reflect.Type{ |
67 | - "CallbackMethods": reflect.TypeOf(&CallbackMethods{}), |
68 | - "ChangeAPIMethods": reflect.TypeOf(&ChangeAPIMethods{}), |
69 | - "DelayedMethods": reflect.TypeOf(&DelayedMethods{}), |
70 | - "ErrorMethods": reflect.TypeOf(&ErrorMethods{}), |
71 | - "InterfaceMethods": reflect.TypeOf((*InterfaceMethods)(nil)).Elem(), |
72 | - "SimpleMethods": reflect.TypeOf(&SimpleMethods{}), |
73 | - } |
74 | - c.Assert(rtype.MethodNames(), gc.HasLen, len(expect)) |
75 | - for name, expectGoType := range expect { |
76 | - m, err := rtype.Method(name) |
77 | + expect := []struct { |
78 | + Name string |
79 | + Id string |
80 | + GoType reflect.Type |
81 | + }{ |
82 | + {"CallbackMethods", "", reflect.TypeOf(&CallbackMethods{})}, |
83 | + {"ChangeAPIMethods", "", reflect.TypeOf(&ChangeAPIMethods{})}, |
84 | + {"DelayedMethods", "", reflect.TypeOf(&DelayedMethods{})}, |
85 | + {"ErrorMethods", "", reflect.TypeOf(&ErrorMethods{})}, |
86 | + {"InterfaceMethods", "a99", reflect.TypeOf((*InterfaceMethods)(nil)).Elem()}, |
87 | + {"SimpleMethods", "a99", reflect.TypeOf(&SimpleMethods{})}, |
88 | + } |
89 | + var uniqueNames set.Strings |
90 | + for _, exp := range expect { |
91 | + uniqueNames.Add(exp.Name) |
92 | + } |
93 | + c.Assert(rtype.MethodNames(), gc.DeepEquals, uniqueNames.SortedValues()) |
94 | + for _, exp := range expect { |
95 | + m, err := rtype.Method(exp.Name) |
96 | c.Assert(err, gc.IsNil) |
97 | c.Assert(m, gc.NotNil) |
98 | c.Assert(m.Call, gc.NotNil) |
99 | - c.Assert(m.ObjType, gc.Equals, rpcreflect.ObjTypeOf(expectGoType)) |
100 | - c.Assert(m.ObjType.GoType(), gc.Equals, expectGoType) |
101 | + obj, err := m.Call(reflect.ValueOf(root), exp.Id) |
102 | + c.Check(obj.Type(), gc.Equals, exp.GoType) |
103 | + c.Check(rpcreflect.ObjTypeOf(obj.Type()), gc.Equals, rpcreflect.ObjTypeOf(exp.GoType)) |
104 | } |
105 | m, err := rtype.Method("not found") |
106 | c.Assert(err, gc.Equals, rpcreflect.ErrMethodNotFound) |
107 | @@ -97,7 +108,7 @@ |
108 | func (*reflectSuite) TestValueOf(c *gc.C) { |
109 | v := rpcreflect.ValueOf(reflect.ValueOf(nil)) |
110 | c.Check(v.IsValid(), jc.IsFalse) |
111 | - c.Check(func() { v.MethodCaller("foo", "bar") }, gc.PanicMatches, "MethodCaller called on invalid Value") |
112 | + c.Check(func() { v.MethodCaller("foo", 0, "bar", "") }, gc.PanicMatches, "MethodCaller called on invalid Value") |
113 | |
114 | root := &Root{} |
115 | v = rpcreflect.ValueOf(reflect.ValueOf(root)) |
116 | @@ -115,22 +126,39 @@ |
117 | root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
118 | v := rpcreflect.ValueOf(reflect.ValueOf(root)) |
119 | |
120 | - m, err := v.MethodCaller("foo", "bar") |
121 | + m, err := v.MethodCaller("foo", 0, "a99", "bar") |
122 | c.Assert(err, gc.ErrorMatches, `unknown object type "foo"`) |
123 | c.Assert(err, gc.FitsTypeOf, (*rpcreflect.CallNotImplementedError)(nil)) |
124 | c.Assert(m, gc.DeepEquals, rpcreflect.MethodCaller{}) |
125 | |
126 | - m, err = v.MethodCaller("SimpleMethods", "bar") |
127 | + m, err = v.MethodCaller("SimpleMethods", 0, "a99", "bar") |
128 | c.Assert(err, gc.ErrorMatches, "no such request - method SimpleMethods.bar is not implemented") |
129 | c.Assert(err, gc.FitsTypeOf, (*rpcreflect.CallNotImplementedError)(nil)) |
130 | c.Assert(m, gc.DeepEquals, rpcreflect.MethodCaller{}) |
131 | |
132 | - m, err = v.MethodCaller("SimpleMethods", "Call1r1e") |
133 | + m, err = v.MethodCaller("SimpleMethods", 0, "a99", "Call1r1e") |
134 | c.Assert(err, gc.IsNil) |
135 | c.Assert(m.ParamsType, gc.Equals, reflect.TypeOf(stringVal{})) |
136 | c.Assert(m.ResultType, gc.Equals, reflect.TypeOf(stringVal{})) |
137 | |
138 | - ret, err := m.Call("a99", reflect.ValueOf(stringVal{"foo"})) |
139 | + ret, err := m.Call(reflect.ValueOf(stringVal{"foo"})) |
140 | c.Assert(err, gc.IsNil) |
141 | c.Assert(ret.Interface(), gc.Equals, stringVal{"Call1r1e ret"}) |
142 | } |
143 | + |
144 | +func (*reflectSuite) TestMethodCallerRefusesVersionsNot0(c *gc.C) { |
145 | + root := &Root{ |
146 | + simple: make(map[string]*SimpleMethods), |
147 | + } |
148 | + root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
149 | + v := rpcreflect.ValueOf(reflect.ValueOf(root)) |
150 | + |
151 | + m, err := v.MethodCaller("SimpleMethods", 0, "a99", "Call1r1e") |
152 | + c.Assert(err, gc.IsNil) |
153 | + c.Assert(m.ParamsType, gc.Equals, reflect.TypeOf(stringVal{})) |
154 | + c.Assert(m.ResultType, gc.Equals, reflect.TypeOf(stringVal{})) |
155 | + |
156 | + m, err = v.MethodCaller("SimpleMethods", 1, "a99", "Call1r1e") |
157 | + c.Assert(err, gc.FitsTypeOf, (*rpcreflect.CallNotImplementedError)(nil)) |
158 | + c.Assert(err, gc.ErrorMatches, `unknown version \(1\) of interface "SimpleMethods"`) |
159 | +} |
160 | |
161 | === added directory 'rpc/registry' |
162 | === added file 'rpc/registry/package_test.go' |
163 | --- rpc/registry/package_test.go 1970-01-01 00:00:00 +0000 |
164 | +++ rpc/registry/package_test.go 2014-05-22 11:41:28 +0000 |
165 | @@ -0,0 +1,14 @@ |
166 | +// Copyright 2014 Canonical Ltd. |
167 | +// Licensed under the AGPLv3, see LICENCE file for details. |
168 | + |
169 | +package registry_test |
170 | + |
171 | +import ( |
172 | + stdtesting "testing" |
173 | + |
174 | + gc "launchpad.net/gocheck" |
175 | +) |
176 | + |
177 | +func TestAll(t *stdtesting.T) { |
178 | + gc.TestingT(t) |
179 | +} |
180 | |
181 | === added file 'rpc/registry/registry.go' |
182 | --- rpc/registry/registry.go 1970-01-01 00:00:00 +0000 |
183 | +++ rpc/registry/registry.go 2014-05-22 11:41:28 +0000 |
184 | @@ -0,0 +1,71 @@ |
185 | +// Copyright 2014 Canonical Ltd. |
186 | +// Licensed under the AGPLv3, see LICENCE file for details. |
187 | + |
188 | +package registry |
189 | + |
190 | +import ( |
191 | + "reflect" |
192 | + |
193 | + "github.com/juju/errors" |
194 | + |
195 | + "launchpad.net/juju-core/rpc/rpcreflect" |
196 | + "launchpad.net/juju-core/utils/registry" |
197 | +) |
198 | + |
199 | +// RootFromRegistry is a map from (facade name, version) to a factory function that |
200 | +// creates an object manifesting that facade. |
201 | +type RootFromRegistry struct { |
202 | + registry *registry.TypedNameVersion |
203 | +} |
204 | + |
205 | +// Register records a factory that will be returned from Get() when passed the |
206 | +// specific (name, version) pair. |
207 | +func (r *RootFromRegistry) Register(name string, version int, f Factory) { |
208 | + err := r.registry.Register(name, version, f) |
209 | + if err != nil { |
210 | + panic(err) |
211 | + } |
212 | +} |
213 | + |
214 | +// NewRegistry creates a place to register facades. |
215 | +func NewRegistry() *RootFromRegistry { |
216 | + // Note: 'type' is not a first class object like functions are, so we |
217 | + // can't just pass Factory. Instead we create an pointer to a (nil) |
218 | + // instance of it, and then use Elem to get the correct reflect.Type. |
219 | + factoryType := reflect.TypeOf((*Factory)(nil)).Elem() |
220 | + return &RootFromRegistry{ |
221 | + registry: registry.NewTypedNameVersion(factoryType), |
222 | + } |
223 | +} |
224 | + |
225 | +type Factory func() (interface{}, error) |
226 | + |
227 | +// MethodCaller is used by rpc.Server to place a call to a given Method on a |
228 | +// Facade at the specific version. |
229 | +// The parameter names match that of the rpcreflect.Value implementation, but |
230 | +// might be better named: |
231 | +// rootMethodName: facadeName |
232 | +// version: facadeVersion (will be translated from a string into an int) |
233 | +// objId: Id for the a concrete version of the facade (used for watchers) |
234 | +// objMethodName: methodName |
235 | +func (r *RootFromRegistry) MethodCaller( |
236 | + rootMethodName string, version int, objId, objMethodName string, |
237 | +) ( |
238 | + rpcreflect.MethodCaller, error, |
239 | +) { |
240 | + factory, err := r.registry.Get(rootMethodName, version) |
241 | + if errors.IsNotFound(err) { |
242 | + return rpcreflect.MethodCaller{}, &rpcreflect.CallNotImplementedError{ |
243 | + RootMethod: rootMethodName, |
244 | + Version: version, |
245 | + } |
246 | + } else if err != nil { |
247 | + return rpcreflect.MethodCaller{}, err |
248 | + } |
249 | + facade, err := factory.(Factory)() |
250 | + if err != nil { |
251 | + return rpcreflect.MethodCaller{}, err |
252 | + } |
253 | + return rpcreflect.MethodCallerFromValue( |
254 | + reflect.ValueOf(facade), rootMethodName, version, objMethodName) |
255 | +} |
256 | |
257 | === added file 'rpc/registry/registry_test.go' |
258 | --- rpc/registry/registry_test.go 1970-01-01 00:00:00 +0000 |
259 | +++ rpc/registry/registry_test.go 2014-05-22 11:41:28 +0000 |
260 | @@ -0,0 +1,96 @@ |
261 | +// Copyright 2014 Canonical Ltd. |
262 | +// Licensed under the AGPLv3, see LICENCE file for details. |
263 | + |
264 | +package registry_test |
265 | + |
266 | +import ( |
267 | + "fmt" |
268 | + "reflect" |
269 | + |
270 | + jc "github.com/juju/testing/checkers" |
271 | + gc "launchpad.net/gocheck" |
272 | + |
273 | + "launchpad.net/juju-core/rpc/registry" |
274 | + "launchpad.net/juju-core/rpc/rpcreflect" |
275 | + "launchpad.net/juju-core/testing/testbase" |
276 | +) |
277 | + |
278 | +type registrySuite struct { |
279 | + testbase.LoggingSuite |
280 | +} |
281 | + |
282 | +var _ = gc.Suite(®istrySuite{}) |
283 | + |
284 | +func nilFactory() (interface{}, error) { |
285 | + return nil, nil |
286 | +} |
287 | + |
288 | +type testFacade struct { |
289 | + version string |
290 | + called bool |
291 | +} |
292 | + |
293 | +type stringVal struct { |
294 | + value string |
295 | +} |
296 | + |
297 | +func (t *testFacade) TestMethod() stringVal { |
298 | + t.called = true |
299 | + return stringVal{"called " + t.version} |
300 | +} |
301 | + |
302 | +func (s *registrySuite) TestMethodCallerNoEntries(c *gc.C) { |
303 | + r := registry.NewRegistry() |
304 | + _, err := r.MethodCaller("facade", 1, "", "method") |
305 | + c.Check(err, gc.FitsTypeOf, (*rpcreflect.CallNotImplementedError)(nil)) |
306 | + c.Check(err, gc.ErrorMatches, `unknown version \(1\) of interface "facade"`) |
307 | +} |
308 | + |
309 | +func (s *registrySuite) TestMethodCallerUnknownVersion(c *gc.C) { |
310 | + r := registry.NewRegistry() |
311 | + r.Register("name", 0, nilFactory) |
312 | + _, err := r.MethodCaller("name", 1, "", "method") |
313 | + c.Check(err, gc.FitsTypeOf, (*rpcreflect.CallNotImplementedError)(nil)) |
314 | + c.Check(err, gc.ErrorMatches, `unknown version \(1\) of interface "name"`) |
315 | +} |
316 | + |
317 | +func (s *registrySuite) TestMethodCaller(c *gc.C) { |
318 | + r := registry.NewRegistry() |
319 | + testF := &testFacade{version: "v0"} |
320 | + singletonFactory := func() (interface{}, error) { return testF, nil } |
321 | + r.Register("test", 0, singletonFactory) |
322 | + mc, err := r.MethodCaller("test", 0, "", "TestMethod") |
323 | + c.Assert(err, gc.IsNil) |
324 | + result, err := mc.Call(reflect.ValueOf(stringVal{""})) |
325 | + c.Assert(err, gc.IsNil) |
326 | + c.Check(result.Interface().(stringVal).value, gc.Equals, "called v0") |
327 | + c.Check(testF.called, jc.IsTrue) |
328 | +} |
329 | + |
330 | +func (s *registrySuite) TestMethodCallerFactoryError(c *gc.C) { |
331 | + r := registry.NewRegistry() |
332 | + r.Register("test", 0, func() (interface{}, error) { |
333 | + return nil, fmt.Errorf("internal factory failure") |
334 | + }) |
335 | + _, err := r.MethodCaller("test", 0, "", "TestMethod") |
336 | + c.Check(err, gc.ErrorMatches, `internal factory failure`) |
337 | +} |
338 | + |
339 | +func (s *registrySuite) TestMethodCallerMultipleVersions(c *gc.C) { |
340 | + r := registry.NewRegistry() |
341 | + testV0 := &testFacade{version: "v0"} |
342 | + testV1 := &testFacade{version: "v1"} |
343 | + r.Register("test", 0, func() (interface{}, error) { |
344 | + return testV0, nil |
345 | + }) |
346 | + r.Register("test", 1, func() (interface{}, error) { |
347 | + return testV1, nil |
348 | + }) |
349 | + mc, err := r.MethodCaller("test", 1, "", "TestMethod") |
350 | + c.Assert(err, gc.IsNil) |
351 | + result, err := mc.Call(reflect.ValueOf(stringVal{""})) |
352 | + c.Assert(err, gc.IsNil) |
353 | + c.Check(result.Interface().(stringVal).value, gc.Equals, "called v1") |
354 | + c.Check(testV0.called, jc.IsFalse) |
355 | + c.Check(testV1.called, jc.IsTrue) |
356 | +} |
357 | |
358 | === modified file 'rpc/rpc_test.go' |
359 | --- rpc/rpc_test.go 2014-05-20 04:27:02 +0000 |
360 | +++ rpc/rpc_test.go 2014-05-22 11:41:28 +0000 |
361 | @@ -18,6 +18,7 @@ |
362 | |
363 | "launchpad.net/juju-core/rpc" |
364 | "launchpad.net/juju-core/rpc/jsoncodec" |
365 | + "launchpad.net/juju-core/rpc/rpcreflect" |
366 | "launchpad.net/juju-core/testing" |
367 | ) |
368 | |
369 | @@ -230,7 +231,7 @@ |
370 | return int64val{1}, nil |
371 | } |
372 | var r int64val |
373 | - err := a.root.conn.Call(rpc.Request{"CallbackMethods", "", "Factorial"}, int64val{x.I - 1}, &r) |
374 | + err := a.root.conn.Call(rpc.Request{"CallbackMethods", 0, "", "Factorial"}, int64val{x.I - 1}, &r) |
375 | if err != nil { |
376 | return int64val{}, err |
377 | } |
378 | @@ -257,11 +258,80 @@ |
379 | return stringVal{"new method result"} |
380 | } |
381 | |
382 | +type VariableMethods1 struct { |
383 | + sm *SimpleMethods |
384 | +} |
385 | + |
386 | +func (vm *VariableMethods1) Call0r1() stringVal { |
387 | + return vm.sm.Call0r1() |
388 | +} |
389 | + |
390 | +type VariableMethods2 struct { |
391 | + sm *SimpleMethods |
392 | +} |
393 | + |
394 | +func (vm *VariableMethods2) Call1r1(s stringVal) stringVal { |
395 | + return vm.sm.Call1r1(s) |
396 | +} |
397 | + |
398 | +type RestrictedMethods struct { |
399 | + InterfaceMethods |
400 | +} |
401 | + |
402 | +type CustomCaller struct { |
403 | + root *Root |
404 | +} |
405 | + |
406 | +func (cc *CustomCaller) MethodCaller( |
407 | + rootMethodName string, version int, objId, objMethodName string, |
408 | +) ( |
409 | + rpcreflect.MethodCaller, error, |
410 | +) { |
411 | + logger.Infof("got to MethodCaller: %q %d %q %q\n", |
412 | + rootMethodName, version, objId, objMethodName) |
413 | + if rootMethodName != "MultiVersion" { |
414 | + return rpcreflect.MethodCaller{}, &rpcreflect.CallNotImplementedError{ |
415 | + RootMethod: rootMethodName, |
416 | + } |
417 | + } |
418 | + sm, err := cc.root.SimpleMethods(objId) |
419 | + if err != nil { |
420 | + return rpcreflect.MethodCaller{}, err |
421 | + } |
422 | + var facade interface{} |
423 | + switch version { |
424 | + case 0: |
425 | + facade = &VariableMethods1{sm} |
426 | + case 1: |
427 | + facade = &VariableMethods2{sm} |
428 | + case 2: |
429 | + var interfaced InterfaceMethods |
430 | + interfaced = sm |
431 | + facade = interfaced |
432 | + case 3: |
433 | + var restricted RestrictedMethods |
434 | + restricted.InterfaceMethods = sm |
435 | + facade = restricted |
436 | + default: |
437 | + return rpcreflect.MethodCaller{}, &rpcreflect.CallNotImplementedError{ |
438 | + RootMethod: rootMethodName, |
439 | + Version: version, |
440 | + } |
441 | + } |
442 | + return rpcreflect.MethodCallerFromValue( |
443 | + reflect.ValueOf(facade), rootMethodName, version, objMethodName) |
444 | +} |
445 | + |
446 | +func SimpleRoot() *Root { |
447 | + root := &Root{ |
448 | + simple: make(map[string]*SimpleMethods), |
449 | + } |
450 | + root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
451 | + return root |
452 | +} |
453 | + |
454 | func (*rpcSuite) TestRPC(c *gc.C) { |
455 | - root := &Root{ |
456 | - simple: make(map[string]*SimpleMethods), |
457 | - } |
458 | - root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
459 | + root := SimpleRoot() |
460 | client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
461 | defer closeClient(c, client, srvDone) |
462 | for narg := 0; narg < 2; narg++ { |
463 | @@ -308,11 +378,11 @@ |
464 | serverNotifier *notifier |
465 | |
466 | // entry holds the top-level type that will be invoked |
467 | - // (e.g. "SimpleMethods") |
468 | + // (e.g. "SimpleMethods"). |
469 | entry string |
470 | |
471 | // narg holds the number of arguments accepted by the |
472 | - // call (0 or 1) |
473 | + // call (0 or 1). |
474 | narg int |
475 | |
476 | // nret holds the number of values returned by the |
477 | @@ -324,14 +394,18 @@ |
478 | |
479 | // testErr specifies whether the call should be made to return an error. |
480 | testErr bool |
481 | + |
482 | + // version specifies what version of the interface to call, defaults to 0. |
483 | + version int |
484 | } |
485 | |
486 | // request returns the RPC request for the test call. |
487 | func (p testCallParams) request() rpc.Request { |
488 | return rpc.Request{ |
489 | - Type: p.entry, |
490 | - Id: "a99", |
491 | - Action: callName(p.narg, p.nret, p.retErr), |
492 | + Type: p.entry, |
493 | + Version: p.version, |
494 | + Id: "a99", |
495 | + Action: callName(p.narg, p.nret, p.retErr), |
496 | } |
497 | } |
498 | |
499 | @@ -356,7 +430,10 @@ |
500 | }) |
501 | c.Assert(r, gc.Equals, stringVal{}) |
502 | case p.nret > 0: |
503 | - c.Assert(r, gc.Equals, stringVal{p.request().Action + " ret"}) |
504 | + c.Check(r, gc.Equals, stringVal{p.request().Action + " ret"}) |
505 | + } |
506 | + if !p.testErr { |
507 | + c.Check(err, gc.IsNil) |
508 | } |
509 | |
510 | // Check that the call was actually made, the right |
511 | @@ -466,10 +543,7 @@ |
512 | } |
513 | |
514 | func (*rpcSuite) TestInterfaceMethods(c *gc.C) { |
515 | - root := &Root{ |
516 | - simple: make(map[string]*SimpleMethods), |
517 | - } |
518 | - root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
519 | + root := SimpleRoot() |
520 | client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
521 | defer closeClient(c, client, srvDone) |
522 | p := testCallParams{ |
523 | @@ -486,6 +560,138 @@ |
524 | root.testCall(c, p) |
525 | p.testErr = true |
526 | root.testCall(c, p) |
527 | + // Call0r0 is defined on the underlying SimpleMethods, but is not |
528 | + // exposed at the InterfaceMethods level, so this call should fail with |
529 | + // CodeNotImplemented. |
530 | + var r stringVal |
531 | + err := client.Call(rpc.Request{"InterfaceMethods", 0, "a99", "Call0r0"}, stringVal{"arg"}, &r) |
532 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
533 | + Message: "no such request - method InterfaceMethods.Call0r0 is not implemented", |
534 | + Code: rpc.CodeNotImplemented, |
535 | + }) |
536 | +} |
537 | + |
538 | +func (*rpcSuite) TestCustomCallerV0(c *gc.C) { |
539 | + root := &CustomCaller{SimpleRoot()} |
540 | + client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
541 | + defer closeClient(c, client, srvDone) |
542 | + // V0 of MultiVersion implements only VariableMethods1.Call0r1. |
543 | + p := testCallParams{ |
544 | + client: client, |
545 | + clientNotifier: clientNotifier, |
546 | + serverNotifier: serverNotifier, |
547 | + entry: "MultiVersion", |
548 | + version: 0, |
549 | + narg: 0, |
550 | + nret: 1, |
551 | + retErr: false, |
552 | + testErr: false, |
553 | + } |
554 | + |
555 | + root.root.testCall(c, p) |
556 | + // Call1r1 is exposed in version 1, but not in version 0. |
557 | + var r stringVal |
558 | + err := client.Call(rpc.Request{"MultiVersion", 0, "a99", "Call1r1"}, stringVal{"arg"}, &r) |
559 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
560 | + Message: "no such request - method MultiVersion.Call1r1 is not implemented", |
561 | + Code: rpc.CodeNotImplemented, |
562 | + }) |
563 | +} |
564 | + |
565 | +func (*rpcSuite) TestCustomCallerV1(c *gc.C) { |
566 | + root := &CustomCaller{SimpleRoot()} |
567 | + client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
568 | + defer closeClient(c, client, srvDone) |
569 | + // V1 of MultiVersion implements only VariableMethods2.Call1r1. |
570 | + p := testCallParams{ |
571 | + client: client, |
572 | + clientNotifier: clientNotifier, |
573 | + serverNotifier: serverNotifier, |
574 | + entry: "MultiVersion", |
575 | + version: 1, |
576 | + narg: 1, |
577 | + nret: 1, |
578 | + retErr: false, |
579 | + testErr: false, |
580 | + } |
581 | + |
582 | + root.root.testCall(c, p) |
583 | + // Call0r1 is exposed in version 0, but not in version 1. |
584 | + var r stringVal |
585 | + err := client.Call(rpc.Request{"MultiVersion", 1, "a99", "Call0r1"}, nil, &r) |
586 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
587 | + Message: "no such request - method MultiVersion(1).Call0r1 is not implemented", |
588 | + Code: rpc.CodeNotImplemented, |
589 | + }) |
590 | +} |
591 | + |
592 | +func (*rpcSuite) TestCustomCallerV2(c *gc.C) { |
593 | + root := &CustomCaller{SimpleRoot()} |
594 | + client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
595 | + defer closeClient(c, client, srvDone) |
596 | + p := testCallParams{ |
597 | + client: client, |
598 | + clientNotifier: clientNotifier, |
599 | + serverNotifier: serverNotifier, |
600 | + entry: "MultiVersion", |
601 | + version: 2, |
602 | + narg: 1, |
603 | + nret: 1, |
604 | + retErr: true, |
605 | + testErr: false, |
606 | + } |
607 | + |
608 | + root.root.testCall(c, p) |
609 | + // Because of interface unwrapping, someone might have thought |
610 | + // "interface" would only expose the methods on InterfaceMethods |
611 | + // (Call1r1e), but it actually exposes all of them. |
612 | + p.narg = 0 |
613 | + root.root.testCall(c, p) |
614 | + var r stringVal |
615 | + err := client.Call(rpc.Request{"MultiVersion", 2, "a99", "Call0r1e"}, nil, &r) |
616 | + c.Assert(err, gc.IsNil) |
617 | + c.Check(r.Val, gc.Equals, "Call0r1e ret") |
618 | +} |
619 | + |
620 | +func (*rpcSuite) TestCustomCallerV3(c *gc.C) { |
621 | + root := &CustomCaller{SimpleRoot()} |
622 | + client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
623 | + defer closeClient(c, client, srvDone) |
624 | + p := testCallParams{ |
625 | + client: client, |
626 | + clientNotifier: clientNotifier, |
627 | + serverNotifier: serverNotifier, |
628 | + entry: "MultiVersion", |
629 | + version: 3, |
630 | + narg: 1, |
631 | + nret: 1, |
632 | + retErr: true, |
633 | + testErr: false, |
634 | + } |
635 | + |
636 | + root.root.testCall(c, p) |
637 | + // By embedding the InterfaceMethods inside a concrete |
638 | + // RestrictedMethods type, we actually only expose the methods defined |
639 | + // in InterfaceMethods. |
640 | + var r stringVal |
641 | + err := client.Call(rpc.Request{"MultiVersion", 3, "a99", "Call0r1e"}, nil, &r) |
642 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
643 | + Message: `no such request - method MultiVersion(3).Call0r1e is not implemented`, |
644 | + Code: rpc.CodeNotImplemented, |
645 | + }) |
646 | +} |
647 | + |
648 | +func (*rpcSuite) TestCustomCallerUnknownVersion(c *gc.C) { |
649 | + root := &CustomCaller{SimpleRoot()} |
650 | + client, srvDone, _, _ := newRPCClientServer(c, root, nil, false) |
651 | + defer closeClient(c, client, srvDone) |
652 | + var r stringVal |
653 | + // Unknown version 5 |
654 | + err := client.Call(rpc.Request{"MultiVersion", 5, "a99", "Call0r1"}, nil, &r) |
655 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
656 | + Message: `unknown version (5) of interface "MultiVersion"`, |
657 | + Code: rpc.CodeNotImplemented, |
658 | + }) |
659 | } |
660 | |
661 | func (*rpcSuite) TestConcurrentCalls(c *gc.C) { |
662 | @@ -505,7 +711,7 @@ |
663 | defer closeClient(c, client, srvDone) |
664 | call := func(id string, done chan<- struct{}) { |
665 | var r stringVal |
666 | - err := client.Call(rpc.Request{"DelayedMethods", id, "Delay"}, nil, &r) |
667 | + err := client.Call(rpc.Request{"DelayedMethods", 0, id, "Delay"}, nil, &r) |
668 | c.Check(err, gc.IsNil) |
669 | c.Check(r.Val, gc.Equals, "return "+id) |
670 | done <- struct{}{} |
671 | @@ -545,7 +751,7 @@ |
672 | } |
673 | client, srvDone, _, _ := newRPCClientServer(c, root, nil, false) |
674 | defer closeClient(c, client, srvDone) |
675 | - err := client.Call(rpc.Request{"ErrorMethods", "", "Call"}, nil, nil) |
676 | + err := client.Call(rpc.Request{"ErrorMethods", 0, "", "Call"}, nil, nil) |
677 | c.Assert(err, gc.ErrorMatches, `request error: message \(code\)`) |
678 | c.Assert(err.(rpc.ErrorCoder).ErrorCode(), gc.Equals, "code") |
679 | } |
680 | @@ -566,18 +772,33 @@ |
681 | } |
682 | client, srvDone, _, _ := newRPCClientServer(c, root, tfErr, false) |
683 | defer closeClient(c, client, srvDone) |
684 | - err := client.Call(rpc.Request{"ErrorMethods", "", "Call"}, nil, nil) |
685 | + // First, we don't transform methods we can't find. |
686 | + err := client.Call(rpc.Request{"foo", 0, "", "bar"}, nil, nil) |
687 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
688 | + Message: `unknown object type "foo"`, |
689 | + Code: rpc.CodeNotImplemented, |
690 | + }) |
691 | + |
692 | + err = client.Call(rpc.Request{"ErrorMethods", 0, "", "NoMethod"}, nil, nil) |
693 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
694 | + Message: "no such request - method ErrorMethods.NoMethod is not implemented", |
695 | + Code: rpc.CodeNotImplemented, |
696 | + }) |
697 | + |
698 | + // We do transform any errors that happen from calling the RootMethod |
699 | + // and beyond. |
700 | + err = client.Call(rpc.Request{"ErrorMethods", 0, "", "Call"}, nil, nil) |
701 | c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
702 | Message: "transformed: message", |
703 | Code: "transformed: code", |
704 | }) |
705 | |
706 | root.errorInst.err = nil |
707 | - err = client.Call(rpc.Request{"ErrorMethods", "", "Call"}, nil, nil) |
708 | + err = client.Call(rpc.Request{"ErrorMethods", 0, "", "Call"}, nil, nil) |
709 | c.Assert(err, gc.IsNil) |
710 | |
711 | root.errorInst = nil |
712 | - err = client.Call(rpc.Request{"ErrorMethods", "", "Call"}, nil, nil) |
713 | + err = client.Call(rpc.Request{"ErrorMethods", 0, "", "Call"}, nil, nil) |
714 | c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
715 | Message: "transformed: no error methods", |
716 | }) |
717 | @@ -600,7 +821,7 @@ |
718 | done := make(chan struct{}) |
719 | go func() { |
720 | var r stringVal |
721 | - err := client.Call(rpc.Request{"DelayedMethods", "1", "Delay"}, nil, &r) |
722 | + err := client.Call(rpc.Request{"DelayedMethods", 0, "1", "Delay"}, nil, &r) |
723 | c.Check(err, gc.Equals, rpc.ErrShutdown) |
724 | done <- struct{}{} |
725 | }() |
726 | @@ -635,7 +856,7 @@ |
727 | defer closeClient(c, client, srvDone) |
728 | call := func(method string, arg, ret interface{}) (passedArg interface{}) { |
729 | root.calls = nil |
730 | - err := client.Call(rpc.Request{"SimpleMethods", "a0", method}, arg, ret) |
731 | + err := client.Call(rpc.Request{"SimpleMethods", 0, "a0", method}, arg, ret) |
732 | c.Assert(err, gc.IsNil) |
733 | c.Assert(root.calls, gc.HasLen, 1) |
734 | info := root.calls[0] |
735 | @@ -678,22 +899,19 @@ |
736 | defer closeClient(c, client, srvDone) |
737 | |
738 | testBadCall(c, client, clientNotifier, serverNotifier, |
739 | - rpc.Request{"BadSomething", "a0", "No"}, |
740 | + rpc.Request{"BadSomething", 0, "a0", "No"}, |
741 | `unknown object type "BadSomething"`, |
742 | rpc.CodeNotImplemented, |
743 | - false, |
744 | ) |
745 | testBadCall(c, client, clientNotifier, serverNotifier, |
746 | - rpc.Request{"SimpleMethods", "xx", "No"}, |
747 | + rpc.Request{"SimpleMethods", 0, "a0", "No"}, |
748 | "no such request - method SimpleMethods.No is not implemented", |
749 | rpc.CodeNotImplemented, |
750 | - false, |
751 | ) |
752 | testBadCall(c, client, clientNotifier, serverNotifier, |
753 | - rpc.Request{"SimpleMethods", "xx", "Call0r0"}, |
754 | + rpc.Request{"SimpleMethods", 0, "xx", "Call0r0"}, |
755 | `unknown SimpleMethods id`, |
756 | "", |
757 | - true, |
758 | ) |
759 | } |
760 | |
761 | @@ -704,7 +922,6 @@ |
762 | req rpc.Request, |
763 | expectedErr string, |
764 | expectedErrCode string, |
765 | - requestKnown bool, |
766 | ) { |
767 | clientNotifier.reset() |
768 | serverNotifier.reset() |
769 | @@ -746,9 +963,6 @@ |
770 | // If the request was not recognized or there was |
771 | // an error reading the body, body will be nil. |
772 | var expectBody interface{} |
773 | - if requestKnown { |
774 | - expectBody = struct{}{} |
775 | - } |
776 | c.Assert(serverReq, gc.DeepEquals, requestEvent{ |
777 | hdr: rpc.Header{ |
778 | RequestId: requestId, |
779 | @@ -786,10 +1000,10 @@ |
780 | }{ |
781 | X: map[string]int{"hello": 65}, |
782 | } |
783 | - err := client.Call(rpc.Request{"SimpleMethods", "a0", "SliceArg"}, arg0, &ret) |
784 | + err := client.Call(rpc.Request{"SimpleMethods", 0, "a0", "SliceArg"}, arg0, &ret) |
785 | c.Assert(err, gc.ErrorMatches, `request error: json: cannot unmarshal object into Go value of type \[\]string`) |
786 | |
787 | - err = client.Call(rpc.Request{"SimpleMethods", "a0", "SliceArg"}, arg0, &ret) |
788 | + err = client.Call(rpc.Request{"SimpleMethods", 0, "a0", "SliceArg"}, arg0, &ret) |
789 | c.Assert(err, gc.ErrorMatches, `request error: json: cannot unmarshal object into Go value of type \[\]string`) |
790 | |
791 | arg1 := struct { |
792 | @@ -797,7 +1011,7 @@ |
793 | }{ |
794 | X: []string{"one"}, |
795 | } |
796 | - err = client.Call(rpc.Request{"SimpleMethods", "a0", "SliceArg"}, arg1, &ret) |
797 | + err = client.Call(rpc.Request{"SimpleMethods", 0, "a0", "SliceArg"}, arg1, &ret) |
798 | c.Assert(err, gc.IsNil) |
799 | c.Assert(ret.Val, gc.Equals, "SliceArg ret") |
800 | } |
801 | @@ -806,7 +1020,7 @@ |
802 | client, srvDone, _, _ := newRPCClientServer(c, &Root{}, nil, false) |
803 | err := client.Close() |
804 | c.Assert(err, gc.IsNil) |
805 | - err = client.Call(rpc.Request{"Foo", "", "Bar"}, nil, nil) |
806 | + err = client.Call(rpc.Request{"Foo", 0, "", "Bar"}, nil, nil) |
807 | c.Assert(err, gc.Equals, rpc.ErrShutdown) |
808 | err = chanReadError(c, srvDone, "server done") |
809 | c.Assert(err, gc.IsNil) |
810 | @@ -848,7 +1062,7 @@ |
811 | clientRoot := &Root{conn: client} |
812 | client.Serve(clientRoot, nil) |
813 | var r int64val |
814 | - err := client.Call(rpc.Request{"CallbackMethods", "", "Factorial"}, int64val{12}, &r) |
815 | + err := client.Call(rpc.Request{"CallbackMethods", 0, "", "Factorial"}, int64val{12}, &r) |
816 | c.Assert(err, gc.IsNil) |
817 | c.Assert(r.I, gc.Equals, int64(479001600)) |
818 | } |
819 | @@ -858,7 +1072,7 @@ |
820 | client, srvDone, _, _ := newRPCClientServer(c, srvRoot, nil, true) |
821 | defer closeClient(c, client, srvDone) |
822 | var r int64val |
823 | - err := client.Call(rpc.Request{"CallbackMethods", "", "Factorial"}, int64val{12}, &r) |
824 | + err := client.Call(rpc.Request{"CallbackMethods", 0, "", "Factorial"}, int64val{12}, &r) |
825 | c.Assert(err, gc.ErrorMatches, "request error: request error: no service") |
826 | } |
827 | |
828 | @@ -867,13 +1081,13 @@ |
829 | client, srvDone, _, _ := newRPCClientServer(c, srvRoot, nil, true) |
830 | defer closeClient(c, client, srvDone) |
831 | var s stringVal |
832 | - err := client.Call(rpc.Request{"NewlyAvailable", "", "NewMethod"}, nil, &s) |
833 | + err := client.Call(rpc.Request{"NewlyAvailable", 0, "", "NewMethod"}, nil, &s) |
834 | c.Assert(err, gc.ErrorMatches, `request error: unknown object type "NewlyAvailable" \(not implemented\)`) |
835 | - err = client.Call(rpc.Request{"ChangeAPIMethods", "", "ChangeAPI"}, nil, nil) |
836 | + err = client.Call(rpc.Request{"ChangeAPIMethods", 0, "", "ChangeAPI"}, nil, nil) |
837 | c.Assert(err, gc.IsNil) |
838 | - err = client.Call(rpc.Request{"ChangeAPIMethods", "", "ChangeAPI"}, nil, nil) |
839 | + err = client.Call(rpc.Request{"ChangeAPIMethods", 0, "", "ChangeAPI"}, nil, nil) |
840 | c.Assert(err, gc.ErrorMatches, `request error: unknown object type "ChangeAPIMethods" \(not implemented\)`) |
841 | - err = client.Call(rpc.Request{"NewlyAvailable", "", "NewMethod"}, nil, &s) |
842 | + err = client.Call(rpc.Request{"NewlyAvailable", 0, "", "NewMethod"}, nil, &s) |
843 | c.Assert(err, gc.IsNil) |
844 | c.Assert(s, gc.Equals, stringVal{"new method result"}) |
845 | } |
846 | @@ -883,10 +1097,10 @@ |
847 | client, srvDone, _, _ := newRPCClientServer(c, srvRoot, nil, true) |
848 | defer closeClient(c, client, srvDone) |
849 | |
850 | - err := client.Call(rpc.Request{"ChangeAPIMethods", "", "RemoveAPI"}, nil, nil) |
851 | + err := client.Call(rpc.Request{"ChangeAPIMethods", 0, "", "RemoveAPI"}, nil, nil) |
852 | c.Assert(err, gc.IsNil) |
853 | |
854 | - err = client.Call(rpc.Request{"ChangeAPIMethods", "", "RemoveAPI"}, nil, nil) |
855 | + err = client.Call(rpc.Request{"ChangeAPIMethods", 0, "", "RemoveAPI"}, nil, nil) |
856 | c.Assert(err, gc.ErrorMatches, "request error: no service") |
857 | } |
858 | |
859 | @@ -906,11 +1120,11 @@ |
860 | |
861 | result := make(chan error) |
862 | go func() { |
863 | - result <- client.Call(rpc.Request{"DelayedMethods", "1", "Delay"}, nil, nil) |
864 | + result <- client.Call(rpc.Request{"DelayedMethods", 0, "1", "Delay"}, nil, nil) |
865 | }() |
866 | chanRead(c, ready, "method ready") |
867 | |
868 | - err := client.Call(rpc.Request{"ChangeAPIMethods", "", "ChangeAPI"}, nil, nil) |
869 | + err := client.Call(rpc.Request{"ChangeAPIMethods", 0, "", "ChangeAPI"}, nil, nil) |
870 | c.Assert(err, gc.IsNil) |
871 | |
872 | // Ensure that not only does the request in progress complete, |
873 | @@ -957,7 +1171,12 @@ |
874 | role = roleBoth |
875 | } |
876 | rpcConn := rpc.NewConn(NewJSONCodec(conn, role), serverNotifier) |
877 | - rpcConn.Serve(root, tfErr) |
878 | + if custroot, ok := root.(*CustomCaller); ok { |
879 | + rpcConn.ServeCaller(custroot, tfErr) |
880 | + custroot.root.conn = rpcConn |
881 | + } else { |
882 | + rpcConn.Serve(root, tfErr) |
883 | + } |
884 | if root, ok := root.(*Root); ok { |
885 | root.conn = rpcConn |
886 | } |
887 | |
888 | === modified file 'rpc/rpcreflect/type.go' |
889 | --- rpc/rpcreflect/type.go 2014-04-02 04:59:44 +0000 |
890 | +++ rpc/rpcreflect/type.go 2014-05-22 11:41:28 +0000 |
891 | @@ -73,10 +73,6 @@ |
892 | // the same type as the root object. The given id is passed |
893 | // as an argument to the method. |
894 | Call func(rcvr reflect.Value, id string) (reflect.Value, error) |
895 | - |
896 | - // Methods holds RPC object-method information about |
897 | - // objects returned from the above call. |
898 | - ObjType *ObjType |
899 | } |
900 | |
901 | // TypeOf returns information on all root-level RPC methods |
902 | @@ -153,11 +149,22 @@ |
903 | err, _ = out[1].Interface().(error) |
904 | } |
905 | r = out[0] |
906 | + if r.Kind() == reflect.Interface && r.NumMethod() == 0 { |
907 | + // The original function returned (interface{}, error). |
908 | + // We want to unwrap the interface{} into its more |
909 | + // concrete type, so that we can get the proper methods |
910 | + // exposed. |
911 | + // TODO: jam 2014-05-08 We could change this to always |
912 | + // defer down to the concrete type, at the cost that to |
913 | + // expose a subset of a type you must always expose a |
914 | + // new struct type. (Though you can expose a struct |
915 | + // that only embeds the interface you want.) |
916 | + r = reflect.ValueOf(r.Interface()) |
917 | + } |
918 | return |
919 | } |
920 | return &RootMethod{ |
921 | - Call: f, |
922 | - ObjType: ObjTypeOf(t.Out(0)), |
923 | + Call: f, |
924 | } |
925 | } |
926 | |
927 | |
928 | === modified file 'rpc/rpcreflect/value.go' |
929 | --- rpc/rpcreflect/value.go 2013-12-17 20:09:27 +0000 |
930 | +++ rpc/rpcreflect/value.go 2014-05-22 11:41:28 +0000 |
931 | @@ -11,15 +11,23 @@ |
932 | // CallNotImplementedError is an error, returned an attempt to call to |
933 | // an unknown API method is made. |
934 | type CallNotImplementedError struct { |
935 | + RootMethod string |
936 | + Version int |
937 | Method string |
938 | - RootMethod string |
939 | } |
940 | |
941 | func (e *CallNotImplementedError) Error() string { |
942 | if e.Method == "" { |
943 | + if e.Version != 0 { |
944 | + return fmt.Sprintf("unknown version (%d) of interface %q", e.Version, e.RootMethod) |
945 | + } |
946 | return fmt.Sprintf("unknown object type %q", e.RootMethod) |
947 | } |
948 | - return fmt.Sprintf("no such request - method %s.%s is not implemented", e.RootMethod, e.Method) |
949 | + methodVersion := e.RootMethod |
950 | + if e.Version != 0 { |
951 | + methodVersion = fmt.Sprintf("%s(%d)", e.RootMethod, e.Version) |
952 | + } |
953 | + return fmt.Sprintf("no such request - method %s.%s is not implemented", methodVersion, e.Method) |
954 | } |
955 | |
956 | // MethodCaller knows how to call a particular RPC method. |
957 | @@ -29,9 +37,8 @@ |
958 | // ResultType holds the result type of the result of caling the object method. |
959 | ResultType reflect.Type |
960 | |
961 | - rootValue reflect.Value |
962 | - rootMethod RootMethod |
963 | - objMethod ObjMethod |
964 | + objMethod ObjMethod |
965 | + obj reflect.Value |
966 | } |
967 | |
968 | // MethodCaller holds the value of the root of an RPC server that |
969 | @@ -69,36 +76,54 @@ |
970 | // It returns an error if either the root method or the object |
971 | // method were not found. |
972 | // It panics if called on the zero Value. |
973 | -func (v Value) MethodCaller(rootMethodName, objMethodName string) (MethodCaller, error) { |
974 | +func (v Value) MethodCaller(rootMethodName string, rootVersion int, objId, objMethodName string) (MethodCaller, error) { |
975 | if !v.IsValid() { |
976 | panic("MethodCaller called on invalid Value") |
977 | } |
978 | - caller := MethodCaller{ |
979 | - rootValue: v.rootValue, |
980 | + if rootVersion != 0 { |
981 | + return MethodCaller{}, &CallNotImplementedError{ |
982 | + RootMethod: rootMethodName, |
983 | + Version: rootVersion, |
984 | + } |
985 | } |
986 | var err error |
987 | - caller.rootMethod, err = v.rootType.Method(rootMethodName) |
988 | + rootMethod, err := v.rootType.Method(rootMethodName) |
989 | if err != nil { |
990 | return MethodCaller{}, &CallNotImplementedError{ |
991 | RootMethod: rootMethodName, |
992 | } |
993 | } |
994 | - caller.objMethod, err = caller.rootMethod.ObjType.Method(objMethodName) |
995 | + obj, err := rootMethod.Call(v.rootValue, objId) |
996 | + if err != nil { |
997 | + return MethodCaller{}, err |
998 | + } |
999 | + return MethodCallerFromValue(obj, rootMethodName, rootVersion, objMethodName) |
1000 | +} |
1001 | + |
1002 | +// MethodCallerFromValue will reflect for ObjType and probe for the actual |
1003 | +// method, wrapping it into a MethodCaller. |
1004 | +func MethodCallerFromValue( |
1005 | + obj reflect.Value, rootMethodName string, version int, objMethodName string, |
1006 | +) ( |
1007 | + MethodCaller, error, |
1008 | +) { |
1009 | + objType := ObjTypeOf(obj.Type()) |
1010 | + objMethod, err := objType.Method(objMethodName) |
1011 | if err != nil { |
1012 | return MethodCaller{}, &CallNotImplementedError{ |
1013 | RootMethod: rootMethodName, |
1014 | + Version: version, |
1015 | Method: objMethodName, |
1016 | } |
1017 | } |
1018 | - caller.ParamsType = caller.objMethod.Params |
1019 | - caller.ResultType = caller.objMethod.Result |
1020 | - return caller, nil |
1021 | + return MethodCaller{ |
1022 | + ParamsType: objMethod.Params, |
1023 | + ResultType: objMethod.Result, |
1024 | + objMethod: objMethod, |
1025 | + obj: obj, |
1026 | + }, nil |
1027 | } |
1028 | |
1029 | -func (caller MethodCaller) Call(objId string, arg reflect.Value) (reflect.Value, error) { |
1030 | - obj, err := caller.rootMethod.Call(caller.rootValue, objId) |
1031 | - if err != nil { |
1032 | - return reflect.Value{}, err |
1033 | - } |
1034 | - return caller.objMethod.Call(obj, arg) |
1035 | +func (caller MethodCaller) Call(arg reflect.Value) (reflect.Value, error) { |
1036 | + return caller.objMethod.Call(caller.obj, arg) |
1037 | } |
1038 | |
1039 | === modified file 'rpc/server.go' |
1040 | --- rpc/server.go 2014-03-05 19:41:34 +0000 |
1041 | +++ rpc/server.go 2014-05-22 11:41:28 +0000 |
1042 | @@ -69,6 +69,9 @@ |
1043 | // Type holds the type of object to act on. |
1044 | Type string |
1045 | |
1046 | + // Version holds the version of the interface to the Object to act on |
1047 | + Version int |
1048 | + |
1049 | // Id holds the id of the object to act on. |
1050 | Id string |
1051 | |
1052 | @@ -107,8 +110,12 @@ |
1053 | // mutex guards the following values. |
1054 | mutex sync.Mutex |
1055 | |
1056 | - // rootValue holds the value to use to serve RPC requests, if any. |
1057 | - rootValue rpcreflect.Value |
1058 | + // rootCaller holds the value to use to serve RPC requests, if any. |
1059 | + rootCaller Caller |
1060 | + |
1061 | + // killer is the original interface{} that was passed to |
1062 | + // Serve/ServeCaller if it implemented the Killer interface |
1063 | + killer Killer |
1064 | |
1065 | // transformErrors is used to transform returned errors. |
1066 | transformErrors func(error) error |
1067 | @@ -238,14 +245,44 @@ |
1068 | // set of methods being served by the connection. This will have |
1069 | // no effect on calls that are currently being services. |
1070 | // If root is nil, the connection will serve no methods. |
1071 | +// |
1072 | +// root can optionally implement the Killer method. If implemented, when |
1073 | +// the connection is closed, root.Kill() will be called. |
1074 | func (conn *Conn) Serve(root interface{}, transformErrors func(error) error) { |
1075 | + var rootCaller Caller |
1076 | rootValue := rpcreflect.ValueOf(reflect.ValueOf(root)) |
1077 | - if rootValue.IsValid() && transformErrors == nil { |
1078 | + if rootValue.IsValid() { |
1079 | + rootCaller = rootValue |
1080 | + } |
1081 | + conn.serve(rootCaller, root, transformErrors) |
1082 | +} |
1083 | + |
1084 | +// ServeCaller serves RPC requests on the connection by invoking methods retrieved |
1085 | +// from rootCaller. Note that it does not start the connection running, though |
1086 | +// it may be called once the connection is already started. |
1087 | +// |
1088 | +// The server executes each client request by calling MethodCaller to obtain a |
1089 | +// method to invoke. It invokes that method with the request parameters, |
1090 | +// possibly returning some result. |
1091 | +// |
1092 | +// rootCaller can optionally implement the Killer method. If implemented, when |
1093 | +// the connection is closed, rootCaller.Kill() will be called. |
1094 | +func (conn *Conn) ServeCaller(rootCaller Caller, transformErrors func(error) error) { |
1095 | + conn.serve(rootCaller, rootCaller, transformErrors) |
1096 | +} |
1097 | + |
1098 | +func (conn *Conn) serve(rootCaller Caller, maybeKiller interface{}, transformErrors func(error) error) { |
1099 | + if rootCaller != nil && transformErrors == nil { |
1100 | transformErrors = func(err error) error { return err } |
1101 | } |
1102 | conn.mutex.Lock() |
1103 | defer conn.mutex.Unlock() |
1104 | - conn.rootValue = rootValue |
1105 | + conn.rootCaller = rootCaller |
1106 | + if killer, ok := maybeKiller.(Killer); ok { |
1107 | + conn.killer = killer |
1108 | + } else { |
1109 | + conn.killer = nil |
1110 | + } |
1111 | conn.transformErrors = transformErrors |
1112 | } |
1113 | |
1114 | @@ -279,10 +316,8 @@ |
1115 | conn.closing = true |
1116 | // Kill server requests if appropriate. Client requests will be |
1117 | // terminated when the input loop finishes. |
1118 | - if conn.rootValue.IsValid() { |
1119 | - if killer, ok := conn.rootValue.GoValue().Interface().(Killer); ok { |
1120 | - killer.Kill() |
1121 | - } |
1122 | + if conn.killer != nil { |
1123 | + conn.killer.Kill() |
1124 | } |
1125 | conn.mutex.Unlock() |
1126 | |
1127 | @@ -305,6 +340,14 @@ |
1128 | ErrorCode() string |
1129 | } |
1130 | |
1131 | +// Caller is a way to dynamically lookup methods from a given (rootName, |
1132 | +// version, id, methodName) quadruplet. |
1133 | +type Caller interface { |
1134 | + // MethodCaller takes a description of a method to call, and returns an |
1135 | + // rpcreflect.MethodCaller that can be used to call the concrete method. |
1136 | + MethodCaller(rootMethodName string, version int, objId, objMethodName string) (rpcreflect.MethodCaller, error) |
1137 | +} |
1138 | + |
1139 | // Killer represents a type that can be asked to abort any outstanding |
1140 | // requests. The Kill method should return immediately. |
1141 | type Killer interface { |
1142 | @@ -373,8 +416,8 @@ |
1143 | if err := conn.readBody(nil, true); err != nil { |
1144 | return err |
1145 | } |
1146 | - // We don't transform the error because there |
1147 | - // may be no transformErrors function available. |
1148 | + // We don't transform the error here. bindRequest will have |
1149 | + // already transformed it and returned a zero req. |
1150 | return conn.writeErrorResponse(hdr, err, startTime) |
1151 | } |
1152 | var argp interface{} |
1153 | @@ -455,20 +498,23 @@ |
1154 | // a boundRequest that can call those methods. |
1155 | func (conn *Conn) bindRequest(hdr *Header) (boundRequest, error) { |
1156 | conn.mutex.Lock() |
1157 | - rootValue := conn.rootValue |
1158 | + rootCaller := conn.rootCaller |
1159 | transformErrors := conn.transformErrors |
1160 | conn.mutex.Unlock() |
1161 | |
1162 | - if !rootValue.IsValid() { |
1163 | + if conn.rootCaller == nil { |
1164 | return boundRequest{}, fmt.Errorf("no service") |
1165 | } |
1166 | - caller, err := rootValue.MethodCaller(hdr.Request.Type, hdr.Request.Action) |
1167 | + caller, err := rootCaller.MethodCaller( |
1168 | + hdr.Request.Type, hdr.Request.Version, hdr.Request.Id, hdr.Request.Action) |
1169 | if err != nil { |
1170 | if _, ok := err.(*rpcreflect.CallNotImplementedError); ok { |
1171 | err = &serverError{ |
1172 | Message: err.Error(), |
1173 | Code: CodeNotImplemented, |
1174 | } |
1175 | + } else { |
1176 | + err = transformErrors(err) |
1177 | } |
1178 | return boundRequest{}, err |
1179 | } |
1180 | @@ -482,7 +528,7 @@ |
1181 | // runRequest runs the given request and sends the reply. |
1182 | func (conn *Conn) runRequest(req boundRequest, arg reflect.Value, startTime time.Time) { |
1183 | defer conn.srvPending.Done() |
1184 | - rv, err := req.Call(req.hdr.Request.Id, arg) |
1185 | + rv, err := req.Call(arg) |
1186 | if err != nil { |
1187 | err = conn.writeErrorResponse(&req.hdr, req.transformErrors(err), startTime) |
1188 | } else { |
1189 | |
1190 | === modified file 'state/apiserver/root_test.go' |
1191 | --- state/apiserver/root_test.go 2014-05-20 08:02:01 +0000 |
1192 | +++ state/apiserver/root_test.go 2014-05-22 11:41:28 +0000 |
1193 | @@ -8,7 +8,6 @@ |
1194 | |
1195 | gc "launchpad.net/gocheck" |
1196 | |
1197 | - "launchpad.net/juju-core/rpc/rpcreflect" |
1198 | "launchpad.net/juju-core/state/apiserver" |
1199 | "launchpad.net/juju-core/testing" |
1200 | ) |
1201 | @@ -27,23 +26,26 @@ |
1202 | "GetAuthTag", |
1203 | } |
1204 | |
1205 | -func (*rootSuite) TestDiscardedAPIMethods(c *gc.C) { |
1206 | - t := rpcreflect.TypeOf(apiserver.RootType) |
1207 | - // We must have some root-level methods. |
1208 | - c.Assert(t.MethodNames(), gc.Not(gc.HasLen), 0) |
1209 | - c.Assert(t.DiscardedMethods(), gc.DeepEquals, allowedDiscardedMethods) |
1210 | - |
1211 | - for _, name := range t.MethodNames() { |
1212 | - m, err := t.Method(name) |
1213 | - c.Assert(err, gc.IsNil) |
1214 | - // We must have some methods on every object returned |
1215 | - // by a root-level method. |
1216 | - c.Assert(m.ObjType.MethodNames(), gc.Not(gc.HasLen), 0) |
1217 | - // We don't allow any methods that don't implement |
1218 | - // an RPC entry point. |
1219 | - c.Assert(m.ObjType.DiscardedMethods(), gc.HasLen, 0) |
1220 | - } |
1221 | -} |
1222 | +// TODO: jam 2014-05-11 reintroduce this test once we properly have types |
1223 | +// registered that can be reflected again. This was only disabled because we |
1224 | +// got rid of ObjType as an exposed attribute of RootType |
1225 | +// func (*rootSuite) TestDiscardedAPIMethods(c *gc.C) { |
1226 | +// t := rpcreflect.TypeOf(apiserver.RootType) |
1227 | +// // We must have some root-level methods. |
1228 | +// c.Assert(t.MethodNames(), gc.Not(gc.HasLen), 0) |
1229 | +// c.Assert(t.DiscardedMethods(), gc.DeepEquals, allowedDiscardedMethods) |
1230 | +// |
1231 | +// for _, name := range t.MethodNames() { |
1232 | +// m, err := t.Method(name) |
1233 | +// c.Assert(err, gc.IsNil) |
1234 | +// // We must have some methods on every object returned |
1235 | +// // by a root-level method. |
1236 | +// c.Assert(m.ObjType.MethodNames(), gc.Not(gc.HasLen), 0) |
1237 | +// // We don't allow any methods that don't implement |
1238 | +// // an RPC entry point. |
1239 | +// c.Assert(m.ObjType.DiscardedMethods(), gc.HasLen, 0) |
1240 | +// } |
1241 | +// } |
1242 | |
1243 | func (r *rootSuite) TestPingTimeout(c *gc.C) { |
1244 | closedc := make(chan time.Time, 1) |
1245 | |
1246 | === modified file 'utils/registry/registry.go' |
1247 | --- utils/registry/registry.go 2014-05-16 11:48:18 +0000 |
1248 | +++ utils/registry/registry.go 2014-05-22 11:41:28 +0000 |
1249 | @@ -21,7 +21,7 @@ |
1250 | versions map[string]Versions |
1251 | } |
1252 | |
1253 | -// NewTypedNameVersion creates a place to register your objects |
1254 | +// NewTypedNameVersion creates a place to register your objects. |
1255 | func NewTypedNameVersion(requiredType reflect.Type) *TypedNameVersion { |
1256 | return &TypedNameVersion{ |
1257 | requiredType: requiredType, |
1258 | @@ -39,11 +39,9 @@ |
1259 | type Versions map[int]interface{} |
1260 | |
1261 | // Register records the factory that can be used to produce an instance of the |
1262 | -// facade at the supplied version. |
1263 | -// If the object being registered doesn't Implement the required Type, then an |
1264 | -// error is returned. |
1265 | -// An error is also returned if an object is already registered with the given |
1266 | -// name and version. |
1267 | +// facade at the supplied version. If the object being registered doesn't |
1268 | +// Implement the required Type, then an error is returned. An error is also |
1269 | +// returned if an object is already registered with the given name and version. |
1270 | func (r *TypedNameVersion) Register(name string, version int, obj interface{}) error { |
1271 | if !reflect.TypeOf(obj).ConvertibleTo(r.requiredType) { |
1272 | return fmt.Errorf("object of type %T cannot be converted to type %s.%s", obj, r.requiredType.PkgPath(), r.requiredType.Name()) |
1273 | @@ -65,7 +63,7 @@ |
1274 | } |
1275 | |
1276 | // descriptionFromVersions aggregates the information in a Versions map into a |
1277 | -// more friendly form for List() |
1278 | +// more friendly form for List(). |
1279 | func descriptionFromVersions(name string, versions Versions) Description { |
1280 | intVersions := make([]int, 0, len(versions)) |
1281 | for version := range versions { |
1282 | @@ -94,7 +92,7 @@ |
1283 | } |
1284 | |
1285 | // Get returns the object for a single name and version. If the requested |
1286 | -// facade is not found, it returns error.NotFound |
1287 | +// facade is not found, it returns error.NotFound. |
1288 | func (r *TypedNameVersion) Get(name string, version int) (interface{}, error) { |
1289 | if versions, ok := r.versions[name]; ok { |
1290 | if factory, ok := versions[version]; ok { |
1291 | |
1292 | === modified file 'utils/registry/registry_test.go' |
1293 | --- utils/registry/registry_test.go 2014-05-20 04:27:02 +0000 |
1294 | +++ utils/registry/registry_test.go 2014-05-22 11:41:28 +0000 |
1295 | @@ -6,10 +6,10 @@ |
1296 | import ( |
1297 | "reflect" |
1298 | |
1299 | + "github.com/juju/errors" |
1300 | jc "github.com/juju/testing/checkers" |
1301 | gc "launchpad.net/gocheck" |
1302 | |
1303 | - "github.com/juju/errors" |
1304 | "launchpad.net/juju-core/testing" |
1305 | "launchpad.net/juju-core/utils/registry" |
1306 | ) |
Reviewers: mp+219653_ code.launchpad. net,
Message:
Please take a look.
Description:
rpc: support a Version field
This adds support for a Version field in RPC requests, and changes the
MethodCaller interface for how and when it looks up objects.
Most of the MethodCaller changes were already reviewed in:
https:/ /code.launchpad .net/~jameinel/ juju-core/ reflect- only-facades/ +merge/ 218764
There are only a couple of tweaks because the lookup is now done with a
real Version instead of overriding the "id" field.
This also uses the registry. TypedNameVersio n code in a sample registry example sort of code.
that
shows one possible way that we can do a Registry with versioned lookups.
Note that the next steps don't actually make use of rpc/registry/* so
I'm willing to pull it out, or put it in as a test-suite only thing.
Alternatively it might be something tha could fit into
documentation/
This does disable one test that assumed you could statically iterate
over all the types and exposed methods. I introduce a similar function
later on once we have the factories registered.
One thing that was brought up before about always unfolding Interface(),
I didn't do that yet, but I still could if we feel it would be more
straightforward.
https:/ /code.launchpad .net/~jameinel/ juju-core/ api-rpc- reflect- version/ +merge/ 219653
Requires: /code.launchpad .net/~jameinel/ juju-core/ typed-registry/ +merge/ 219161
https:/
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/92410043/
Affected files (+636, -124 lines): codec.go package_ test.go registry. go registry_ test.go type.go value.go /root_test. go
A [revision details]
M rpc/jsoncodec/
M rpc/reflect_test.go
A rpc/registry/
A rpc/registry/
A rpc/registry/
M rpc/rpc_test.go
M rpc/rpcreflect/
M rpc/rpcreflect/
M rpc/server.go
M state/apiserver