Merge lp:~jameinel/juju-core/reflect-only-facades into lp:~go-bot/juju-core/trunk
- reflect-only-facades
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~jameinel/juju-core/reflect-only-facades |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
652 lines (+316/-90) 6 files modified
rpc/reflect_test.go (+36/-20) rpc/rpc_test.go (+208/-19) rpc/rpcreflect/type.go (+13/-6) rpc/rpcreflect/value.go (+33/-23) rpc/server.go (+6/-4) state/apiserver/root_test.go (+20/-18) |
To merge this branch: | bzr merge lp:~jameinel/juju-core/reflect-only-facades |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+218764@code.launchpad.net |
Commit message
Description of the change
rpc: expose concrete types
This changes the RPC package a bit so that if you are declaring
RootMethod that returns 'interface{}' it can find the concrete type that
is being returned and expose the methods on that type (instead of
exposing 0 methods from interface{}).
This is a step along the way to supporting passing versions for Facades
in the "id" slot. We currently let you pass the value, but the concrete
type that is returned defines all of the Methods that could be
available. Which means that if you add a new Method in V2 of your API,
you would suddenly have that exposed in the V1 of your API. Similarly
you couldn't *actually* ever change the interface for a give method,
because then you couldn't call it one way for V1 and a different way for
V2.
There is a fair bit more to be done before we have actual API
versioning, but this is a step along that path and it seemed cohesive
enough to review on its own.
Roger Peppe (rogpeppe) wrote : | # |
I realise this is the direction you have decided to go, but before you do,
I think it is worth considering the advantages that a strictly static set
of exported methods provides. Specifically it allows the possibility of
allowing clients to introspect rpc method types without invoking the
methods, and other code to see all the methods exported, for example to
automatically generate documentation for the entire API or to generate
client entry points similarly (both of which I have done in the past).
I personally think that it is a pity we are to lose these possibilities,
when there is a reasonable versioning alternative available (we could just
include the version number in the top level method names).
On 8 May 2014 12:36, "John A Meinel" <email address hidden> wrote:
> Reviewers: mp+218764_
>
> Message:
> Please take a look.
>
> Description:
> rpc: expose concrete types
>
> This changes the RPC package a bit so that if you are declaring
> RootMethod that returns 'interface{}' it can find the concrete type that
> is being returned and expose the methods on that type (instead of
> exposing 0 methods from interface{}).
>
> This is a step along the way to supporting passing versions for Facades
> in the "id" slot. We currently let you pass the value, but the concrete
> type that is returned defines all of the Methods that could be
> available. Which means that if you add a new Method in V2 of your API,
> you would suddenly have that exposed in the V1 of your API. Similarly
> you couldn't *actually* ever change the interface for a give method,
> because then you couldn't call it one way for V1 and a different way for
> V2.
>
> There is a fair bit more to be done before we have actual API
> versioning, but this is a step along that path and it seemed cohesive
> enough to review on its own.
>
>
> https:/
>
> (do not edit description out of merge proposal)
>
>
> Please review this at https:/
>
> Affected files (+290, -61 lines):
> A [revision details]
> M rpc/reflect_test.go
> M rpc/rpc_test.go
> M rpc/rpcreflect/
> M rpc/rpcreflect/
> M rpc/server.go
>
>
>
> --
>
> https:/
> Your team juju hackers is requested to review the proposed merge of
> lp:~jameinel/juju-core/reflect-only-facades into lp:juju-core.
>
William Reade (fwereade) wrote : | # |
LGTM, thanks
https:/
File rpc/reflect_test.go (right):
https:/
rpc/reflect_
It's a bit of a mouthful, but I think so.
https:/
File rpc/rpc_test.go (right):
https:/
rpc/rpc_
SimpleMethods code expects
<gentle twitch>
I've made this choice often enough myself, but rarely felt wholly
comfortable with it in retrospect. It's probably OK, I guess :).
https:/
File rpc/rpcreflect/
https:/
rpc/rpcreflect/
That does indeed work for me, but you don't have to do it here if it
complicates this CL.
John A Meinel (jameinel) wrote : | # |
FWIW I plan on going further to introduce a runtime registry so that as
types are declared they are registered with what version is available, etc.
And while it doesn't let you use static introspection, I think it provides
the same dynamic introspection.
I think it also allows for less coupling and repeating yourself. (Types can
register themselves in an init() hook rather that having to have a concrete
root type that has lots of:
func (r *srvRoot) Machiner(id string) (*machine.
if id != "" {
// Safeguard id for possible future use.
return nil, common.ErrBadId
}
return machine.
}
redundancy.
On Thu, May 8, 2014 at 1:54 PM, Roger Peppe <email address hidden>wrote:
> I realise this is the direction you have decided to go, but before you do,
> I think it is worth considering the advantages that a strictly static set
> of exported methods provides. Specifically it allows the possibility of
> allowing clients to introspect rpc method types without invoking the
> methods, and other code to see all the methods exported, for example to
> automatically generate documentation for the entire API or to generate
> client entry points similarly (both of which I have done in the past).
>
> I personally think that it is a pity we are to lose these possibilities,
> when there is a reasonable versioning alternative available (we could just
> include the version number in the top level method names).
> On 8 May 2014 12:36, "John A Meinel" <email address hidden> wrote:
>
> > Reviewers: mp+218764_
> >
> > Message:
> > Please take a look.
> >
> > Description:
> > rpc: expose concrete types
> >
> > This changes the RPC package a bit so that if you are declaring
> > RootMethod that returns 'interface{}' it can find the concrete type that
> > is being returned and expose the methods on that type (instead of
> > exposing 0 methods from interface{}).
> >
> > This is a step along the way to supporting passing versions for Facades
> > in the "id" slot. We currently let you pass the value, but the concrete
> > type that is returned defines all of the Methods that could be
> > available. Which means that if you add a new Method in V2 of your API,
> > you would suddenly have that exposed in the V1 of your API. Similarly
> > you couldn't *actually* ever change the interface for a give method,
> > because then you couldn't call it one way for V1 and a different way for
> > V2.
> >
> > There is a fair bit more to be done before we have actual API
> > versioning, but this is a step along that path and it seemed cohesive
> > enough to review on its own.
> >
> >
> >
> https:/
> >
> > (do not edit description out of merge proposal)
> >
> >
> > Please review this at https:/
> >
> > Affected files (+290, -61 lines):
> > A [revision details]
> > M rpc/reflect_test.go
> > M rpc/rpc_test.go
> > M rpc/rpcreflect/
> > M rpc/rpcreflect/
> > M rpc/server.go
> >
> >
> >
> > --
> >
> >
> https:/
John A Meinel (jameinel) wrote : | # |
I also think it is actually easier to do collapsing of versions if you
specify it somewhere rather than just in the name. Yes you could look for
V\d+ at the end of every Method in order to collapse them down into named
Facades at different versions.
On Thu, May 8, 2014 at 4:15 PM, John A Meinel <email address hidden>wrote:
> FWIW I plan on going further to introduce a runtime registry so that as
> types are declared they are registered with what version is available, etc.
> And while it doesn't let you use static introspection, I think it provides
> the same dynamic introspection.
> I think it also allows for less coupling and repeating yourself. (Types can
> register themselves in an init() hook rather that having to have a concrete
> root type that has lots of:
> func (r *srvRoot) Machiner(id string) (*machine.
> if id != "" {
> // Safeguard id for possible future use.
> return nil, common.ErrBadId
> }
> return machine.
> }
>
> redundancy.
>
>
>
> On Thu, May 8, 2014 at 1:54 PM, Roger Peppe <<email address hidden>
> >wrote:
>
> > I realise this is the direction you have decided to go, but before you
> do,
> > I think it is worth considering the advantages that a strictly static set
> > of exported methods provides. Specifically it allows the possibility of
> > allowing clients to introspect rpc method types without invoking the
> > methods, and other code to see all the methods exported, for example to
> > automatically generate documentation for the entire API or to generate
> > client entry points similarly (both of which I have done in the past).
> >
> > I personally think that it is a pity we are to lose these possibilities,
> > when there is a reasonable versioning alternative available (we could
> just
> > include the version number in the top level method names).
> > On 8 May 2014 12:36, "John A Meinel" <email address hidden> wrote:
> >
> > > Reviewers: mp+218764_
> > >
> > > Message:
> > > Please take a look.
> > >
> > > Description:
> > > rpc: expose concrete types
> > >
> > > This changes the RPC package a bit so that if you are declaring
> > > RootMethod that returns 'interface{}' it can find the concrete type
> that
> > > is being returned and expose the methods on that type (instead of
> > > exposing 0 methods from interface{}).
> > >
> > > This is a step along the way to supporting passing versions for Facades
> > > in the "id" slot. We currently let you pass the value, but the concrete
> > > type that is returned defines all of the Methods that could be
> > > available. Which means that if you add a new Method in V2 of your API,
> > > you would suddenly have that exposed in the V1 of your API. Similarly
> > > you couldn't *actually* ever change the interface for a give method,
> > > because then you couldn't call it one way for V1 and a different way
> for
> > > V2.
> > >
> > > There is a fair bit more to be done before we have actual API
> > > versioning, but this is a step along that path and it seemed cohesive
> > > enough to review on its own.
> > >
> > >
> > >
> >
> https:/
Roger Peppe (rogpeppe) wrote : | # |
I am not convinced a global name space for types is a good idea. It should
be possible for two separate modules within the same program to export
entirely separate APIs without risk of clashing. I am on holiday though. I
will follow this no more!
On 8 May 2014 15:15, "John A Meinel" <email address hidden> wrote:
> FWIW I plan on going further to introduce a runtime registry so that as
> types are declared they are registered with what version is available, etc.
> And while it doesn't let you use static introspection, I think it provides
> the same dynamic introspection.
> I think it also allows for less coupling and repeating yourself. (Types can
> register themselves in an init() hook rather that having to have a concrete
> root type that has lots of:
> func (r *srvRoot) Machiner(id string) (*machine.
> if id != "" {
> // Safeguard id for possible future use.
> return nil, common.ErrBadId
> }
> return machine.
> }
>
> redundancy.
>
>
>
> On Thu, May 8, 2014 at 1:54 PM, Roger Peppe <<email address hidden>
> >wrote:
>
> > I realise this is the direction you have decided to go, but before you
> do,
> > I think it is worth considering the advantages that a strictly static set
> > of exported methods provides. Specifically it allows the possibility of
> > allowing clients to introspect rpc method types without invoking the
> > methods, and other code to see all the methods exported, for example to
> > automatically generate documentation for the entire API or to generate
> > client entry points similarly (both of which I have done in the past).
> >
> > I personally think that it is a pity we are to lose these possibilities,
> > when there is a reasonable versioning alternative available (we could
> just
> > include the version number in the top level method names).
> > On 8 May 2014 12:36, "John A Meinel" <email address hidden> wrote:
> >
> > > Reviewers: mp+218764_
> > >
> > > Message:
> > > Please take a look.
> > >
> > > Description:
> > > rpc: expose concrete types
> > >
> > > This changes the RPC package a bit so that if you are declaring
> > > RootMethod that returns 'interface{}' it can find the concrete type
> that
> > > is being returned and expose the methods on that type (instead of
> > > exposing 0 methods from interface{}).
> > >
> > > This is a step along the way to supporting passing versions for Facades
> > > in the "id" slot. We currently let you pass the value, but the concrete
> > > type that is returned defines all of the Methods that could be
> > > available. Which means that if you add a new Method in V2 of your API,
> > > you would suddenly have that exposed in the V1 of your API. Similarly
> > > you couldn't *actually* ever change the interface for a give method,
> > > because then you couldn't call it one way for V1 and a different way
> for
> > > V2.
> > >
> > > There is a fair bit more to be done before we have actual API
> > > versioning, but this is a step along that path and it seemed cohesive
> > > enough to review on its own.
> > >
> > >
> > >
> >
> https:/
John A Meinel (jameinel) wrote : | # |
The plan is to still support multiple roots, so the registry would be root
specific. So multiple potential registries and the current one gets handed
to the RPC package from the Root object.
On Fri, May 9, 2014 at 12:18 AM, Roger Peppe <email address hidden>wrote:
> I am not convinced a global name space for types is a good idea. It should
> be possible for two separate modules within the same program to export
> entirely separate APIs without risk of clashing. I am on holiday though. I
> will follow this no more!
> On 8 May 2014 15:15, "John A Meinel" <email address hidden> wrote:
>
> > FWIW I plan on going further to introduce a runtime registry so that as
> > types are declared they are registered with what version is available,
> etc.
> > And while it doesn't let you use static introspection, I think it
> provides
> > the same dynamic introspection.
> > I think it also allows for less coupling and repeating yourself. (Types
> can
> > register themselves in an init() hook rather that having to have a
> concrete
> > root type that has lots of:
> > func (r *srvRoot) Machiner(id string) (*machine.
> > if id != "" {
> > // Safeguard id for possible future use.
> > return nil, common.ErrBadId
> > }
> > return machine.
> > }
> >
> > redundancy.
> >
> >
> >
> > On Thu, May 8, 2014 at 1:54 PM, Roger Peppe <<email address hidden>
> > >wrote:
> >
> > > I realise this is the direction you have decided to go, but before you
> > do,
> > > I think it is worth considering the advantages that a strictly static
> set
> > > of exported methods provides. Specifically it allows the possibility
> of
> > > allowing clients to introspect rpc method types without invoking the
> > > methods, and other code to see all the methods exported, for example to
> > > automatically generate documentation for the entire API or to generate
> > > client entry points similarly (both of which I have done in the past).
> > >
> > > I personally think that it is a pity we are to lose these
> possibilities,
> > > when there is a reasonable versioning alternative available (we could
> > just
> > > include the version number in the top level method names).
> > > On 8 May 2014 12:36, "John A Meinel" <email address hidden> wrote:
> > >
> > > > Reviewers: mp+218764_
> > > >
> > > > Message:
> > > > Please take a look.
> > > >
> > > > Description:
> > > > rpc: expose concrete types
> > > >
> > > > This changes the RPC package a bit so that if you are declaring
> > > > RootMethod that returns 'interface{}' it can find the concrete type
> > that
> > > > is being returned and expose the methods on that type (instead of
> > > > exposing 0 methods from interface{}).
> > > >
> > > > This is a step along the way to supporting passing versions for
> Facades
> > > > in the "id" slot. We currently let you pass the value, but the
> concrete
> > > > type that is returned defines all of the Methods that could be
> > > > available. Which means that if you add a new Method in V2 of your
> API,
> > > > you would suddenly have that exposed in the V1 of your API. Similarly
> > > > you couldn't *actually* ...
- 2716. By John A Meinel
-
We don't need the rootMethod of MethodCaller anymore
- 2717. By John A Meinel
-
remove the unused attributes of MethodCaller
- 2718. By John A Meinel
-
Remove the test that would fail if we just merged reflect-
only-facades - 2719. By John A Meinel
-
don't refer to the now unused package
- 2720. By John A Meinel
-
Merge trunk to get bugfix for bug #1301353
This lets the test suite run on Trusty with juju-mongodb.
Unmerged revisions
- 2720. By John A Meinel
-
Merge trunk to get bugfix for bug #1301353
This lets the test suite run on Trusty with juju-mongodb.
- 2719. By John A Meinel
-
don't refer to the now unused package
- 2718. By John A Meinel
-
Remove the test that would fail if we just merged reflect-
only-facades - 2717. By John A Meinel
-
remove the unused attributes of MethodCaller
- 2716. By John A Meinel
-
We don't need the rootMethod of MethodCaller anymore
- 2715. By John A Meinel
-
We don't use the Method.ObjType anymore, so we can get rid of it.
- 2714. By John A Meinel
-
Start cleaning up the test suite.
- 2713. By John A Meinel
-
tweak the spelling for unknown versions
- 2712. By John A Meinel
-
MethodCaller.Call doesn't need the Id anymore.
We've already resolved the concrete object, cache it and use it
rather than looking it up a second time. - 2711. By John A Meinel
-
Since now methods may exist on different versions of the Interface, we need to put more detail into the error message.
For backwards compatibility if id = "" we omit that portion.
Preview Diff
1 | === modified file 'rpc/reflect_test.go' |
2 | --- rpc/reflect_test.go 2014-03-13 07:54:56 +0000 |
3 | +++ rpc/reflect_test.go 2014-05-12 09:51:08 +0000 |
4 | @@ -11,6 +11,7 @@ |
5 | |
6 | "launchpad.net/juju-core/rpc/rpcreflect" |
7 | "launchpad.net/juju-core/testing/testbase" |
8 | + "launchpad.net/juju-core/utils/set" |
9 | ) |
10 | |
11 | // We test rpcreflect in this package, so that the |
12 | @@ -23,28 +24,43 @@ |
13 | var _ = gc.Suite(&reflectSuite{}) |
14 | |
15 | func (*reflectSuite) TestTypeOf(c *gc.C) { |
16 | - rtype := rpcreflect.TypeOf(reflect.TypeOf(&Root{})) |
17 | + root := SimpleRoot() |
18 | + rtype := rpcreflect.TypeOf(reflect.TypeOf(root)) |
19 | c.Assert(rtype.DiscardedMethods(), gc.DeepEquals, []string{ |
20 | "Discard1", |
21 | "Discard2", |
22 | "Discard3", |
23 | }) |
24 | - expect := map[string]reflect.Type{ |
25 | - "CallbackMethods": reflect.TypeOf(&CallbackMethods{}), |
26 | - "ChangeAPIMethods": reflect.TypeOf(&ChangeAPIMethods{}), |
27 | - "DelayedMethods": reflect.TypeOf(&DelayedMethods{}), |
28 | - "ErrorMethods": reflect.TypeOf(&ErrorMethods{}), |
29 | - "InterfaceMethods": reflect.TypeOf((*InterfaceMethods)(nil)).Elem(), |
30 | - "SimpleMethods": reflect.TypeOf(&SimpleMethods{}), |
31 | - } |
32 | - c.Assert(rtype.MethodNames(), gc.HasLen, len(expect)) |
33 | - for name, expectGoType := range expect { |
34 | - m, err := rtype.Method(name) |
35 | + expect := []struct { |
36 | + Name string |
37 | + Id string |
38 | + GoType reflect.Type |
39 | + }{ |
40 | + {"CallbackMethods", "", reflect.TypeOf(&CallbackMethods{})}, |
41 | + {"ChangeAPIMethods", "", reflect.TypeOf(&ChangeAPIMethods{})}, |
42 | + {"DelayedMethods", "", reflect.TypeOf(&DelayedMethods{})}, |
43 | + {"ErrorMethods", "", reflect.TypeOf(&ErrorMethods{})}, |
44 | + {"InterfaceMethods", "a99", reflect.TypeOf((*InterfaceMethods)(nil)).Elem()}, |
45 | + {"SimpleMethods", "a99", reflect.TypeOf(&SimpleMethods{})}, |
46 | + {"VariableMethods", "1", reflect.TypeOf(&VariableMethods1{})}, |
47 | + {"VariableMethods", "2", reflect.TypeOf(&VariableMethods2{})}, |
48 | + {"VariableMethods", "interface", reflect.TypeOf(&SimpleMethods{})}, |
49 | + {"VariableMethods", "restricted", reflect.TypeOf(RestrictedMethods{})}, |
50 | + } |
51 | + var uniqueNames set.Strings |
52 | + for _, exp := range expect { |
53 | + uniqueNames.Add(exp.Name) |
54 | + } |
55 | + c.Assert(rtype.MethodNames(), gc.DeepEquals, uniqueNames.SortedValues()) |
56 | + for _, exp := range expect { |
57 | + m, err := rtype.Method(exp.Name) |
58 | c.Assert(err, gc.IsNil) |
59 | c.Assert(m, gc.NotNil) |
60 | c.Assert(m.Call, gc.NotNil) |
61 | - c.Assert(m.ObjType, gc.Equals, rpcreflect.ObjTypeOf(expectGoType)) |
62 | - c.Assert(m.ObjType.GoType(), gc.Equals, expectGoType) |
63 | + obj, err := m.Call(reflect.ValueOf(root), exp.Id) |
64 | + c.Check(obj.Type(), gc.Equals, exp.GoType) |
65 | + // Is this worth specifying? |
66 | + c.Check(rpcreflect.ObjTypeOf(obj.Type()), gc.Equals, rpcreflect.ObjTypeOf(exp.GoType)) |
67 | } |
68 | m, err := rtype.Method("not found") |
69 | c.Assert(err, gc.Equals, rpcreflect.ErrMethodNotFound) |
70 | @@ -97,7 +113,7 @@ |
71 | func (*reflectSuite) TestValueOf(c *gc.C) { |
72 | v := rpcreflect.ValueOf(reflect.ValueOf(nil)) |
73 | c.Check(v.IsValid(), jc.IsFalse) |
74 | - c.Check(func() { v.MethodCaller("foo", "bar") }, gc.PanicMatches, "MethodCaller called on invalid Value") |
75 | + c.Check(func() { v.MethodCaller("foo", "bar", "") }, gc.PanicMatches, "MethodCaller called on invalid Value") |
76 | |
77 | root := &Root{} |
78 | v = rpcreflect.ValueOf(reflect.ValueOf(root)) |
79 | @@ -115,22 +131,22 @@ |
80 | root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
81 | v := rpcreflect.ValueOf(reflect.ValueOf(root)) |
82 | |
83 | - m, err := v.MethodCaller("foo", "bar") |
84 | + m, err := v.MethodCaller("foo", "a99", "bar") |
85 | c.Assert(err, gc.ErrorMatches, `unknown object type "foo"`) |
86 | c.Assert(err, gc.FitsTypeOf, (*rpcreflect.CallNotImplementedError)(nil)) |
87 | c.Assert(m, gc.DeepEquals, rpcreflect.MethodCaller{}) |
88 | |
89 | - m, err = v.MethodCaller("SimpleMethods", "bar") |
90 | - c.Assert(err, gc.ErrorMatches, "no such request - method SimpleMethods.bar is not implemented") |
91 | + m, err = v.MethodCaller("SimpleMethods", "a99", "bar") |
92 | + c.Assert(err, gc.ErrorMatches, `no such request - method SimpleMethods\(a99\)\.bar is not implemented`) |
93 | c.Assert(err, gc.FitsTypeOf, (*rpcreflect.CallNotImplementedError)(nil)) |
94 | c.Assert(m, gc.DeepEquals, rpcreflect.MethodCaller{}) |
95 | |
96 | - m, err = v.MethodCaller("SimpleMethods", "Call1r1e") |
97 | + m, err = v.MethodCaller("SimpleMethods", "a99", "Call1r1e") |
98 | c.Assert(err, gc.IsNil) |
99 | c.Assert(m.ParamsType, gc.Equals, reflect.TypeOf(stringVal{})) |
100 | c.Assert(m.ResultType, gc.Equals, reflect.TypeOf(stringVal{})) |
101 | |
102 | - ret, err := m.Call("a99", reflect.ValueOf(stringVal{"foo"})) |
103 | + ret, err := m.Call(reflect.ValueOf(stringVal{"foo"})) |
104 | c.Assert(err, gc.IsNil) |
105 | c.Assert(ret.Interface(), gc.Equals, stringVal{"Call1r1e ret"}) |
106 | } |
107 | |
108 | === modified file 'rpc/rpc_test.go' |
109 | --- rpc/rpc_test.go 2014-04-17 01:26:34 +0000 |
110 | +++ rpc/rpc_test.go 2014-05-12 09:51:08 +0000 |
111 | @@ -18,6 +18,7 @@ |
112 | |
113 | "launchpad.net/juju-core/rpc" |
114 | "launchpad.net/juju-core/rpc/jsoncodec" |
115 | + "launchpad.net/juju-core/rpc/rpcreflect" |
116 | "launchpad.net/juju-core/testing/testbase" |
117 | ) |
118 | |
119 | @@ -257,11 +258,59 @@ |
120 | return stringVal{"new method result"} |
121 | } |
122 | |
123 | +type VariableMethods1 struct { |
124 | + sm *SimpleMethods |
125 | +} |
126 | + |
127 | +func (vm *VariableMethods1) Call0r1() stringVal { |
128 | + return vm.sm.Call0r1() |
129 | +} |
130 | + |
131 | +type VariableMethods2 struct { |
132 | + sm *SimpleMethods |
133 | +} |
134 | + |
135 | +func (vm *VariableMethods2) Call1r1(s stringVal) stringVal { |
136 | + return vm.sm.Call1r1(s) |
137 | +} |
138 | + |
139 | +type RestrictedMethods struct { |
140 | + InterfaceMethods |
141 | +} |
142 | + |
143 | +func (r *Root) VariableMethods(id string) (interface{}, error) { |
144 | + sm, err := r.SimpleMethods("a99") |
145 | + switch id { |
146 | + case "1": |
147 | + return &VariableMethods1{sm}, nil |
148 | + case "2": |
149 | + return &VariableMethods2{sm}, nil |
150 | + case "interface": |
151 | + var interfaced InterfaceMethods |
152 | + interfaced = sm |
153 | + return interfaced, err |
154 | + case "restricted": |
155 | + var restricted RestrictedMethods |
156 | + restricted.InterfaceMethods = sm |
157 | + return restricted, err |
158 | + default: |
159 | + return nil, &rpcreflect.CallNotImplementedError{ |
160 | + RootMethod: "VariableMethods", |
161 | + Id: id, |
162 | + } |
163 | + } |
164 | +} |
165 | + |
166 | +func SimpleRoot() *Root { |
167 | + root := &Root{ |
168 | + simple: make(map[string]*SimpleMethods), |
169 | + } |
170 | + root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
171 | + return root |
172 | +} |
173 | + |
174 | func (*rpcSuite) TestRPC(c *gc.C) { |
175 | - root := &Root{ |
176 | - simple: make(map[string]*SimpleMethods), |
177 | - } |
178 | - root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
179 | + root := SimpleRoot() |
180 | client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
181 | defer closeClient(c, client, srvDone) |
182 | for narg := 0; narg < 2; narg++ { |
183 | @@ -324,13 +373,21 @@ |
184 | |
185 | // testErr specifies whether the call should be made to return an error. |
186 | testErr bool |
187 | + |
188 | + // id specifies what id string to pass to the RPC, if empty we default |
189 | + // to "a99" since that is what most of the SimpleMethods code expects |
190 | + id string |
191 | } |
192 | |
193 | // request returns the RPC request for the test call. |
194 | func (p testCallParams) request() rpc.Request { |
195 | + id := p.id |
196 | + if id == "" { |
197 | + id = "a99" |
198 | + } |
199 | return rpc.Request{ |
200 | Type: p.entry, |
201 | - Id: "a99", |
202 | + Id: id, |
203 | Action: callName(p.narg, p.nret, p.retErr), |
204 | } |
205 | } |
206 | @@ -466,10 +523,7 @@ |
207 | } |
208 | |
209 | func (*rpcSuite) TestInterfaceMethods(c *gc.C) { |
210 | - root := &Root{ |
211 | - simple: make(map[string]*SimpleMethods), |
212 | - } |
213 | - root.simple["a99"] = &SimpleMethods{root: root, id: "a99"} |
214 | + root := SimpleRoot() |
215 | client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
216 | defer closeClient(c, client, srvDone) |
217 | p := testCallParams{ |
218 | @@ -486,6 +540,133 @@ |
219 | root.testCall(c, p) |
220 | p.testErr = true |
221 | root.testCall(c, p) |
222 | + // Call0r0 is defined on the underlying SimpleMethods, but is not |
223 | + // exposed at the InterfaceMethods level, so this call should fail with |
224 | + // CodeNotImplemented |
225 | + var r stringVal |
226 | + err := client.Call(rpc.Request{"InterfaceMethods", "a99", "Call0r0"}, stringVal{"arg"}, &r) |
227 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
228 | + Message: "no such request - method InterfaceMethods(a99).Call0r0 is not implemented", |
229 | + Code: rpc.CodeNotImplemented, |
230 | + }) |
231 | +} |
232 | + |
233 | +func (*rpcSuite) TestVariableMethods1(c *gc.C) { |
234 | + root := SimpleRoot() |
235 | + client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
236 | + defer closeClient(c, client, srvDone) |
237 | + p := testCallParams{ |
238 | + client: client, |
239 | + clientNotifier: clientNotifier, |
240 | + serverNotifier: serverNotifier, |
241 | + entry: "VariableMethods", |
242 | + id: "1", |
243 | + narg: 0, |
244 | + nret: 1, |
245 | + retErr: false, |
246 | + testErr: false, |
247 | + } |
248 | + |
249 | + root.testCall(c, p) |
250 | + // Call1r1 is exposed on id "2", but not on id "1" |
251 | + var r stringVal |
252 | + err := client.Call(rpc.Request{"VariableMethods", "1", "Call1r1"}, stringVal{"arg"}, &r) |
253 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
254 | + Message: "no such request - method VariableMethods(1).Call1r1 is not implemented", |
255 | + Code: rpc.CodeNotImplemented, |
256 | + }) |
257 | +} |
258 | + |
259 | +func (*rpcSuite) TestVariableMethods2(c *gc.C) { |
260 | + root := SimpleRoot() |
261 | + client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
262 | + defer closeClient(c, client, srvDone) |
263 | + p := testCallParams{ |
264 | + client: client, |
265 | + clientNotifier: clientNotifier, |
266 | + serverNotifier: serverNotifier, |
267 | + entry: "VariableMethods", |
268 | + id: "2", |
269 | + narg: 1, |
270 | + nret: 1, |
271 | + retErr: false, |
272 | + testErr: false, |
273 | + } |
274 | + |
275 | + root.testCall(c, p) |
276 | + // Call0r1 is exposed on id "1", but not on id "2" |
277 | + var r stringVal |
278 | + err := client.Call(rpc.Request{"VariableMethods", "2", "Call0r1"}, nil, &r) |
279 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
280 | + Message: "no such request - method VariableMethods(2).Call0r1 is not implemented", |
281 | + Code: rpc.CodeNotImplemented, |
282 | + }) |
283 | +} |
284 | + |
285 | +func (*rpcSuite) TestVariableMethodsInterface(c *gc.C) { |
286 | + root := SimpleRoot() |
287 | + client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
288 | + defer closeClient(c, client, srvDone) |
289 | + p := testCallParams{ |
290 | + client: client, |
291 | + clientNotifier: clientNotifier, |
292 | + serverNotifier: serverNotifier, |
293 | + entry: "VariableMethods", |
294 | + id: "interface", |
295 | + narg: 1, |
296 | + nret: 1, |
297 | + retErr: true, |
298 | + testErr: false, |
299 | + } |
300 | + |
301 | + root.testCall(c, p) |
302 | + // Because of interface unwrapping, someone might have thought |
303 | + // "interface" would only expose the methods on InterfaceMethods, but |
304 | + // it actually exposes all of them |
305 | + p.narg = 0 |
306 | + root.testCall(c, p) |
307 | + p.nret = 0 |
308 | + root.testCall(c, p) |
309 | +} |
310 | + |
311 | +func (*rpcSuite) TestVariableMethodsRestricted(c *gc.C) { |
312 | + root := SimpleRoot() |
313 | + client, srvDone, clientNotifier, serverNotifier := newRPCClientServer(c, root, nil, false) |
314 | + defer closeClient(c, client, srvDone) |
315 | + p := testCallParams{ |
316 | + client: client, |
317 | + clientNotifier: clientNotifier, |
318 | + serverNotifier: serverNotifier, |
319 | + entry: "VariableMethods", |
320 | + id: "restricted", |
321 | + narg: 1, |
322 | + nret: 1, |
323 | + retErr: true, |
324 | + testErr: false, |
325 | + } |
326 | + |
327 | + root.testCall(c, p) |
328 | + // By embedding the InterfaceMethods inside a concrete |
329 | + // RestrictedMethods type, we actually only expose the methods defined |
330 | + // in InterfaceMethods |
331 | + var r stringVal |
332 | + err := client.Call(rpc.Request{"VariableMethods", "restricted", "Call0r0"}, nil, &r) |
333 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
334 | + Message: "no such request - method VariableMethods(restricted).Call0r0 is not implemented", |
335 | + Code: rpc.CodeNotImplemented, |
336 | + }) |
337 | +} |
338 | + |
339 | +func (*rpcSuite) TestVariableMethodsUnknown(c *gc.C) { |
340 | + root := SimpleRoot() |
341 | + client, srvDone, _, _ := newRPCClientServer(c, root, nil, false) |
342 | + defer closeClient(c, client, srvDone) |
343 | + var r stringVal |
344 | + err := client.Call(rpc.Request{"VariableMethods", "unknown", "Call0r0"}, nil, &r) |
345 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
346 | + Message: `unknown version "unknown" of interface "VariableMethods"`, |
347 | + Code: rpc.CodeNotImplemented, |
348 | + }) |
349 | } |
350 | |
351 | func (*rpcSuite) TestConcurrentCalls(c *gc.C) { |
352 | @@ -566,7 +747,22 @@ |
353 | } |
354 | client, srvDone, _, _ := newRPCClientServer(c, root, tfErr, false) |
355 | defer closeClient(c, client, srvDone) |
356 | - err := client.Call(rpc.Request{"ErrorMethods", "", "Call"}, nil, nil) |
357 | + // First, we don't transform methods we can't find |
358 | + err := client.Call(rpc.Request{"foo", "", "bar"}, nil, nil) |
359 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
360 | + Message: `unknown object type "foo"`, |
361 | + Code: rpc.CodeNotImplemented, |
362 | + }) |
363 | + |
364 | + err = client.Call(rpc.Request{"ErrorMethods", "", "NoMethod"}, nil, nil) |
365 | + c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
366 | + Message: "no such request - method ErrorMethods.NoMethod is not implemented", |
367 | + Code: rpc.CodeNotImplemented, |
368 | + }) |
369 | + |
370 | + // We do transform any errors that happen from calling the RootMethod |
371 | + // and beyond |
372 | + err = client.Call(rpc.Request{"ErrorMethods", "", "Call"}, nil, nil) |
373 | c.Assert(err, gc.DeepEquals, &rpc.RequestError{ |
374 | Message: "transformed: message", |
375 | Code: "transformed: code", |
376 | @@ -681,19 +877,16 @@ |
377 | rpc.Request{"BadSomething", "a0", "No"}, |
378 | `unknown object type "BadSomething"`, |
379 | rpc.CodeNotImplemented, |
380 | - false, |
381 | ) |
382 | testBadCall(c, client, clientNotifier, serverNotifier, |
383 | - rpc.Request{"SimpleMethods", "xx", "No"}, |
384 | - "no such request - method SimpleMethods.No is not implemented", |
385 | + rpc.Request{"SimpleMethods", "a0", "No"}, |
386 | + "no such request - method SimpleMethods(a0).No is not implemented", |
387 | rpc.CodeNotImplemented, |
388 | - false, |
389 | ) |
390 | testBadCall(c, client, clientNotifier, serverNotifier, |
391 | rpc.Request{"SimpleMethods", "xx", "Call0r0"}, |
392 | `unknown SimpleMethods id`, |
393 | "", |
394 | - true, |
395 | ) |
396 | } |
397 | |
398 | @@ -704,7 +897,6 @@ |
399 | req rpc.Request, |
400 | expectedErr string, |
401 | expectedErrCode string, |
402 | - requestKnown bool, |
403 | ) { |
404 | clientNotifier.reset() |
405 | serverNotifier.reset() |
406 | @@ -746,9 +938,6 @@ |
407 | // If the request was not recognized or there was |
408 | // an error reading the body, body will be nil. |
409 | var expectBody interface{} |
410 | - if requestKnown { |
411 | - expectBody = struct{}{} |
412 | - } |
413 | c.Assert(serverReq, gc.DeepEquals, requestEvent{ |
414 | hdr: rpc.Header{ |
415 | RequestId: requestId, |
416 | |
417 | === modified file 'rpc/rpcreflect/type.go' |
418 | --- rpc/rpcreflect/type.go 2014-04-02 04:59:44 +0000 |
419 | +++ rpc/rpcreflect/type.go 2014-05-12 09:51:08 +0000 |
420 | @@ -73,10 +73,6 @@ |
421 | // the same type as the root object. The given id is passed |
422 | // as an argument to the method. |
423 | Call func(rcvr reflect.Value, id string) (reflect.Value, error) |
424 | - |
425 | - // Methods holds RPC object-method information about |
426 | - // objects returned from the above call. |
427 | - ObjType *ObjType |
428 | } |
429 | |
430 | // TypeOf returns information on all root-level RPC methods |
431 | @@ -153,11 +149,22 @@ |
432 | err, _ = out[1].Interface().(error) |
433 | } |
434 | r = out[0] |
435 | + if r.Kind() == reflect.Interface && r.NumMethod() == 0 { |
436 | + // The original function returned (interface{}, error). |
437 | + // We want to unwrap the interface{} into its more |
438 | + // concrete type, so that we can get the proper methods |
439 | + // exposed. |
440 | + // TODO: jam 2014-05-08 We could change this to always |
441 | + // defer down to the concrete type, at the cost that to |
442 | + // expose a subset of a type you must always expose a |
443 | + // new struct type. (Though you can expose a struct |
444 | + // that only embeds the interface you want.) |
445 | + r = reflect.ValueOf(r.Interface()) |
446 | + } |
447 | return |
448 | } |
449 | return &RootMethod{ |
450 | - Call: f, |
451 | - ObjType: ObjTypeOf(t.Out(0)), |
452 | + Call: f, |
453 | } |
454 | } |
455 | |
456 | |
457 | === modified file 'rpc/rpcreflect/value.go' |
458 | --- rpc/rpcreflect/value.go 2013-12-17 20:09:27 +0000 |
459 | +++ rpc/rpcreflect/value.go 2014-05-12 09:51:08 +0000 |
460 | @@ -11,15 +11,23 @@ |
461 | // CallNotImplementedError is an error, returned an attempt to call to |
462 | // an unknown API method is made. |
463 | type CallNotImplementedError struct { |
464 | + RootMethod string |
465 | + Id string |
466 | Method string |
467 | - RootMethod string |
468 | } |
469 | |
470 | func (e *CallNotImplementedError) Error() string { |
471 | if e.Method == "" { |
472 | - return fmt.Sprintf("unknown object type %q", e.RootMethod) |
473 | - } |
474 | - return fmt.Sprintf("no such request - method %s.%s is not implemented", e.RootMethod, e.Method) |
475 | + if e.Id == "" { |
476 | + return fmt.Sprintf("unknown object type %q", e.RootMethod) |
477 | + } |
478 | + return fmt.Sprintf("unknown version %q of interface %q", e.Id, e.RootMethod) |
479 | + } |
480 | + methodVersion := e.RootMethod |
481 | + if e.Id != "" { |
482 | + methodVersion = fmt.Sprintf("%s(%s)", e.RootMethod, e.Id) |
483 | + } |
484 | + return fmt.Sprintf("no such request - method %s.%s is not implemented", methodVersion, e.Method) |
485 | } |
486 | |
487 | // MethodCaller knows how to call a particular RPC method. |
488 | @@ -29,9 +37,8 @@ |
489 | // ResultType holds the result type of the result of caling the object method. |
490 | ResultType reflect.Type |
491 | |
492 | - rootValue reflect.Value |
493 | - rootMethod RootMethod |
494 | - objMethod ObjMethod |
495 | + objMethod ObjMethod |
496 | + obj reflect.Value |
497 | } |
498 | |
499 | // MethodCaller holds the value of the root of an RPC server that |
500 | @@ -69,36 +76,39 @@ |
501 | // It returns an error if either the root method or the object |
502 | // method were not found. |
503 | // It panics if called on the zero Value. |
504 | -func (v Value) MethodCaller(rootMethodName, objMethodName string) (MethodCaller, error) { |
505 | +func (v Value) MethodCaller(rootMethodName, objId, objMethodName string) (MethodCaller, error) { |
506 | if !v.IsValid() { |
507 | panic("MethodCaller called on invalid Value") |
508 | } |
509 | - caller := MethodCaller{ |
510 | - rootValue: v.rootValue, |
511 | - } |
512 | - var err error |
513 | - caller.rootMethod, err = v.rootType.Method(rootMethodName) |
514 | + rootMethod, err := v.rootType.Method(rootMethodName) |
515 | if err != nil { |
516 | return MethodCaller{}, &CallNotImplementedError{ |
517 | RootMethod: rootMethodName, |
518 | } |
519 | } |
520 | - caller.objMethod, err = caller.rootMethod.ObjType.Method(objMethodName) |
521 | + obj, err := rootMethod.Call(v.rootValue, objId) |
522 | + if err != nil { |
523 | + // TODO: better handling of CallNotImplementedError for bad |
524 | + // objId |
525 | + return MethodCaller{}, err |
526 | + } |
527 | + objType := ObjTypeOf(obj.Type()) |
528 | + objMethod, err := objType.Method(objMethodName) |
529 | if err != nil { |
530 | return MethodCaller{}, &CallNotImplementedError{ |
531 | RootMethod: rootMethodName, |
532 | + Id: objId, |
533 | Method: objMethodName, |
534 | } |
535 | } |
536 | - caller.ParamsType = caller.objMethod.Params |
537 | - caller.ResultType = caller.objMethod.Result |
538 | - return caller, nil |
539 | + return MethodCaller{ |
540 | + ParamsType: objMethod.Params, |
541 | + ResultType: objMethod.Result, |
542 | + objMethod: objMethod, |
543 | + obj: obj, |
544 | + }, nil |
545 | } |
546 | |
547 | -func (caller MethodCaller) Call(objId string, arg reflect.Value) (reflect.Value, error) { |
548 | - obj, err := caller.rootMethod.Call(caller.rootValue, objId) |
549 | - if err != nil { |
550 | - return reflect.Value{}, err |
551 | - } |
552 | - return caller.objMethod.Call(obj, arg) |
553 | +func (caller MethodCaller) Call(arg reflect.Value) (reflect.Value, error) { |
554 | + return caller.objMethod.Call(caller.obj, arg) |
555 | } |
556 | |
557 | === modified file 'rpc/server.go' |
558 | --- rpc/server.go 2014-03-05 19:41:34 +0000 |
559 | +++ rpc/server.go 2014-05-12 09:51:08 +0000 |
560 | @@ -373,8 +373,8 @@ |
561 | if err := conn.readBody(nil, true); err != nil { |
562 | return err |
563 | } |
564 | - // We don't transform the error because there |
565 | - // may be no transformErrors function available. |
566 | + // We don't transform the error here. bindRequest will have |
567 | + // already transformed it and returned a zero req. |
568 | return conn.writeErrorResponse(hdr, err, startTime) |
569 | } |
570 | var argp interface{} |
571 | @@ -462,13 +462,15 @@ |
572 | if !rootValue.IsValid() { |
573 | return boundRequest{}, fmt.Errorf("no service") |
574 | } |
575 | - caller, err := rootValue.MethodCaller(hdr.Request.Type, hdr.Request.Action) |
576 | + caller, err := rootValue.MethodCaller(hdr.Request.Type, hdr.Request.Id, hdr.Request.Action) |
577 | if err != nil { |
578 | if _, ok := err.(*rpcreflect.CallNotImplementedError); ok { |
579 | err = &serverError{ |
580 | Message: err.Error(), |
581 | Code: CodeNotImplemented, |
582 | } |
583 | + } else { |
584 | + err = transformErrors(err) |
585 | } |
586 | return boundRequest{}, err |
587 | } |
588 | @@ -482,7 +484,7 @@ |
589 | // runRequest runs the given request and sends the reply. |
590 | func (conn *Conn) runRequest(req boundRequest, arg reflect.Value, startTime time.Time) { |
591 | defer conn.srvPending.Done() |
592 | - rv, err := req.Call(req.hdr.Request.Id, arg) |
593 | + rv, err := req.Call(arg) |
594 | if err != nil { |
595 | err = conn.writeErrorResponse(&req.hdr, req.transformErrors(err), startTime) |
596 | } else { |
597 | |
598 | === modified file 'state/apiserver/root_test.go' |
599 | --- state/apiserver/root_test.go 2014-01-22 19:28:08 +0000 |
600 | +++ state/apiserver/root_test.go 2014-05-12 09:51:08 +0000 |
601 | @@ -8,7 +8,6 @@ |
602 | |
603 | gc "launchpad.net/gocheck" |
604 | |
605 | - "launchpad.net/juju-core/rpc/rpcreflect" |
606 | "launchpad.net/juju-core/state/apiserver" |
607 | "launchpad.net/juju-core/testing" |
608 | ) |
609 | @@ -27,23 +26,26 @@ |
610 | "GetAuthTag", |
611 | } |
612 | |
613 | -func (*rootSuite) TestDiscardedAPIMethods(c *gc.C) { |
614 | - t := rpcreflect.TypeOf(apiserver.RootType) |
615 | - // We must have some root-level methods. |
616 | - c.Assert(t.MethodNames(), gc.Not(gc.HasLen), 0) |
617 | - c.Assert(t.DiscardedMethods(), gc.DeepEquals, allowedDiscardedMethods) |
618 | - |
619 | - for _, name := range t.MethodNames() { |
620 | - m, err := t.Method(name) |
621 | - c.Assert(err, gc.IsNil) |
622 | - // We must have some methods on every object returned |
623 | - // by a root-level method. |
624 | - c.Assert(m.ObjType.MethodNames(), gc.Not(gc.HasLen), 0) |
625 | - // We don't allow any methods that don't implement |
626 | - // an RPC entry point. |
627 | - c.Assert(m.ObjType.DiscardedMethods(), gc.HasLen, 0) |
628 | - } |
629 | -} |
630 | +// TODO: jam 2014-05-11 reintroduce this test once we properly have types |
631 | +// registered that can be reflected again. This was only disabled because we |
632 | +// got rid of ObjType as an exposed attribute of RootType |
633 | +// func (*rootSuite) TestDiscardedAPIMethods(c *gc.C) { |
634 | +// t := rpcreflect.TypeOf(apiserver.RootType) |
635 | +// // We must have some root-level methods. |
636 | +// c.Assert(t.MethodNames(), gc.Not(gc.HasLen), 0) |
637 | +// c.Assert(t.DiscardedMethods(), gc.DeepEquals, allowedDiscardedMethods) |
638 | +// |
639 | +// for _, name := range t.MethodNames() { |
640 | +// m, err := t.Method(name) |
641 | +// c.Assert(err, gc.IsNil) |
642 | +// // We must have some methods on every object returned |
643 | +// // by a root-level method. |
644 | +// c.Assert(m.ObjType.MethodNames(), gc.Not(gc.HasLen), 0) |
645 | +// // We don't allow any methods that don't implement |
646 | +// // an RPC entry point. |
647 | +// c.Assert(m.ObjType.DiscardedMethods(), gc.HasLen, 0) |
648 | +// } |
649 | +// } |
650 | |
651 | func (r *rootSuite) TestPingTimeout(c *gc.C) { |
652 | closedc := make(chan time.Time, 1) |
Reviewers: mp+218764_ code.launchpad. net,
Message:
Please take a look.
Description:
rpc: expose concrete types
This changes the RPC package a bit so that if you are declaring
RootMethod that returns 'interface{}' it can find the concrete type that
is being returned and expose the methods on that type (instead of
exposing 0 methods from interface{}).
This is a step along the way to supporting passing versions for Facades
in the "id" slot. We currently let you pass the value, but the concrete
type that is returned defines all of the Methods that could be
available. Which means that if you add a new Method in V2 of your API,
you would suddenly have that exposed in the V1 of your API. Similarly
you couldn't *actually* ever change the interface for a give method,
because then you couldn't call it one way for V1 and a different way for
V2.
There is a fair bit more to be done before we have actual API
versioning, but this is a step along that path and it seemed cohesive
enough to review on its own.
https:/ /code.launchpad .net/~jameinel/ juju-core/ reflect- only-facades/ +merge/ 218764
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/98090043/
Affected files (+290, -61 lines): type.go value.go
A [revision details]
M rpc/reflect_test.go
M rpc/rpc_test.go
M rpc/rpcreflect/
M rpc/rpcreflect/
M rpc/server.go