Merge lp:~jtv/gwacl/name-factory into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 166
Merged at revision: 165
Proposed branch: lp:~jtv/gwacl/name-factory
Merge into: lp:gwacl
Diff against target: 160 lines (+130/-3)
3 files modified
example/management/run.go (+2/-3)
names.go (+74/-0)
names_test.go (+54/-0)
To merge this branch: bzr merge lp:~jtv/gwacl/name-factory
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+173178@code.launchpad.net

Commit message

Export some factories for generating random names.

Description of the change

As discussed with rvba. This encodes knowledge about what exactly can be in the identifier Azure needs for a given type.

The driving use-case is not testing, so much as the need to come up with unique names when working against the real Azure. That's why I didn't go all-out. Of course with this in place, a logical next step would be to export similar factories for other types of identifiers.

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good.

[0]

9 - hostServiceName := makeRandomIdentifier("gwacl", 24)
10 + hostServiceName := gwacl.MakeRandomHostedServiceName("gwacl")

You can probably get rid of the version of makeRandomIdentifier that is in example/management/run.go and use the one from names.go when it's still used directly in run.go.

[1]

79 +// Be sure your random generator is initialized, or this will always produce
80 +// the same sequence!

Why don't we initialize the generator in names.go? This is, I think, so easily forgotten that it's worth not letting that up to the caller.

[2]

56 + if len(prefix) > length {
57 + panic(fmt.Errorf("prefix '%s' is more than the requested %d characters long", prefix, length))
58 + }

I think we're missing a test for that code path.

[3]

75 +// The prefix must be as short as possible, be entirely in ASCII, start with
76 +// a lower-case letter, and contain only lower-case letters and digits after
77 +// that.

Maybe it's me but it's not very clear what "must" means here: is it something that the method checks? I'd add something like "For the prefix to be compatible with Azure's constraints, the prefix must be…" or something.

[4]

81 +func MakeRandomHostedServiceName(prefix string) string {
82 + // A hosted-service name must be between 3 and 24 letters long.
83 + return makeRandomIdentifier(prefix, 24)
84 +}

Could you add links to the relevant piece of documentation please. I'm reading http://msdn.microsoft.com/en-us/library/windowsazure/gg441304.aspx and there is something odd in there:
"""
ServiceName
Required. A name for the cloud service that is unique within Windows Azure. This name is the DNS prefix name and can be used to access the service.
For example: http://ServiceName.cloudapp.net//
"""

This, AFAIK, is just plain wrong :/

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (3.4 KiB)

You put your finger on an interesting spot. See all the way below.

> 9 - hostServiceName := makeRandomIdentifier("gwacl", 24)
> 10 + hostServiceName := gwacl.MakeRandomHostedServiceName("gwacl")
>
> You can probably get rid of the version of makeRandomIdentifier that is in
> example/management/run.go and use the one from names.go when it's still used
> directly in run.go.

Then I'd have to export it, and update the call sites. But we may still want to mess with makeRandomIdentifier later, e.g. because there may be different kinds of identifier with different rules. I opted to leave as much as I could untouched for now, and not make any promises for a public MakeRandomIdentifier. It'll make much more sense to drop makeRandomIdentifier here once we have replacements for the other calls.

FWIW Pike says duplicating code between packages is the Go way.

> 79 +// Be sure your random generator is initialized, or this will always
> produce
> 80 +// the same sequence!
>
> Why don't we initialize the generator in names.go? This is, I think, so
> easily forgotten that it's worth not letting that up to the caller.

Because for this, I'm perfectly happy with repeated results between contiguous test runs.

> 56 + if len(prefix) > length {
> 57 + panic(fmt.Errorf("prefix '%s' is more than the requested %d
> characters long", prefix, length))
> 58 + }
>
> I think we're missing a test for that code path.

I started to write one, then decided it wasn't worth it. This is outside the function's contract, so the check is lagniappe.

> [3]
>
> 75 +// The prefix must be as short as possible, be entirely in ASCII,
> start with
> 76 +// a lower-case letter, and contain only lower-case letters and
> digits after
> 77 +// that.
>
> Maybe it's me but it's not very clear what "must" means here: is it something
> that the method checks? I'd add something like "For the prefix to be
> compatible with Azure's constraints, the prefix must be…" or something.

No, it's just specifying the contract for this function. If the caller steps outside the contract, they have no right to expect anything.

> 81 +func MakeRandomHostedServiceName(prefix string) string {
> 82 + // A hosted-service name must be between 3 and 24 letters long.
> 83 + return makeRandomIdentifier(prefix, 24)
> 84 +}
>
> Could you add links to the relevant piece of documentation please. I'm
> reading http://msdn.microsoft.com/en-us/library/windowsazure/gg441304.aspx and
> there is something odd in there:
> """
> ServiceName
> Required. A name for the cloud service that is unique within Windows Azure.
> This name is the DNS prefix name and can be used to access the service.
> For example: http://ServiceName.cloudapp.net//
> """
>
> This, AFAIK, is just plain wrong :/

Seriously!? That explains why we got confused over this the other day. What you quote here is the bit that I'd read, and I assumed it made some kind of sense. Sheesh.

But that doesn't explain where the numbers 3 and 24 came from. I think now that that may have been a hurried misinterpretation: a *storage service* has that restric...

Read more...

lp:~jtv/gwacl/name-factory updated
166. By Jeroen T. Vermeulen

No, 3—24 characters was for a storage service name, not a hosted service name.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Oh, if you're talking about initializing the randomizer *inside a call that uses the randomizer*, in production code: please do not do that. It's basically a way to sabotage your randomizer.

For example, say you happen to be using a timer with a resolution is one millisecond. (Exaggerating, but it might be happening somewhere. The timer's API might work in nanoseconds or even picoseconds, but that makes absolutely no promises about actual timer resolution.) You might be resetting your random seed and *repeating the same sequence of random numbers* on every call in the same millisecond!

There may be other patterns as well. Randomizers are carefully designed, and attempts to second-guess them will generally make things worse.

We could keep track of "somebody else may have seeded the randomizer, but has gwacl done it yet?" — but that's a function second-guessing the program's global state. If we impose requirements on the program's global state, the best thing to do is to expose a minimum interface to that state. In this case, it's enough to say "you must have seeded the randomizer if you want fresh pseudorandomness."

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Friday 05 Jul 2013 13:15:33 you wrote:
> 79 +// Be sure your random generator is initialized, or this will
> always produce 80 +// the same sequence!
>
> Why don't we initialize the generator in names.go? This is, I think, so
> easily forgotten that it's worth not letting that up to the caller.

Why not move all of this into helpers_factory_test.go where it really belongs,
which already has *Random* stuff in it and a corresponding init() to
initialise the generator?

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

It really does not belong there — see the MP: the primary use case is *not* testing. We want to call this from Juju.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 08 Jul 2013 10:51:35 you wrote:
> It really does not belong there — see the MP: the primary use case is *not*
> testing. We want to call this from Juju.

Oh, missed that. Sorry.

In that case, it belongs in the Juju code.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

I don't think it belongs in Juju code. It encodes some very Azure-specific, yet application-agnostic knowledge that other gwacl-using projects might well want to use.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'example/management/run.go'
2--- example/management/run.go 2013-07-05 10:33:40 +0000
3+++ example/management/run.go 2013-07-05 14:23:22 +0000
4@@ -99,8 +99,7 @@
5 fmt.Printf("Got image named '%s'\n", sourceImageName)
6 fmt.Println("Done getting OS Image\n")
7
8- // A hosted-service name must be between 3 and 24 characters long.
9- hostServiceName := makeRandomIdentifier("gwacl", 24)
10+ hostServiceName := gwacl.MakeRandomHostedServiceName("gwacl")
11 fmt.Printf("Creating a hosted service: '%s'...\n", hostServiceName)
12 createHostedService := gwacl.NewCreateHostedServiceWithLocation(hostServiceName, "testLabel", location)
13 err = api.AddHostedService(createHostedService)
14@@ -135,7 +134,7 @@
15 fmt.Println("Done listing hosted services\n")
16
17 fmt.Println("Adding VM deployment...")
18- hostname := makeRandomIdentifier("gwaclhost", 25)
19+ hostname := gwacl.MakeRandomHostname("gwaclhost")
20 // Random passwords are no use to man nor beast here if you want to
21 // test with your instance, so we'll use a fixed one. It's not really a
22 // security hazard in such a short-lived private instance.
23
24=== added file 'names.go'
25--- names.go 1970-01-01 00:00:00 +0000
26+++ names.go 2013-07-05 14:23:22 +0000
27@@ -0,0 +1,74 @@
28+// Copyright 2013 Canonical Ltd. This software is licensed under the
29+// GNU Lesser General Public License version 3 (see the file COPYING).
30+
31+package gwacl
32+
33+import (
34+ "fmt"
35+ "math/rand"
36+)
37+
38+// pickOne returns a random choice of one of the characters in chars.
39+func pickOne(chars string) string {
40+ index := rand.Intn(len(chars))
41+ return string(chars[index])
42+}
43+
44+// makeRandomIdentifier creates an arbitrary identifier of the given length,
45+// consisting of only ASCII digits and lower-case ASCII letters.
46+// The identifier will start with the given prefix. The prefix must be no
47+// longer than the specified length, or there'll be trouble.
48+func makeRandomIdentifier(prefix string, length int) string {
49+ // Only digits and lower-case ASCII letters are accepted.
50+ const (
51+ letters = "abcdefghijklmnopqrstuvwxyz"
52+ digits = "0123456789"
53+ chars = letters + digits
54+ )
55+
56+ if len(prefix) > length {
57+ panic(fmt.Errorf("prefix '%s' is more than the requested %d characters long", prefix, length))
58+ }
59+
60+ if len(prefix) == 0 {
61+ // No prefix. Still have to start with a letter, so pick one.
62+ prefix = pickOne(letters)
63+ }
64+
65+ id := prefix
66+ for len(id) < length {
67+ id += pickOne(chars)
68+ }
69+ return id
70+}
71+
72+// MakeRandomHostedServiceName generates a pseudo-random name for a hosted
73+// service, with the given prefix.
74+//
75+// The prefix must be as short as possible, be entirely in ASCII, start with
76+// a lower-case letter, and contain only lower-case letters and digits after
77+// that.
78+//
79+// Be sure your random generator is initialized, or this will always produce
80+// the same sequence!
81+func MakeRandomHostedServiceName(prefix string) string {
82+ // We don't know of any documentation on long a hosted-service name can
83+ // be, but this is the maximum length that worked in experiments.
84+ return makeRandomIdentifier(prefix, 63)
85+}
86+
87+// MakeRandomHostname generates a pseudo-random hostname for a virtual machine,
88+// with the given prefix.
89+//
90+// The prefix must be as short as possible, be entirely in ASCII, start with
91+// a lower-case letter, and contain only lower-case letters and digits after
92+// that.
93+//
94+// Be sure your random generator is initialized, or this will always produce
95+// the same sequence!
96+func MakeRandomHostname(prefix string) string {
97+ // Azure documentation says the hostname can be between 1 and 64
98+ // letters long, but in practice we found it didn't work with anything
99+ // over 55 letters long.
100+ return makeRandomIdentifier(prefix, 55)
101+}
102
103=== added file 'names_test.go'
104--- names_test.go 1970-01-01 00:00:00 +0000
105+++ names_test.go 2013-07-05 14:23:22 +0000
106@@ -0,0 +1,54 @@
107+// Copyright 2013 Canonical Ltd. This software is licensed under the
108+// GNU Lesser General Public License version 3 (see the file COPYING).
109+
110+package gwacl
111+
112+import (
113+ . "launchpad.net/gocheck"
114+ "math/rand"
115+)
116+
117+type namesSuite struct{}
118+
119+var _ = Suite(&namesSuite{})
120+
121+func (*namesSuite) TestPickOneReturnsOneCharacter(c *C) {
122+ c.Check(len(pickOne("abcd")), Equals, 1)
123+}
124+
125+func (*namesSuite) TestMakeRandomIdentifierObeysLength(c *C) {
126+ length := 1 + rand.Intn(50)
127+ c.Check(len(makeRandomIdentifier("x", length)), Equals, length)
128+}
129+
130+func (*namesSuite) TestMakeRandomIdentifierRandomizes(c *C) {
131+ // There is a minute chance that this will start failing just because
132+ // the randomizer repeats a pattern of results. If so, seed it.
133+ c.Check(
134+ makeRandomIdentifier("x", 100),
135+ Not(Equals),
136+ makeRandomIdentifier("x", 100))
137+}
138+
139+func (*namesSuite) TestMakeRandomIdentifierPicksDifferentCharacters(c *C) {
140+ // There is a minute chance that this will start failing just because
141+ // the randomizer repeats a pattern of results. If so, seed it.
142+ chars := make(map[rune]bool)
143+ for _, chr := range makeRandomIdentifier("", 100) {
144+ chars[chr] = true
145+ }
146+ c.Check(len(chars), Not(Equals), 1)
147+}
148+
149+func (*namesSuite) TestMakeRandomIdentifierUsesPrefix(c *C) {
150+ c.Check(makeRandomIdentifier("prefix", 10), Matches, "prefix.*")
151+}
152+
153+func (*namesSuite) TestMakeRandomIdentifierUsesOnlyAcceptedCharacters(c *C) {
154+ c.Check(makeRandomIdentifier("", 100), Matches, "[0-9a-z]*")
155+}
156+
157+func (*namesSuite) TestMakeRandomIdentifierAcceptsEmptyPrefix(c *C) {
158+ // In particular, the first character must still be a letter.
159+ c.Check(makeRandomIdentifier("", 1), Matches, "[a-z]")
160+}

Subscribers

People subscribed via source and target branches