Merge lp:~axwalk/juju-core/filestorage-write-tmpdir into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 1836
Proposed branch: lp:~axwalk/juju-core/filestorage-write-tmpdir
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 247 lines (+84/-19)
9 files modified
cmd/juju/synctools.go (+1/-1)
cmd/plugins/juju-metadata/toolsmetadata.go (+1/-1)
environs/filestorage/filestorage.go (+41/-8)
environs/filestorage/filestorage_test.go (+33/-1)
environs/httpstorage/backend_test.go (+1/-1)
environs/testing/storage.go (+1/-1)
environs/tools/simplestreams_test.go (+3/-3)
provider/local/environ.go (+1/-1)
provider/local/storage/worker.go (+2/-2)
To merge this branch: bzr merge lp:~axwalk/juju-core/filestorage-write-tmpdir
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Review via email: mp+185715@code.launchpad.net

Commit message

environs/filestorage: optional atomic Put

As requested in review of manual-bootstrap:
https://code.launchpad.net/~axwalk/juju-core/manual-bootstrap/+merge/184714

A new parameter is added to NewFileStorageWriter
to specify a temporary directory. Put will now
write to a temporary file in this location, and
then move it to the final destination; as long
as the temporary directory exists on the same
filesystem as the storage directory, this should
be atomic.

If the temporary directory is unspecified/blank,
then storagedir+".tmp" will be used to ensure
a "default" of colocation on the storage directory's
filesystem.

While it is not strictly necessary, the temporary
directory location will match that of sshstorage
(merge proposal pending).

https://codereview.appspot.com/13727043/

Description of the change

environs/filestorage: optional atomic Put

As requested in review of manual-bootstrap:
https://code.launchpad.net/~axwalk/juju-core/manual-bootstrap/+merge/184714

A new parameter is added to NewFileStorageWriter
to specify a temporary directory. Put will now
write to a temporary file in this location, and
then move it to the final destination; as long
as the temporary directory exists on the same
filesystem as the storage directory, this should
be atomic.

If the temporary directory is unspecified/blank,
then storagedir+".tmp" will be used to ensure
a "default" of colocation on the storage directory's
filesystem.

While it is not strictly necessary, the temporary
directory location will match that of sshstorage
(merge proposal pending).

https://codereview.appspot.com/13727043/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (5.8 KiB)

Reviewers: mp+185715_code.launchpad.net,

Message:
Please take a look.

Description:
environs/filestorage: optional atomic Put

As requested in review of manual-bootstrap:
https://code.launchpad.net/~axwalk/juju-core/manual-bootstrap/+merge/184714

A new parameter is added to NewFileStorageWriter
to specify a temporary directory. Put will now
write to a temporary file in this location, and
then move it to the final destination; as long
as the temporary directory exists on the same
filesystem as the storage directory, this should
be atomic.

As with io/ioutil TemporaryFile, if the temporary
directory is blank, os.TempDir will be used to
get the default temporary directory.

https://code.launchpad.net/~axwalk/juju-core/filestorage-write-tmpdir/+merge/185715

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/13727043/
Index: [revision details]
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision: tarmac-20130916061421-7r11uoyztquho14o
+New revision: <email address hidden>

Index: cmd/juju/synctools.go
=== modified file 'cmd/juju/synctools.go'
--- cmd/juju/synctools.go 2013-09-13 04:16:42 +0000
+++ cmd/juju/synctools.go 2013-09-16 06:41:18 +0000
@@ -83,7 +83,7 @@

   target := environ.Storage()
   if c.destination != "" {
- target, err = filestorage.NewFileStorageWriter(c.destination)
+ target, err = filestorage.NewFileStorageWriter(c.destination, "")
    if err != nil {
     return err
    }

Index: environs/filestorage/filestorage.go
=== modified file 'environs/filestorage/filestorage.go'
--- environs/filestorage/filestorage.go 2013-09-12 05:04:40 +0000
+++ environs/filestorage/filestorage.go 2013-09-16 06:41:18 +0000
@@ -21,7 +21,7 @@
   path string
  }

-// newFileStorageReader returns a new storage reader for
+// NewFileStorageReader returns a new storage reader for
  // a directory inside the local file system.
  func NewFileStorageReader(path string) (environs.StorageReader, error) {
   p := filepath.Clean(path)
@@ -85,14 +85,22 @@

  type fileStorageWriter struct {
   fileStorageReader
+ tmpdir string
  }

-func NewFileStorageWriter(path string) (environs.Storage, error) {
+// NewFileStorageWriter returns a new read/write storag for
+// a directory inside the local file system.
+//
+// A temporary directory may be specified, in which files will be written
+// to before moving to the final destination. If specified, the temporary
+// directory should be on the same filesystem as the storage directory
+// to ensure atomicity. If left unspecified (blank), $TMPDIR will be used.
+func NewFileStorageWriter(path, tmpdir string) (environs.Storage, error) {
   reader, err := NewFileStorageReader(path)
   if err != nil {
    return nil, err
   }
- return &fileStorageWriter{*reader.(*fileStorageReader)}, nil
+ return &fileStorageWriter{*reader.(*fileStorageReader), tmpdir}, nil
  }

  func (f *fileStorageWriter) Put(name string, r io.Reader, length int64)
error {
@@ -101,11 +109,18 @@
   if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Tim Penhey (thumper) wrote :

https://codereview.appspot.com/13727043/diff/7001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13727043/diff/7001/environs/filestorage/filestorage.go#newcode107
environs/filestorage/filestorage.go:107: if err := os.MkdirAll(tmpdir,
0755); err != nil && !os.IsExist(err) {
Do you ever remove the tmpdir?

https://codereview.appspot.com/13727043/diff/7001/environs/filestorage/filestorage_test.go
File environs/filestorage/filestorage_test.go (right):

https://codereview.appspot.com/13727043/diff/7001/environs/filestorage/filestorage_test.go#newcode39
environs/filestorage/filestorage_test.go:39: s.writer, err =
filestorage.NewFileStorageWriter(s.dir, "")
how about a constant in filestorage
   const UseDefaultTmpDir = ""

then

s.writer, err = filestorage.NewFileStorageWriter(s.dir,
filestorage.UseDefaultTmpDir)

It means I don't have to go and read NewFileStorageWriter to work out
what "" means.

https://codereview.appspot.com/13727043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/13727043/diff/7001/environs/filestorage/filestorage.go
File environs/filestorage/filestorage.go (right):

https://codereview.appspot.com/13727043/diff/7001/environs/filestorage/filestorage.go#newcode107
environs/filestorage/filestorage.go:107: if err := os.MkdirAll(tmpdir,
0755); err != nil && !os.IsExist(err) {
On 2013/09/18 04:09:23, thumper wrote:
> Do you ever remove the tmpdir?

No, never. I wasn't sure about this when I implemented it, but thinking
about it some more I think it should create/remove it iff
tmpdir==UseDefaultTmpDir. If a directory is explicitly specified, it
must exist and won't be removed. Then juju sync-tools won't leave tmpdir
turds.

I've updated the implementation, docstring and tests to match this.

https://codereview.appspot.com/13727043/diff/7001/environs/filestorage/filestorage_test.go
File environs/filestorage/filestorage_test.go (right):

https://codereview.appspot.com/13727043/diff/7001/environs/filestorage/filestorage_test.go#newcode39
environs/filestorage/filestorage_test.go:39: s.writer, err =
filestorage.NewFileStorageWriter(s.dir, "")
On 2013/09/18 04:09:23, thumper wrote:
> how about a constant in filestorage
> const UseDefaultTmpDir = ""

> then

> s.writer, err = filestorage.NewFileStorageWriter(s.dir,
> filestorage.UseDefaultTmpDir)

> It means I don't have to go and read NewFileStorageWriter to work out
what ""
> means.

Good idea. Done.

https://codereview.appspot.com/13727043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Tim Penhey (thumper) wrote :

On 2013/09/18 07:12:02, axw wrote:
> Please take a look.

LGTM

https://codereview.appspot.com/13727043/

Revision history for this message
Tim Penhey (thumper) :
review: Approve
Revision history for this message
Go Bot (go-bot) wrote :

Attempt to merge into lp:juju-core failed due to conflicts:

text conflict in environs/filestorage/filestorage.go
text conflict in environs/filestorage/filestorage_test.go

Revision history for this message
Go Bot (go-bot) wrote :

The attempt to merge lp:~axwalk/juju-core/filestorage-write-tmpdir into lp:juju-core failed. Below is the output from the failed tests.

# launchpad.net/juju-core/provider/local
provider/local/environ.go:169: not enough arguments in call to filestorage.NewFileStorageWriter
# launchpad.net/juju-core/provider/local/storage
provider/local/storage/worker.go:44: not enough arguments in call to filestorage.NewFileStorageWriter
provider/local/storage/worker.go:60: not enough arguments in call to filestorage.NewFileStorageWriter
# launchpad.net/juju-core/environs/testing
environs/testing/storage.go:31: not enough arguments in call to filestorage.NewFileStorageWriter

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/juju/synctools.go'
2--- cmd/juju/synctools.go 2013-09-13 04:16:42 +0000
3+++ cmd/juju/synctools.go 2013-09-19 03:28:43 +0000
4@@ -83,7 +83,7 @@
5
6 target := environ.Storage()
7 if c.destination != "" {
8- target, err = filestorage.NewFileStorageWriter(c.destination)
9+ target, err = filestorage.NewFileStorageWriter(c.destination, filestorage.UseDefaultTmpDir)
10 if err != nil {
11 return err
12 }
13
14=== modified file 'cmd/plugins/juju-metadata/toolsmetadata.go'
15--- cmd/plugins/juju-metadata/toolsmetadata.go 2013-09-16 10:47:28 +0000
16+++ cmd/plugins/juju-metadata/toolsmetadata.go 2013-09-19 03:28:43 +0000
17@@ -69,7 +69,7 @@
18 return err
19 }
20
21- targetStorage, err := filestorage.NewFileStorageWriter(c.metadataDir)
22+ targetStorage, err := filestorage.NewFileStorageWriter(c.metadataDir, filestorage.UseDefaultTmpDir)
23 if err != nil {
24 return err
25 }
26
27=== modified file 'environs/filestorage/filestorage.go'
28--- environs/filestorage/filestorage.go 2013-09-19 00:22:15 +0000
29+++ environs/filestorage/filestorage.go 2013-09-19 03:28:43 +0000
30@@ -23,7 +23,7 @@
31 path string
32 }
33
34-// newFileStorageReader returns a new storage reader for
35+// NewFileStorageReader returns a new storage reader for
36 // a directory inside the local file system.
37 func NewFileStorageReader(path string) (storage.StorageReader, error) {
38 p := filepath.Clean(path)
39@@ -98,27 +98,60 @@
40
41 type fileStorageWriter struct {
42 fileStorageReader
43+ tmpdir string
44 }
45
46-func NewFileStorageWriter(path string) (storage.Storage, error) {
47+// UseDefaultTmpDir may be passed into NewFileStorageWriter
48+// for the tmpdir argument, to signify that the default
49+// value should be used. See NewFileStorageWriter for more.
50+const UseDefaultTmpDir = ""
51+
52+// NewFileStorageWriter returns a new read/write storag for
53+// a directory inside the local file system.
54+//
55+// A temporary directory may be specified, in which files will be written
56+// to before moving to the final destination. If specified, the temporary
57+// directory should be on the same filesystem as the storage directory
58+// to ensure atomicity. If tmpdir == UseDefaultTmpDir (""), then path+".tmp"
59+// will be used.
60+//
61+// If tmpdir == UseDefaultTmpDir, it will be created when Put is invoked,
62+// and will be removed afterwards. If tmpdir != UseDefaultTmpDir, it must
63+// already exist, and will never be removed.
64+func NewFileStorageWriter(path, tmpdir string) (storage.Storage, error) {
65 reader, err := NewFileStorageReader(path)
66 if err != nil {
67 return nil, err
68 }
69- return &fileStorageWriter{*reader.(*fileStorageReader)}, nil
70+ return &fileStorageWriter{*reader.(*fileStorageReader), tmpdir}, nil
71 }
72
73 func (f *fileStorageWriter) Put(name string, r io.Reader, length int64) error {
74 fullpath := f.fullPath(name)
75 dir := filepath.Dir(fullpath)
76+ tmpdir := f.tmpdir
77+ if tmpdir == UseDefaultTmpDir {
78+ tmpdir = f.path + ".tmp"
79+ if err := os.MkdirAll(tmpdir, 0755); err != nil && !os.IsExist(err) {
80+ return err
81+ }
82+ defer os.Remove(tmpdir)
83+ }
84 if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
85 return err
86 }
87- data, err := ioutil.ReadAll(r)
88- if err != nil {
89- return err
90- }
91- return ioutil.WriteFile(fullpath, data, 0644)
92+ // Write to a temporary file first, and then move (atomically).
93+ file, err := ioutil.TempFile(tmpdir, "juju-filestorage-")
94+ if err != nil {
95+ return err
96+ }
97+ _, err = io.CopyN(file, r, length)
98+ file.Close()
99+ if err != nil {
100+ os.Remove(file.Name())
101+ return err
102+ }
103+ return os.Rename(file.Name(), fullpath)
104 }
105
106 func (f *fileStorageWriter) Remove(name string) error {
107
108=== modified file 'environs/filestorage/filestorage_test.go'
109--- environs/filestorage/filestorage_test.go 2013-09-19 00:22:15 +0000
110+++ environs/filestorage/filestorage_test.go 2013-09-19 03:28:43 +0000
111@@ -39,7 +39,7 @@
112 var err error
113 s.reader, err = filestorage.NewFileStorageReader(s.dir)
114 c.Assert(err, gc.IsNil)
115- s.writer, err = filestorage.NewFileStorageWriter(s.dir)
116+ s.writer, err = filestorage.NewFileStorageWriter(s.dir, filestorage.UseDefaultTmpDir)
117 c.Assert(err, gc.IsNil)
118 }
119
120@@ -135,3 +135,35 @@
121 _, err = ioutil.ReadFile(expectedpath)
122 c.Assert(err, gc.Not(gc.IsNil))
123 }
124+
125+func (s *filestorageSuite) TestPutTmpDir(c *gc.C) {
126+ // Put should create and clean up the temporary directory if
127+ // tmpdir==UseDefaultTmpDir.
128+ err := s.writer.Put("test-write", bytes.NewReader(nil), 0)
129+ c.Assert(err, gc.IsNil)
130+ _, err = os.Stat(s.dir + ".tmp")
131+ c.Assert(err, jc.Satisfies, os.IsNotExist)
132+
133+ // To deal with recovering from hard failure, UseDefaultTmpDir
134+ // doesn't care if the temporary directory already exists. It
135+ // still removes it, though.
136+ err = os.Mkdir(s.dir+".tmp", 0755)
137+ c.Assert(err, gc.IsNil)
138+ err = s.writer.Put("test-write", bytes.NewReader(nil), 0)
139+ c.Assert(err, gc.IsNil)
140+ _, err = os.Stat(s.dir + ".tmp")
141+ c.Assert(err, jc.Satisfies, os.IsNotExist)
142+
143+ // If we explicitly set the temporary directory, it must already exist.
144+ s.writer, err = filestorage.NewFileStorageWriter(s.dir, s.dir+".tmp")
145+ c.Assert(err, gc.IsNil)
146+ err = s.writer.Put("test-write", bytes.NewReader(nil), 0)
147+ c.Assert(err, jc.Satisfies, os.IsNotExist)
148+ err = os.Mkdir(s.dir+".tmp", 0755)
149+ c.Assert(err, gc.IsNil)
150+ err = s.writer.Put("test-write", bytes.NewReader(nil), 0)
151+ c.Assert(err, gc.IsNil)
152+ // Temporary directory should not have been moved.
153+ _, err = os.Stat(s.dir + ".tmp")
154+ c.Assert(err, gc.IsNil)
155+}
156
157=== modified file 'environs/httpstorage/backend_test.go'
158--- environs/httpstorage/backend_test.go 2013-09-18 05:32:18 +0000
159+++ environs/httpstorage/backend_test.go 2013-09-19 03:28:43 +0000
160@@ -36,7 +36,7 @@
161 // a base URL for the server and the directory path.
162 func startServer(c *gc.C) (listener net.Listener, url, dataDir string) {
163 dataDir = c.MkDir()
164- embedded, err := filestorage.NewFileStorageWriter(dataDir)
165+ embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir)
166 c.Assert(err, gc.IsNil)
167 listener, err = httpstorage.Serve("localhost:0", embedded)
168 c.Assert(err, gc.IsNil)
169
170=== modified file 'environs/testing/storage.go'
171--- environs/testing/storage.go 2013-09-19 00:22:15 +0000
172+++ environs/testing/storage.go 2013-09-19 03:28:43 +0000
173@@ -28,7 +28,7 @@
174 // directory.
175 func CreateLocalTestStorage(c *gc.C) (closer io.Closer, stor storage.Storage, dataDir string) {
176 dataDir = c.MkDir()
177- underlying, err := filestorage.NewFileStorageWriter(dataDir)
178+ underlying, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir)
179 c.Assert(err, gc.IsNil)
180 listener, err := httpstorage.Serve("localhost:0", underlying)
181 c.Assert(err, gc.IsNil)
182
183=== modified file 'environs/tools/simplestreams_test.go'
184--- environs/tools/simplestreams_test.go 2013-09-13 02:02:59 +0000
185+++ environs/tools/simplestreams_test.go 2013-09-19 03:28:43 +0000
186@@ -284,7 +284,7 @@
187 },
188 }
189 dir := c.MkDir()
190- writer, err := filestorage.NewFileStorageWriter(dir)
191+ writer, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir)
192 c.Assert(err, gc.IsNil)
193 err = tools.WriteMetadata(toolsList, false, writer)
194 c.Assert(err, gc.IsNil)
195@@ -311,7 +311,7 @@
196 URL: "file://" + filepath.Join(dir, "tools/releases/juju-2.0.1-raring-amd64.tgz"),
197 },
198 }
199- writer, err := filestorage.NewFileStorageWriter(dir)
200+ writer, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir)
201 c.Assert(err, gc.IsNil)
202 err = tools.WriteMetadata(toolsList, true, writer)
203 c.Assert(err, gc.IsNil)
204@@ -332,7 +332,7 @@
205 SHA256: "xyz",
206 },
207 }
208- writer, err := filestorage.NewFileStorageWriter(dir)
209+ writer, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir)
210 c.Assert(err, gc.IsNil)
211 err = tools.WriteMetadata(existingToolsList, true, writer)
212 c.Assert(err, gc.IsNil)
213
214=== modified file 'provider/local/environ.go'
215--- provider/local/environ.go 2013-09-19 00:22:15 +0000
216+++ provider/local/environ.go 2013-09-19 03:28:43 +0000
217@@ -166,7 +166,7 @@
218 } else if !info.Mode().IsDir() {
219 return nil, fmt.Errorf("%q exists but is not a directory (and it needs to be)", dir)
220 }
221- storage, err := filestorage.NewFileStorageWriter(dir)
222+ storage, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir)
223 if err != nil {
224 return nil, err
225 }
226
227=== modified file 'provider/local/storage/worker.go'
228--- provider/local/storage/worker.go 2013-09-17 01:33:41 +0000
229+++ provider/local/storage/worker.go 2013-09-19 03:28:43 +0000
230@@ -41,7 +41,7 @@
231 storageAddr := s.config.Value(agent.StorageAddr)
232 logger.Infof("serving %s on %s", storageDir, storageAddr)
233
234- storage, err := filestorage.NewFileStorageWriter(storageDir)
235+ storage, err := filestorage.NewFileStorageWriter(storageDir, filestorage.UseDefaultTmpDir)
236 if err != nil {
237 logger.Errorf("error with local storage: %v", err)
238 return err
239@@ -57,7 +57,7 @@
240 sharedStorageAddr := s.config.Value(agent.SharedStorageAddr)
241 logger.Infof("serving %s on %s", sharedStorageDir, sharedStorageAddr)
242
243- sharedStorage, err := filestorage.NewFileStorageWriter(sharedStorageDir)
244+ sharedStorage, err := filestorage.NewFileStorageWriter(sharedStorageDir, filestorage.UseDefaultTmpDir)
245 if err != nil {
246 logger.Errorf("error with local storage: %v", err)
247 return err

Subscribers

People subscribed via source and target branches

to status/vote changes: