Introduced a path level lock to synchronize updates
to /etc/hosts writes. A path level cache is maintained
to only synchronize only at the file level.
Signed-off-by: Jana Radhakrishnan <mrjana@docker.com>
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
"io/ioutil" |
| 8 | 8 |
"os" |
| 9 | 9 |
"regexp" |
| 10 |
+ "sync" |
|
| 10 | 11 |
) |
| 11 | 12 |
|
| 12 | 13 |
// Record Structure for a single host record |
| ... | ... |
@@ -21,14 +22,47 @@ func (r Record) WriteTo(w io.Writer) (int64, error) {
|
| 21 | 21 |
return int64(n), err |
| 22 | 22 |
} |
| 23 | 23 |
|
| 24 |
-// Default hosts config records slice |
|
| 25 |
-var defaultContent = []Record{
|
|
| 26 |
- {Hosts: "localhost", IP: "127.0.0.1"},
|
|
| 27 |
- {Hosts: "localhost ip6-localhost ip6-loopback", IP: "::1"},
|
|
| 28 |
- {Hosts: "ip6-localnet", IP: "fe00::0"},
|
|
| 29 |
- {Hosts: "ip6-mcastprefix", IP: "ff00::0"},
|
|
| 30 |
- {Hosts: "ip6-allnodes", IP: "ff02::1"},
|
|
| 31 |
- {Hosts: "ip6-allrouters", IP: "ff02::2"},
|
|
| 24 |
+var ( |
|
| 25 |
+ // Default hosts config records slice |
|
| 26 |
+ defaultContent = []Record{
|
|
| 27 |
+ {Hosts: "localhost", IP: "127.0.0.1"},
|
|
| 28 |
+ {Hosts: "localhost ip6-localhost ip6-loopback", IP: "::1"},
|
|
| 29 |
+ {Hosts: "ip6-localnet", IP: "fe00::0"},
|
|
| 30 |
+ {Hosts: "ip6-mcastprefix", IP: "ff00::0"},
|
|
| 31 |
+ {Hosts: "ip6-allnodes", IP: "ff02::1"},
|
|
| 32 |
+ {Hosts: "ip6-allrouters", IP: "ff02::2"},
|
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ // A cache of path level locks for synchronizing /etc/hosts |
|
| 36 |
+ // updates on a file level |
|
| 37 |
+ pathMap = make(map[string]*sync.Mutex) |
|
| 38 |
+ |
|
| 39 |
+ // A package level mutex to synchronize the cache itself |
|
| 40 |
+ pathMutex sync.Mutex |
|
| 41 |
+) |
|
| 42 |
+ |
|
| 43 |
+func pathLock(path string) func() {
|
|
| 44 |
+ pathMutex.Lock() |
|
| 45 |
+ defer pathMutex.Unlock() |
|
| 46 |
+ |
|
| 47 |
+ pl, ok := pathMap[path] |
|
| 48 |
+ if !ok {
|
|
| 49 |
+ pl = &sync.Mutex{}
|
|
| 50 |
+ pathMap[path] = pl |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ pl.Lock() |
|
| 54 |
+ return func() {
|
|
| 55 |
+ pl.Unlock() |
|
| 56 |
+ } |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+// Drop drops the path string from the path cache |
|
| 60 |
+func Drop(path string) {
|
|
| 61 |
+ pathMutex.Lock() |
|
| 62 |
+ defer pathMutex.Unlock() |
|
| 63 |
+ |
|
| 64 |
+ delete(pathMap, path) |
|
| 32 | 65 |
} |
| 33 | 66 |
|
| 34 | 67 |
// Build function |
| ... | ... |
@@ -36,6 +70,8 @@ var defaultContent = []Record{
|
| 36 | 36 |
// IP, hostname, and domainname set main record leave empty for no master record |
| 37 | 37 |
// extraContent is an array of extra host records. |
| 38 | 38 |
func Build(path, IP, hostname, domainname string, extraContent []Record) error {
|
| 39 |
+ defer pathLock(path)() |
|
| 40 |
+ |
|
| 39 | 41 |
content := bytes.NewBuffer(nil) |
| 40 | 42 |
if IP != "" {
|
| 41 | 43 |
//set main record |
| ... | ... |
@@ -68,6 +104,8 @@ func Build(path, IP, hostname, domainname string, extraContent []Record) error {
|
| 68 | 68 |
|
| 69 | 69 |
// Add adds an arbitrary number of Records to an already existing /etc/hosts file |
| 70 | 70 |
func Add(path string, recs []Record) error {
|
| 71 |
+ defer pathLock(path)() |
|
| 72 |
+ |
|
| 71 | 73 |
if len(recs) == 0 {
|
| 72 | 74 |
return nil |
| 73 | 75 |
} |
| ... | ... |
@@ -95,6 +133,8 @@ func Add(path string, recs []Record) error {
|
| 95 | 95 |
|
| 96 | 96 |
// Delete deletes an arbitrary number of Records already existing in /etc/hosts file |
| 97 | 97 |
func Delete(path string, recs []Record) error {
|
| 98 |
+ defer pathLock(path)() |
|
| 99 |
+ |
|
| 98 | 100 |
if len(recs) == 0 {
|
| 99 | 101 |
return nil |
| 100 | 102 |
} |
| ... | ... |
@@ -118,6 +158,8 @@ func Delete(path string, recs []Record) error {
|
| 118 | 118 |
// IP is new IP address |
| 119 | 119 |
// hostname is hostname to search for to replace IP |
| 120 | 120 |
func Update(path, IP, hostname string) error {
|
| 121 |
+ defer pathLock(path)() |
|
| 122 |
+ |
|
| 121 | 123 |
old, err := ioutil.ReadFile(path) |
| 122 | 124 |
if err != nil {
|
| 123 | 125 |
return err |
| ... | ... |
@@ -2,8 +2,10 @@ package etchosts |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"bytes" |
| 5 |
+ "fmt" |
|
| 5 | 6 |
"io/ioutil" |
| 6 | 7 |
"os" |
| 8 |
+ "sync" |
|
| 7 | 9 |
"testing" |
| 8 | 10 |
|
| 9 | 11 |
_ "github.com/docker/libnetwork/testutils" |
| ... | ... |
@@ -247,3 +249,61 @@ func TestDelete(t *testing.T) {
|
| 247 | 247 |
t.Fatalf("Did not expect to find '%s' got '%s'", expected, content)
|
| 248 | 248 |
} |
| 249 | 249 |
} |
| 250 |
+ |
|
| 251 |
+func TestConcurrentWrites(t *testing.T) {
|
|
| 252 |
+ file, err := ioutil.TempFile("", "")
|
|
| 253 |
+ if err != nil {
|
|
| 254 |
+ t.Fatal(err) |
|
| 255 |
+ } |
|
| 256 |
+ defer os.Remove(file.Name()) |
|
| 257 |
+ |
|
| 258 |
+ err = Build(file.Name(), "", "", "", nil) |
|
| 259 |
+ if err != nil {
|
|
| 260 |
+ t.Fatal(err) |
|
| 261 |
+ } |
|
| 262 |
+ |
|
| 263 |
+ if err := Add(file.Name(), []Record{
|
|
| 264 |
+ Record{
|
|
| 265 |
+ Hosts: "inithostname", |
|
| 266 |
+ IP: "172.17.0.1", |
|
| 267 |
+ }, |
|
| 268 |
+ }); err != nil {
|
|
| 269 |
+ t.Fatal(err) |
|
| 270 |
+ } |
|
| 271 |
+ |
|
| 272 |
+ var wg sync.WaitGroup |
|
| 273 |
+ for i := 0; i < 10; i++ {
|
|
| 274 |
+ wg.Add(1) |
|
| 275 |
+ go func() {
|
|
| 276 |
+ defer wg.Done() |
|
| 277 |
+ |
|
| 278 |
+ rec := []Record{
|
|
| 279 |
+ Record{
|
|
| 280 |
+ IP: fmt.Sprintf("%d.%d.%d.%d", i, i, i, i),
|
|
| 281 |
+ Hosts: fmt.Sprintf("testhostname%d", i),
|
|
| 282 |
+ }, |
|
| 283 |
+ } |
|
| 284 |
+ |
|
| 285 |
+ for j := 0; j < 25; j++ {
|
|
| 286 |
+ if err := Add(file.Name(), rec); err != nil {
|
|
| 287 |
+ t.Fatal(err) |
|
| 288 |
+ } |
|
| 289 |
+ |
|
| 290 |
+ if err := Delete(file.Name(), rec); err != nil {
|
|
| 291 |
+ t.Fatal(err) |
|
| 292 |
+ } |
|
| 293 |
+ } |
|
| 294 |
+ }() |
|
| 295 |
+ } |
|
| 296 |
+ |
|
| 297 |
+ wg.Wait() |
|
| 298 |
+ |
|
| 299 |
+ content, err := ioutil.ReadFile(file.Name()) |
|
| 300 |
+ if err != nil {
|
|
| 301 |
+ t.Fatal(err) |
|
| 302 |
+ } |
|
| 303 |
+ |
|
| 304 |
+ if expected := "172.17.0.1\tinithostname\n"; !bytes.Contains(content, []byte(expected)) {
|
|
| 305 |
+ t.Fatalf("Expected to find '%s' got '%s'", expected, content)
|
|
| 306 |
+ } |
|
| 307 |
+} |
| ... | ... |
@@ -182,6 +182,10 @@ func (sb *sandbox) Delete() error {
|
| 182 | 182 |
} |
| 183 | 183 |
} |
| 184 | 184 |
|
| 185 |
+ // Container is going away. Path cache in etchosts is most |
|
| 186 |
+ // likely not required any more. Drop it. |
|
| 187 |
+ etchosts.Drop(sb.config.hostsPath) |
|
| 188 |
+ |
|
| 185 | 189 |
if sb.osSbox != nil {
|
| 186 | 190 |
sb.osSbox.Destroy() |
| 187 | 191 |
} |