progressreader.Broadcaster becomes broadcaster.Buffered and
broadcastwriter.Writer becomes broadcaster.Unbuffered.
The package broadcastwriter is thus renamed to broadcaster.
Signed-off-by: Tibor Vass <tibor@docker.com>
| ... | ... |
@@ -22,7 +22,7 @@ import ( |
| 22 | 22 |
derr "github.com/docker/docker/errors" |
| 23 | 23 |
"github.com/docker/docker/image" |
| 24 | 24 |
"github.com/docker/docker/pkg/archive" |
| 25 |
- "github.com/docker/docker/pkg/broadcastwriter" |
|
| 25 |
+ "github.com/docker/docker/pkg/broadcaster" |
|
| 26 | 26 |
"github.com/docker/docker/pkg/fileutils" |
| 27 | 27 |
"github.com/docker/docker/pkg/ioutils" |
| 28 | 28 |
"github.com/docker/docker/pkg/mount" |
| ... | ... |
@@ -41,8 +41,8 @@ var ( |
| 41 | 41 |
) |
| 42 | 42 |
|
| 43 | 43 |
type streamConfig struct {
|
| 44 |
- stdout *broadcastwriter.BroadcastWriter |
|
| 45 |
- stderr *broadcastwriter.BroadcastWriter |
|
| 44 |
+ stdout *broadcaster.Unbuffered |
|
| 45 |
+ stderr *broadcaster.Unbuffered |
|
| 46 | 46 |
stdin io.ReadCloser |
| 47 | 47 |
stdinPipe io.WriteCloser |
| 48 | 48 |
} |
| ... | ... |
@@ -318,13 +318,13 @@ func (streamConfig *streamConfig) StdinPipe() io.WriteCloser {
|
| 318 | 318 |
|
| 319 | 319 |
func (streamConfig *streamConfig) StdoutPipe() io.ReadCloser {
|
| 320 | 320 |
reader, writer := io.Pipe() |
| 321 |
- streamConfig.stdout.AddWriter(writer) |
|
| 321 |
+ streamConfig.stdout.Add(writer) |
|
| 322 | 322 |
return ioutils.NewBufReader(reader) |
| 323 | 323 |
} |
| 324 | 324 |
|
| 325 | 325 |
func (streamConfig *streamConfig) StderrPipe() io.ReadCloser {
|
| 326 | 326 |
reader, writer := io.Pipe() |
| 327 |
- streamConfig.stderr.AddWriter(writer) |
|
| 327 |
+ streamConfig.stderr.Add(writer) |
|
| 328 | 328 |
return ioutils.NewBufReader(reader) |
| 329 | 329 |
} |
| 330 | 330 |
|
| ... | ... |
@@ -32,7 +32,7 @@ import ( |
| 32 | 32 |
"github.com/docker/docker/graph" |
| 33 | 33 |
"github.com/docker/docker/image" |
| 34 | 34 |
"github.com/docker/docker/pkg/archive" |
| 35 |
- "github.com/docker/docker/pkg/broadcastwriter" |
|
| 35 |
+ "github.com/docker/docker/pkg/broadcaster" |
|
| 36 | 36 |
"github.com/docker/docker/pkg/discovery" |
| 37 | 37 |
"github.com/docker/docker/pkg/fileutils" |
| 38 | 38 |
"github.com/docker/docker/pkg/graphdb" |
| ... | ... |
@@ -194,8 +194,8 @@ func (daemon *Daemon) Register(container *Container) error {
|
| 194 | 194 |
container.daemon = daemon |
| 195 | 195 |
|
| 196 | 196 |
// Attach to stdout and stderr |
| 197 |
- container.stderr = broadcastwriter.New() |
|
| 198 |
- container.stdout = broadcastwriter.New() |
|
| 197 |
+ container.stderr = new(broadcaster.Unbuffered) |
|
| 198 |
+ container.stdout = new(broadcaster.Unbuffered) |
|
| 199 | 199 |
// Attach to stdin |
| 200 | 200 |
if container.Config.OpenStdin {
|
| 201 | 201 |
container.stdin, container.stdinPipe = io.Pipe() |
| ... | ... |
@@ -10,7 +10,7 @@ import ( |
| 10 | 10 |
"github.com/Sirupsen/logrus" |
| 11 | 11 |
"github.com/docker/docker/daemon/execdriver" |
| 12 | 12 |
derr "github.com/docker/docker/errors" |
| 13 |
- "github.com/docker/docker/pkg/broadcastwriter" |
|
| 13 |
+ "github.com/docker/docker/pkg/broadcaster" |
|
| 14 | 14 |
"github.com/docker/docker/pkg/ioutils" |
| 15 | 15 |
"github.com/docker/docker/pkg/pools" |
| 16 | 16 |
"github.com/docker/docker/pkg/stringid" |
| ... | ... |
@@ -233,8 +233,8 @@ func (d *Daemon) ContainerExecStart(name string, stdin io.ReadCloser, stdout io. |
| 233 | 233 |
cStderr = stderr |
| 234 | 234 |
} |
| 235 | 235 |
|
| 236 |
- ec.streamConfig.stderr = broadcastwriter.New() |
|
| 237 |
- ec.streamConfig.stdout = broadcastwriter.New() |
|
| 236 |
+ ec.streamConfig.stderr = new(broadcaster.Unbuffered) |
|
| 237 |
+ ec.streamConfig.stdout = new(broadcaster.Unbuffered) |
|
| 238 | 238 |
// Attach to stdin |
| 239 | 239 |
if ec.OpenStdin {
|
| 240 | 240 |
ec.streamConfig.stdin, ec.streamConfig.stdinPipe = io.Pipe() |
| ... | ... |
@@ -3,7 +3,7 @@ package graph |
| 3 | 3 |
import ( |
| 4 | 4 |
"testing" |
| 5 | 5 |
|
| 6 |
- "github.com/docker/docker/pkg/progressreader" |
|
| 6 |
+ "github.com/docker/docker/pkg/broadcaster" |
|
| 7 | 7 |
"github.com/docker/docker/pkg/reexec" |
| 8 | 8 |
) |
| 9 | 9 |
|
| ... | ... |
@@ -13,8 +13,8 @@ func init() {
|
| 13 | 13 |
|
| 14 | 14 |
func TestPools(t *testing.T) {
|
| 15 | 15 |
s := &TagStore{
|
| 16 |
- pullingPool: make(map[string]*progressreader.Broadcaster), |
|
| 17 |
- pushingPool: make(map[string]*progressreader.Broadcaster), |
|
| 16 |
+ pullingPool: make(map[string]*broadcaster.Buffered), |
|
| 17 |
+ pushingPool: make(map[string]*broadcaster.Buffered), |
|
| 18 | 18 |
} |
| 19 | 19 |
|
| 20 | 20 |
if _, found := s.poolAdd("pull", "test1"); found {
|
| ... | ... |
@@ -11,6 +11,7 @@ import ( |
| 11 | 11 |
"github.com/docker/distribution/digest" |
| 12 | 12 |
"github.com/docker/distribution/manifest" |
| 13 | 13 |
"github.com/docker/docker/image" |
| 14 |
+ "github.com/docker/docker/pkg/broadcaster" |
|
| 14 | 15 |
"github.com/docker/docker/pkg/progressreader" |
| 15 | 16 |
"github.com/docker/docker/pkg/streamformatter" |
| 16 | 17 |
"github.com/docker/docker/pkg/stringid" |
| ... | ... |
@@ -110,7 +111,7 @@ type downloadInfo struct {
|
| 110 | 110 |
size int64 |
| 111 | 111 |
err chan error |
| 112 | 112 |
poolKey string |
| 113 |
- broadcaster *progressreader.Broadcaster |
|
| 113 |
+ broadcaster *broadcaster.Buffered |
|
| 114 | 114 |
} |
| 115 | 115 |
|
| 116 | 116 |
type errVerification struct{}
|
| ... | ... |
@@ -16,8 +16,8 @@ import ( |
| 16 | 16 |
"github.com/docker/docker/daemon/events" |
| 17 | 17 |
"github.com/docker/docker/graph/tags" |
| 18 | 18 |
"github.com/docker/docker/image" |
| 19 |
+ "github.com/docker/docker/pkg/broadcaster" |
|
| 19 | 20 |
"github.com/docker/docker/pkg/parsers" |
| 20 |
- "github.com/docker/docker/pkg/progressreader" |
|
| 21 | 21 |
"github.com/docker/docker/pkg/stringid" |
| 22 | 22 |
"github.com/docker/docker/registry" |
| 23 | 23 |
"github.com/docker/docker/trust" |
| ... | ... |
@@ -37,8 +37,8 @@ type TagStore struct {
|
| 37 | 37 |
sync.Mutex |
| 38 | 38 |
// FIXME: move push/pull-related fields |
| 39 | 39 |
// to a helper type |
| 40 |
- pullingPool map[string]*progressreader.Broadcaster |
|
| 41 |
- pushingPool map[string]*progressreader.Broadcaster |
|
| 40 |
+ pullingPool map[string]*broadcaster.Buffered |
|
| 41 |
+ pushingPool map[string]*broadcaster.Buffered |
|
| 42 | 42 |
registryService *registry.Service |
| 43 | 43 |
eventsService *events.Events |
| 44 | 44 |
trustService *trust.Store |
| ... | ... |
@@ -94,8 +94,8 @@ func NewTagStore(path string, cfg *TagStoreConfig) (*TagStore, error) {
|
| 94 | 94 |
graph: cfg.Graph, |
| 95 | 95 |
trustKey: cfg.Key, |
| 96 | 96 |
Repositories: make(map[string]Repository), |
| 97 |
- pullingPool: make(map[string]*progressreader.Broadcaster), |
|
| 98 |
- pushingPool: make(map[string]*progressreader.Broadcaster), |
|
| 97 |
+ pullingPool: make(map[string]*broadcaster.Buffered), |
|
| 98 |
+ pushingPool: make(map[string]*broadcaster.Buffered), |
|
| 99 | 99 |
registryService: cfg.Registry, |
| 100 | 100 |
eventsService: cfg.Events, |
| 101 | 101 |
trustService: cfg.Trust, |
| ... | ... |
@@ -437,7 +437,7 @@ func validateDigest(dgst string) error {
|
| 437 | 437 |
// poolAdd checks if a push or pull is already running, and returns |
| 438 | 438 |
// (broadcaster, true) if a running operation is found. Otherwise, it creates a |
| 439 | 439 |
// new one and returns (broadcaster, false). |
| 440 |
-func (store *TagStore) poolAdd(kind, key string) (*progressreader.Broadcaster, bool) {
|
|
| 440 |
+func (store *TagStore) poolAdd(kind, key string) (*broadcaster.Buffered, bool) {
|
|
| 441 | 441 |
store.Lock() |
| 442 | 442 |
defer store.Unlock() |
| 443 | 443 |
|
| ... | ... |
@@ -448,7 +448,7 @@ func (store *TagStore) poolAdd(kind, key string) (*progressreader.Broadcaster, b |
| 448 | 448 |
return p, true |
| 449 | 449 |
} |
| 450 | 450 |
|
| 451 |
- broadcaster := progressreader.NewBroadcaster() |
|
| 451 |
+ broadcaster := broadcaster.NewBuffered() |
|
| 452 | 452 |
|
| 453 | 453 |
switch kind {
|
| 454 | 454 |
case "pull": |
| 455 | 455 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,167 @@ |
| 0 |
+package broadcaster |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "errors" |
|
| 4 |
+ "io" |
|
| 5 |
+ "sync" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+// Buffered keeps track of one or more observers watching the progress |
|
| 9 |
+// of an operation. For example, if multiple clients are trying to pull an |
|
| 10 |
+// image, they share a Buffered struct for the download operation. |
|
| 11 |
+type Buffered struct {
|
|
| 12 |
+ sync.Mutex |
|
| 13 |
+ // c is a channel that observers block on, waiting for the operation |
|
| 14 |
+ // to finish. |
|
| 15 |
+ c chan struct{}
|
|
| 16 |
+ // cond is a condition variable used to wake up observers when there's |
|
| 17 |
+ // new data available. |
|
| 18 |
+ cond *sync.Cond |
|
| 19 |
+ // history is a buffer of the progress output so far, so a new observer |
|
| 20 |
+ // can catch up. The history is stored as a slice of separate byte |
|
| 21 |
+ // slices, so that if the writer is a WriteFlusher, the flushes will |
|
| 22 |
+ // happen in the right places. |
|
| 23 |
+ history [][]byte |
|
| 24 |
+ // wg is a WaitGroup used to wait for all writes to finish on Close |
|
| 25 |
+ wg sync.WaitGroup |
|
| 26 |
+ // result is the argument passed to the first call of Close, and |
|
| 27 |
+ // returned to callers of Wait |
|
| 28 |
+ result error |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// NewBuffered returns an initialized Buffered structure. |
|
| 32 |
+func NewBuffered() *Buffered {
|
|
| 33 |
+ b := &Buffered{
|
|
| 34 |
+ c: make(chan struct{}),
|
|
| 35 |
+ } |
|
| 36 |
+ b.cond = sync.NewCond(b) |
|
| 37 |
+ return b |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// closed returns true if and only if the broadcaster has been closed |
|
| 41 |
+func (broadcaster *Buffered) closed() bool {
|
|
| 42 |
+ select {
|
|
| 43 |
+ case <-broadcaster.c: |
|
| 44 |
+ return true |
|
| 45 |
+ default: |
|
| 46 |
+ return false |
|
| 47 |
+ } |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// receiveWrites runs as a goroutine so that writes don't block the Write |
|
| 51 |
+// function. It writes the new data in broadcaster.history each time there's |
|
| 52 |
+// activity on the broadcaster.cond condition variable. |
|
| 53 |
+func (broadcaster *Buffered) receiveWrites(observer io.Writer) {
|
|
| 54 |
+ n := 0 |
|
| 55 |
+ |
|
| 56 |
+ broadcaster.Lock() |
|
| 57 |
+ |
|
| 58 |
+ // The condition variable wait is at the end of this loop, so that the |
|
| 59 |
+ // first iteration will write the history so far. |
|
| 60 |
+ for {
|
|
| 61 |
+ newData := broadcaster.history[n:] |
|
| 62 |
+ // Make a copy of newData so we can release the lock |
|
| 63 |
+ sendData := make([][]byte, len(newData), len(newData)) |
|
| 64 |
+ copy(sendData, newData) |
|
| 65 |
+ broadcaster.Unlock() |
|
| 66 |
+ |
|
| 67 |
+ for len(sendData) > 0 {
|
|
| 68 |
+ _, err := observer.Write(sendData[0]) |
|
| 69 |
+ if err != nil {
|
|
| 70 |
+ broadcaster.wg.Done() |
|
| 71 |
+ return |
|
| 72 |
+ } |
|
| 73 |
+ n++ |
|
| 74 |
+ sendData = sendData[1:] |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ broadcaster.Lock() |
|
| 78 |
+ |
|
| 79 |
+ // If we are behind, we need to catch up instead of waiting |
|
| 80 |
+ // or handling a closure. |
|
| 81 |
+ if len(broadcaster.history) != n {
|
|
| 82 |
+ continue |
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ // detect closure of the broadcast writer |
|
| 86 |
+ if broadcaster.closed() {
|
|
| 87 |
+ broadcaster.Unlock() |
|
| 88 |
+ broadcaster.wg.Done() |
|
| 89 |
+ return |
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 92 |
+ broadcaster.cond.Wait() |
|
| 93 |
+ |
|
| 94 |
+ // Mutex is still locked as the loop continues |
|
| 95 |
+ } |
|
| 96 |
+} |
|
| 97 |
+ |
|
| 98 |
+// Write adds data to the history buffer, and also writes it to all current |
|
| 99 |
+// observers. |
|
| 100 |
+func (broadcaster *Buffered) Write(p []byte) (n int, err error) {
|
|
| 101 |
+ broadcaster.Lock() |
|
| 102 |
+ defer broadcaster.Unlock() |
|
| 103 |
+ |
|
| 104 |
+ // Is the broadcaster closed? If so, the write should fail. |
|
| 105 |
+ if broadcaster.closed() {
|
|
| 106 |
+ return 0, errors.New("attempted write to a closed broadcaster.Buffered")
|
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ // Add message in p to the history slice |
|
| 110 |
+ newEntry := make([]byte, len(p), len(p)) |
|
| 111 |
+ copy(newEntry, p) |
|
| 112 |
+ broadcaster.history = append(broadcaster.history, newEntry) |
|
| 113 |
+ |
|
| 114 |
+ broadcaster.cond.Broadcast() |
|
| 115 |
+ |
|
| 116 |
+ return len(p), nil |
|
| 117 |
+} |
|
| 118 |
+ |
|
| 119 |
+// Add adds an observer to the broadcaster. The new observer receives the |
|
| 120 |
+// data from the history buffer, and also all subsequent data. |
|
| 121 |
+func (broadcaster *Buffered) Add(w io.Writer) error {
|
|
| 122 |
+ // The lock is acquired here so that Add can't race with Close |
|
| 123 |
+ broadcaster.Lock() |
|
| 124 |
+ defer broadcaster.Unlock() |
|
| 125 |
+ |
|
| 126 |
+ if broadcaster.closed() {
|
|
| 127 |
+ return errors.New("attempted to add observer to a closed broadcaster.Buffered")
|
|
| 128 |
+ } |
|
| 129 |
+ |
|
| 130 |
+ broadcaster.wg.Add(1) |
|
| 131 |
+ go broadcaster.receiveWrites(w) |
|
| 132 |
+ |
|
| 133 |
+ return nil |
|
| 134 |
+} |
|
| 135 |
+ |
|
| 136 |
+// CloseWithError signals to all observers that the operation has finished. Its |
|
| 137 |
+// argument is a result that should be returned to waiters blocking on Wait. |
|
| 138 |
+func (broadcaster *Buffered) CloseWithError(result error) {
|
|
| 139 |
+ broadcaster.Lock() |
|
| 140 |
+ if broadcaster.closed() {
|
|
| 141 |
+ broadcaster.Unlock() |
|
| 142 |
+ return |
|
| 143 |
+ } |
|
| 144 |
+ broadcaster.result = result |
|
| 145 |
+ close(broadcaster.c) |
|
| 146 |
+ broadcaster.cond.Broadcast() |
|
| 147 |
+ broadcaster.Unlock() |
|
| 148 |
+ |
|
| 149 |
+ // Don't return until all writers have caught up. |
|
| 150 |
+ broadcaster.wg.Wait() |
|
| 151 |
+} |
|
| 152 |
+ |
|
| 153 |
+// Close signals to all observers that the operation has finished. It causes |
|
| 154 |
+// all calls to Wait to return nil. |
|
| 155 |
+func (broadcaster *Buffered) Close() {
|
|
| 156 |
+ broadcaster.CloseWithError(nil) |
|
| 157 |
+} |
|
| 158 |
+ |
|
| 159 |
+// Wait blocks until the operation is marked as completed by the Close method, |
|
| 160 |
+// and all writer goroutines have completed. It returns the argument that was |
|
| 161 |
+// passed to Close. |
|
| 162 |
+func (broadcaster *Buffered) Wait() error {
|
|
| 163 |
+ <-broadcaster.c |
|
| 164 |
+ broadcaster.wg.Wait() |
|
| 165 |
+ return broadcaster.result |
|
| 166 |
+} |
| 0 | 167 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,49 @@ |
| 0 |
+package broadcaster |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io" |
|
| 4 |
+ "sync" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+// Unbuffered accumulates multiple io.WriteCloser by stream. |
|
| 8 |
+type Unbuffered struct {
|
|
| 9 |
+ mu sync.Mutex |
|
| 10 |
+ writers []io.WriteCloser |
|
| 11 |
+} |
|
| 12 |
+ |
|
| 13 |
+// Add adds new io.WriteCloser. |
|
| 14 |
+func (w *Unbuffered) Add(writer io.WriteCloser) {
|
|
| 15 |
+ w.mu.Lock() |
|
| 16 |
+ w.writers = append(w.writers, writer) |
|
| 17 |
+ w.mu.Unlock() |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+// Write writes bytes to all writers. Failed writers will be evicted during |
|
| 21 |
+// this call. |
|
| 22 |
+func (w *Unbuffered) Write(p []byte) (n int, err error) {
|
|
| 23 |
+ w.mu.Lock() |
|
| 24 |
+ var evict []int |
|
| 25 |
+ for i, sw := range w.writers {
|
|
| 26 |
+ if n, err := sw.Write(p); err != nil || n != len(p) {
|
|
| 27 |
+ // On error, evict the writer |
|
| 28 |
+ evict = append(evict, i) |
|
| 29 |
+ } |
|
| 30 |
+ } |
|
| 31 |
+ for n, i := range evict {
|
|
| 32 |
+ w.writers = append(w.writers[:i-n], w.writers[i-n+1:]...) |
|
| 33 |
+ } |
|
| 34 |
+ w.mu.Unlock() |
|
| 35 |
+ return len(p), nil |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+// Clean closes and removes all writers. Last non-eol-terminated part of data |
|
| 39 |
+// will be saved. |
|
| 40 |
+func (w *Unbuffered) Clean() error {
|
|
| 41 |
+ w.mu.Lock() |
|
| 42 |
+ for _, sw := range w.writers {
|
|
| 43 |
+ sw.Close() |
|
| 44 |
+ } |
|
| 45 |
+ w.writers = nil |
|
| 46 |
+ w.mu.Unlock() |
|
| 47 |
+ return nil |
|
| 48 |
+} |
| 0 | 49 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,162 @@ |
| 0 |
+package broadcaster |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "strings" |
|
| 6 |
+ |
|
| 7 |
+ "testing" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+type dummyWriter struct {
|
|
| 11 |
+ buffer bytes.Buffer |
|
| 12 |
+ failOnWrite bool |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+func (dw *dummyWriter) Write(p []byte) (n int, err error) {
|
|
| 16 |
+ if dw.failOnWrite {
|
|
| 17 |
+ return 0, errors.New("Fake fail")
|
|
| 18 |
+ } |
|
| 19 |
+ return dw.buffer.Write(p) |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+func (dw *dummyWriter) String() string {
|
|
| 23 |
+ return dw.buffer.String() |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+func (dw *dummyWriter) Close() error {
|
|
| 27 |
+ return nil |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+func TestUnbuffered(t *testing.T) {
|
|
| 31 |
+ writer := new(Unbuffered) |
|
| 32 |
+ |
|
| 33 |
+ // Test 1: Both bufferA and bufferB should contain "foo" |
|
| 34 |
+ bufferA := &dummyWriter{}
|
|
| 35 |
+ writer.Add(bufferA) |
|
| 36 |
+ bufferB := &dummyWriter{}
|
|
| 37 |
+ writer.Add(bufferB) |
|
| 38 |
+ writer.Write([]byte("foo"))
|
|
| 39 |
+ |
|
| 40 |
+ if bufferA.String() != "foo" {
|
|
| 41 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ if bufferB.String() != "foo" {
|
|
| 45 |
+ t.Errorf("Buffer contains %v", bufferB.String())
|
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ // Test2: bufferA and bufferB should contain "foobar", |
|
| 49 |
+ // while bufferC should only contain "bar" |
|
| 50 |
+ bufferC := &dummyWriter{}
|
|
| 51 |
+ writer.Add(bufferC) |
|
| 52 |
+ writer.Write([]byte("bar"))
|
|
| 53 |
+ |
|
| 54 |
+ if bufferA.String() != "foobar" {
|
|
| 55 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ if bufferB.String() != "foobar" {
|
|
| 59 |
+ t.Errorf("Buffer contains %v", bufferB.String())
|
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ if bufferC.String() != "bar" {
|
|
| 63 |
+ t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ // Test3: Test eviction on failure |
|
| 67 |
+ bufferA.failOnWrite = true |
|
| 68 |
+ writer.Write([]byte("fail"))
|
|
| 69 |
+ if bufferA.String() != "foobar" {
|
|
| 70 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 71 |
+ } |
|
| 72 |
+ if bufferC.String() != "barfail" {
|
|
| 73 |
+ t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 74 |
+ } |
|
| 75 |
+ // Even though we reset the flag, no more writes should go in there |
|
| 76 |
+ bufferA.failOnWrite = false |
|
| 77 |
+ writer.Write([]byte("test"))
|
|
| 78 |
+ if bufferA.String() != "foobar" {
|
|
| 79 |
+ t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 80 |
+ } |
|
| 81 |
+ if bufferC.String() != "barfailtest" {
|
|
| 82 |
+ t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ // Test4: Test eviction on multiple simultaneous failures |
|
| 86 |
+ bufferB.failOnWrite = true |
|
| 87 |
+ bufferC.failOnWrite = true |
|
| 88 |
+ bufferD := &dummyWriter{}
|
|
| 89 |
+ writer.Add(bufferD) |
|
| 90 |
+ writer.Write([]byte("yo"))
|
|
| 91 |
+ writer.Write([]byte("ink"))
|
|
| 92 |
+ if strings.Contains(bufferB.String(), "yoink") {
|
|
| 93 |
+ t.Errorf("bufferB received write. contents: %q", bufferB)
|
|
| 94 |
+ } |
|
| 95 |
+ if strings.Contains(bufferC.String(), "yoink") {
|
|
| 96 |
+ t.Errorf("bufferC received write. contents: %q", bufferC)
|
|
| 97 |
+ } |
|
| 98 |
+ if g, w := bufferD.String(), "yoink"; g != w {
|
|
| 99 |
+ t.Errorf("bufferD = %q, want %q", g, w)
|
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ writer.Clean() |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+type devNullCloser int |
|
| 106 |
+ |
|
| 107 |
+func (d devNullCloser) Close() error {
|
|
| 108 |
+ return nil |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 111 |
+func (d devNullCloser) Write(buf []byte) (int, error) {
|
|
| 112 |
+ return len(buf), nil |
|
| 113 |
+} |
|
| 114 |
+ |
|
| 115 |
+// This test checks for races. It is only useful when run with the race detector. |
|
| 116 |
+func TestRaceUnbuffered(t *testing.T) {
|
|
| 117 |
+ writer := new(Unbuffered) |
|
| 118 |
+ c := make(chan bool) |
|
| 119 |
+ go func() {
|
|
| 120 |
+ writer.Add(devNullCloser(0)) |
|
| 121 |
+ c <- true |
|
| 122 |
+ }() |
|
| 123 |
+ writer.Write([]byte("hello"))
|
|
| 124 |
+ <-c |
|
| 125 |
+} |
|
| 126 |
+ |
|
| 127 |
+func BenchmarkUnbuffered(b *testing.B) {
|
|
| 128 |
+ writer := new(Unbuffered) |
|
| 129 |
+ setUpWriter := func() {
|
|
| 130 |
+ for i := 0; i < 100; i++ {
|
|
| 131 |
+ writer.Add(devNullCloser(0)) |
|
| 132 |
+ writer.Add(devNullCloser(0)) |
|
| 133 |
+ writer.Add(devNullCloser(0)) |
|
| 134 |
+ } |
|
| 135 |
+ } |
|
| 136 |
+ testLine := "Line that thinks that it is log line from docker" |
|
| 137 |
+ var buf bytes.Buffer |
|
| 138 |
+ for i := 0; i < 100; i++ {
|
|
| 139 |
+ buf.Write([]byte(testLine + "\n")) |
|
| 140 |
+ } |
|
| 141 |
+ // line without eol |
|
| 142 |
+ buf.Write([]byte(testLine)) |
|
| 143 |
+ testText := buf.Bytes() |
|
| 144 |
+ b.SetBytes(int64(5 * len(testText))) |
|
| 145 |
+ b.ResetTimer() |
|
| 146 |
+ for i := 0; i < b.N; i++ {
|
|
| 147 |
+ b.StopTimer() |
|
| 148 |
+ setUpWriter() |
|
| 149 |
+ b.StartTimer() |
|
| 150 |
+ |
|
| 151 |
+ for j := 0; j < 5; j++ {
|
|
| 152 |
+ if _, err := writer.Write(testText); err != nil {
|
|
| 153 |
+ b.Fatal(err) |
|
| 154 |
+ } |
|
| 155 |
+ } |
|
| 156 |
+ |
|
| 157 |
+ b.StopTimer() |
|
| 158 |
+ writer.Clean() |
|
| 159 |
+ b.StartTimer() |
|
| 160 |
+ } |
|
| 161 |
+} |
| 0 | 162 |
deleted file mode 100644 |
| ... | ... |
@@ -1,54 +0,0 @@ |
| 1 |
-package broadcastwriter |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "io" |
|
| 5 |
- "sync" |
|
| 6 |
-) |
|
| 7 |
- |
|
| 8 |
-// BroadcastWriter accumulate multiple io.WriteCloser by stream. |
|
| 9 |
-type BroadcastWriter struct {
|
|
| 10 |
- mu sync.Mutex |
|
| 11 |
- writers []io.WriteCloser |
|
| 12 |
-} |
|
| 13 |
- |
|
| 14 |
-// AddWriter adds new io.WriteCloser. |
|
| 15 |
-func (w *BroadcastWriter) AddWriter(writer io.WriteCloser) {
|
|
| 16 |
- w.mu.Lock() |
|
| 17 |
- w.writers = append(w.writers, writer) |
|
| 18 |
- w.mu.Unlock() |
|
| 19 |
-} |
|
| 20 |
- |
|
| 21 |
-// Write writes bytes to all writers. Failed writers will be evicted during |
|
| 22 |
-// this call. |
|
| 23 |
-func (w *BroadcastWriter) Write(p []byte) (n int, err error) {
|
|
| 24 |
- w.mu.Lock() |
|
| 25 |
- var evict []int |
|
| 26 |
- for i, sw := range w.writers {
|
|
| 27 |
- if n, err := sw.Write(p); err != nil || n != len(p) {
|
|
| 28 |
- // On error, evict the writer |
|
| 29 |
- evict = append(evict, i) |
|
| 30 |
- } |
|
| 31 |
- } |
|
| 32 |
- for n, i := range evict {
|
|
| 33 |
- w.writers = append(w.writers[:i-n], w.writers[i-n+1:]...) |
|
| 34 |
- } |
|
| 35 |
- w.mu.Unlock() |
|
| 36 |
- return len(p), nil |
|
| 37 |
-} |
|
| 38 |
- |
|
| 39 |
-// Clean closes and removes all writers. Last non-eol-terminated part of data |
|
| 40 |
-// will be saved. |
|
| 41 |
-func (w *BroadcastWriter) Clean() error {
|
|
| 42 |
- w.mu.Lock() |
|
| 43 |
- for _, sw := range w.writers {
|
|
| 44 |
- sw.Close() |
|
| 45 |
- } |
|
| 46 |
- w.writers = nil |
|
| 47 |
- w.mu.Unlock() |
|
| 48 |
- return nil |
|
| 49 |
-} |
|
| 50 |
- |
|
| 51 |
-// New creates a new BroadcastWriter. |
|
| 52 |
-func New() *BroadcastWriter {
|
|
| 53 |
- return &BroadcastWriter{}
|
|
| 54 |
-} |
| 55 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,162 +0,0 @@ |
| 1 |
-package broadcastwriter |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "bytes" |
|
| 5 |
- "errors" |
|
| 6 |
- "strings" |
|
| 7 |
- |
|
| 8 |
- "testing" |
|
| 9 |
-) |
|
| 10 |
- |
|
| 11 |
-type dummyWriter struct {
|
|
| 12 |
- buffer bytes.Buffer |
|
| 13 |
- failOnWrite bool |
|
| 14 |
-} |
|
| 15 |
- |
|
| 16 |
-func (dw *dummyWriter) Write(p []byte) (n int, err error) {
|
|
| 17 |
- if dw.failOnWrite {
|
|
| 18 |
- return 0, errors.New("Fake fail")
|
|
| 19 |
- } |
|
| 20 |
- return dw.buffer.Write(p) |
|
| 21 |
-} |
|
| 22 |
- |
|
| 23 |
-func (dw *dummyWriter) String() string {
|
|
| 24 |
- return dw.buffer.String() |
|
| 25 |
-} |
|
| 26 |
- |
|
| 27 |
-func (dw *dummyWriter) Close() error {
|
|
| 28 |
- return nil |
|
| 29 |
-} |
|
| 30 |
- |
|
| 31 |
-func TestBroadcastWriter(t *testing.T) {
|
|
| 32 |
- writer := New() |
|
| 33 |
- |
|
| 34 |
- // Test 1: Both bufferA and bufferB should contain "foo" |
|
| 35 |
- bufferA := &dummyWriter{}
|
|
| 36 |
- writer.AddWriter(bufferA) |
|
| 37 |
- bufferB := &dummyWriter{}
|
|
| 38 |
- writer.AddWriter(bufferB) |
|
| 39 |
- writer.Write([]byte("foo"))
|
|
| 40 |
- |
|
| 41 |
- if bufferA.String() != "foo" {
|
|
| 42 |
- t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 43 |
- } |
|
| 44 |
- |
|
| 45 |
- if bufferB.String() != "foo" {
|
|
| 46 |
- t.Errorf("Buffer contains %v", bufferB.String())
|
|
| 47 |
- } |
|
| 48 |
- |
|
| 49 |
- // Test2: bufferA and bufferB should contain "foobar", |
|
| 50 |
- // while bufferC should only contain "bar" |
|
| 51 |
- bufferC := &dummyWriter{}
|
|
| 52 |
- writer.AddWriter(bufferC) |
|
| 53 |
- writer.Write([]byte("bar"))
|
|
| 54 |
- |
|
| 55 |
- if bufferA.String() != "foobar" {
|
|
| 56 |
- t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 57 |
- } |
|
| 58 |
- |
|
| 59 |
- if bufferB.String() != "foobar" {
|
|
| 60 |
- t.Errorf("Buffer contains %v", bufferB.String())
|
|
| 61 |
- } |
|
| 62 |
- |
|
| 63 |
- if bufferC.String() != "bar" {
|
|
| 64 |
- t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 65 |
- } |
|
| 66 |
- |
|
| 67 |
- // Test3: Test eviction on failure |
|
| 68 |
- bufferA.failOnWrite = true |
|
| 69 |
- writer.Write([]byte("fail"))
|
|
| 70 |
- if bufferA.String() != "foobar" {
|
|
| 71 |
- t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 72 |
- } |
|
| 73 |
- if bufferC.String() != "barfail" {
|
|
| 74 |
- t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 75 |
- } |
|
| 76 |
- // Even though we reset the flag, no more writes should go in there |
|
| 77 |
- bufferA.failOnWrite = false |
|
| 78 |
- writer.Write([]byte("test"))
|
|
| 79 |
- if bufferA.String() != "foobar" {
|
|
| 80 |
- t.Errorf("Buffer contains %v", bufferA.String())
|
|
| 81 |
- } |
|
| 82 |
- if bufferC.String() != "barfailtest" {
|
|
| 83 |
- t.Errorf("Buffer contains %v", bufferC.String())
|
|
| 84 |
- } |
|
| 85 |
- |
|
| 86 |
- // Test4: Test eviction on multiple simultaneous failures |
|
| 87 |
- bufferB.failOnWrite = true |
|
| 88 |
- bufferC.failOnWrite = true |
|
| 89 |
- bufferD := &dummyWriter{}
|
|
| 90 |
- writer.AddWriter(bufferD) |
|
| 91 |
- writer.Write([]byte("yo"))
|
|
| 92 |
- writer.Write([]byte("ink"))
|
|
| 93 |
- if strings.Contains(bufferB.String(), "yoink") {
|
|
| 94 |
- t.Errorf("bufferB received write. contents: %q", bufferB)
|
|
| 95 |
- } |
|
| 96 |
- if strings.Contains(bufferC.String(), "yoink") {
|
|
| 97 |
- t.Errorf("bufferC received write. contents: %q", bufferC)
|
|
| 98 |
- } |
|
| 99 |
- if g, w := bufferD.String(), "yoink"; g != w {
|
|
| 100 |
- t.Errorf("bufferD = %q, want %q", g, w)
|
|
| 101 |
- } |
|
| 102 |
- |
|
| 103 |
- writer.Clean() |
|
| 104 |
-} |
|
| 105 |
- |
|
| 106 |
-type devNullCloser int |
|
| 107 |
- |
|
| 108 |
-func (d devNullCloser) Close() error {
|
|
| 109 |
- return nil |
|
| 110 |
-} |
|
| 111 |
- |
|
| 112 |
-func (d devNullCloser) Write(buf []byte) (int, error) {
|
|
| 113 |
- return len(buf), nil |
|
| 114 |
-} |
|
| 115 |
- |
|
| 116 |
-// This test checks for races. It is only useful when run with the race detector. |
|
| 117 |
-func TestRaceBroadcastWriter(t *testing.T) {
|
|
| 118 |
- writer := New() |
|
| 119 |
- c := make(chan bool) |
|
| 120 |
- go func() {
|
|
| 121 |
- writer.AddWriter(devNullCloser(0)) |
|
| 122 |
- c <- true |
|
| 123 |
- }() |
|
| 124 |
- writer.Write([]byte("hello"))
|
|
| 125 |
- <-c |
|
| 126 |
-} |
|
| 127 |
- |
|
| 128 |
-func BenchmarkBroadcastWriter(b *testing.B) {
|
|
| 129 |
- writer := New() |
|
| 130 |
- setUpWriter := func() {
|
|
| 131 |
- for i := 0; i < 100; i++ {
|
|
| 132 |
- writer.AddWriter(devNullCloser(0)) |
|
| 133 |
- writer.AddWriter(devNullCloser(0)) |
|
| 134 |
- writer.AddWriter(devNullCloser(0)) |
|
| 135 |
- } |
|
| 136 |
- } |
|
| 137 |
- testLine := "Line that thinks that it is log line from docker" |
|
| 138 |
- var buf bytes.Buffer |
|
| 139 |
- for i := 0; i < 100; i++ {
|
|
| 140 |
- buf.Write([]byte(testLine + "\n")) |
|
| 141 |
- } |
|
| 142 |
- // line without eol |
|
| 143 |
- buf.Write([]byte(testLine)) |
|
| 144 |
- testText := buf.Bytes() |
|
| 145 |
- b.SetBytes(int64(5 * len(testText))) |
|
| 146 |
- b.ResetTimer() |
|
| 147 |
- for i := 0; i < b.N; i++ {
|
|
| 148 |
- b.StopTimer() |
|
| 149 |
- setUpWriter() |
|
| 150 |
- b.StartTimer() |
|
| 151 |
- |
|
| 152 |
- for j := 0; j < 5; j++ {
|
|
| 153 |
- if _, err := writer.Write(testText); err != nil {
|
|
| 154 |
- b.Fatal(err) |
|
| 155 |
- } |
|
| 156 |
- } |
|
| 157 |
- |
|
| 158 |
- b.StopTimer() |
|
| 159 |
- writer.Clean() |
|
| 160 |
- b.StartTimer() |
|
| 161 |
- } |
|
| 162 |
-} |
| 163 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,167 +0,0 @@ |
| 1 |
-package progressreader |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "errors" |
|
| 5 |
- "io" |
|
| 6 |
- "sync" |
|
| 7 |
-) |
|
| 8 |
- |
|
| 9 |
-// Broadcaster keeps track of one or more observers watching the progress |
|
| 10 |
-// of an operation. For example, if multiple clients are trying to pull an |
|
| 11 |
-// image, they share a Broadcaster for the download operation. |
|
| 12 |
-type Broadcaster struct {
|
|
| 13 |
- sync.Mutex |
|
| 14 |
- // c is a channel that observers block on, waiting for the operation |
|
| 15 |
- // to finish. |
|
| 16 |
- c chan struct{}
|
|
| 17 |
- // cond is a condition variable used to wake up observers when there's |
|
| 18 |
- // new data available. |
|
| 19 |
- cond *sync.Cond |
|
| 20 |
- // history is a buffer of the progress output so far, so a new observer |
|
| 21 |
- // can catch up. The history is stored as a slice of separate byte |
|
| 22 |
- // slices, so that if the writer is a WriteFlusher, the flushes will |
|
| 23 |
- // happen in the right places. |
|
| 24 |
- history [][]byte |
|
| 25 |
- // wg is a WaitGroup used to wait for all writes to finish on Close |
|
| 26 |
- wg sync.WaitGroup |
|
| 27 |
- // result is the argument passed to the first call of Close, and |
|
| 28 |
- // returned to callers of Wait |
|
| 29 |
- result error |
|
| 30 |
-} |
|
| 31 |
- |
|
| 32 |
-// NewBroadcaster returns a Broadcaster structure |
|
| 33 |
-func NewBroadcaster() *Broadcaster {
|
|
| 34 |
- b := &Broadcaster{
|
|
| 35 |
- c: make(chan struct{}),
|
|
| 36 |
- } |
|
| 37 |
- b.cond = sync.NewCond(b) |
|
| 38 |
- return b |
|
| 39 |
-} |
|
| 40 |
- |
|
| 41 |
-// closed returns true if and only if the broadcaster has been closed |
|
| 42 |
-func (broadcaster *Broadcaster) closed() bool {
|
|
| 43 |
- select {
|
|
| 44 |
- case <-broadcaster.c: |
|
| 45 |
- return true |
|
| 46 |
- default: |
|
| 47 |
- return false |
|
| 48 |
- } |
|
| 49 |
-} |
|
| 50 |
- |
|
| 51 |
-// receiveWrites runs as a goroutine so that writes don't block the Write |
|
| 52 |
-// function. It writes the new data in broadcaster.history each time there's |
|
| 53 |
-// activity on the broadcaster.cond condition variable. |
|
| 54 |
-func (broadcaster *Broadcaster) receiveWrites(observer io.Writer) {
|
|
| 55 |
- n := 0 |
|
| 56 |
- |
|
| 57 |
- broadcaster.Lock() |
|
| 58 |
- |
|
| 59 |
- // The condition variable wait is at the end of this loop, so that the |
|
| 60 |
- // first iteration will write the history so far. |
|
| 61 |
- for {
|
|
| 62 |
- newData := broadcaster.history[n:] |
|
| 63 |
- // Make a copy of newData so we can release the lock |
|
| 64 |
- sendData := make([][]byte, len(newData), len(newData)) |
|
| 65 |
- copy(sendData, newData) |
|
| 66 |
- broadcaster.Unlock() |
|
| 67 |
- |
|
| 68 |
- for len(sendData) > 0 {
|
|
| 69 |
- _, err := observer.Write(sendData[0]) |
|
| 70 |
- if err != nil {
|
|
| 71 |
- broadcaster.wg.Done() |
|
| 72 |
- return |
|
| 73 |
- } |
|
| 74 |
- n++ |
|
| 75 |
- sendData = sendData[1:] |
|
| 76 |
- } |
|
| 77 |
- |
|
| 78 |
- broadcaster.Lock() |
|
| 79 |
- |
|
| 80 |
- // If we are behind, we need to catch up instead of waiting |
|
| 81 |
- // or handling a closure. |
|
| 82 |
- if len(broadcaster.history) != n {
|
|
| 83 |
- continue |
|
| 84 |
- } |
|
| 85 |
- |
|
| 86 |
- // detect closure of the broadcast writer |
|
| 87 |
- if broadcaster.closed() {
|
|
| 88 |
- broadcaster.Unlock() |
|
| 89 |
- broadcaster.wg.Done() |
|
| 90 |
- return |
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- broadcaster.cond.Wait() |
|
| 94 |
- |
|
| 95 |
- // Mutex is still locked as the loop continues |
|
| 96 |
- } |
|
| 97 |
-} |
|
| 98 |
- |
|
| 99 |
-// Write adds data to the history buffer, and also writes it to all current |
|
| 100 |
-// observers. |
|
| 101 |
-func (broadcaster *Broadcaster) Write(p []byte) (n int, err error) {
|
|
| 102 |
- broadcaster.Lock() |
|
| 103 |
- defer broadcaster.Unlock() |
|
| 104 |
- |
|
| 105 |
- // Is the broadcaster closed? If so, the write should fail. |
|
| 106 |
- if broadcaster.closed() {
|
|
| 107 |
- return 0, errors.New("attempted write to closed progressreader Broadcaster")
|
|
| 108 |
- } |
|
| 109 |
- |
|
| 110 |
- // Add message in p to the history slice |
|
| 111 |
- newEntry := make([]byte, len(p), len(p)) |
|
| 112 |
- copy(newEntry, p) |
|
| 113 |
- broadcaster.history = append(broadcaster.history, newEntry) |
|
| 114 |
- |
|
| 115 |
- broadcaster.cond.Broadcast() |
|
| 116 |
- |
|
| 117 |
- return len(p), nil |
|
| 118 |
-} |
|
| 119 |
- |
|
| 120 |
-// Add adds an observer to the Broadcaster. The new observer receives the |
|
| 121 |
-// data from the history buffer, and also all subsequent data. |
|
| 122 |
-func (broadcaster *Broadcaster) Add(w io.Writer) error {
|
|
| 123 |
- // The lock is acquired here so that Add can't race with Close |
|
| 124 |
- broadcaster.Lock() |
|
| 125 |
- defer broadcaster.Unlock() |
|
| 126 |
- |
|
| 127 |
- if broadcaster.closed() {
|
|
| 128 |
- return errors.New("attempted to add observer to closed progressreader Broadcaster")
|
|
| 129 |
- } |
|
| 130 |
- |
|
| 131 |
- broadcaster.wg.Add(1) |
|
| 132 |
- go broadcaster.receiveWrites(w) |
|
| 133 |
- |
|
| 134 |
- return nil |
|
| 135 |
-} |
|
| 136 |
- |
|
| 137 |
-// CloseWithError signals to all observers that the operation has finished. Its |
|
| 138 |
-// argument is a result that should be returned to waiters blocking on Wait. |
|
| 139 |
-func (broadcaster *Broadcaster) CloseWithError(result error) {
|
|
| 140 |
- broadcaster.Lock() |
|
| 141 |
- if broadcaster.closed() {
|
|
| 142 |
- broadcaster.Unlock() |
|
| 143 |
- return |
|
| 144 |
- } |
|
| 145 |
- broadcaster.result = result |
|
| 146 |
- close(broadcaster.c) |
|
| 147 |
- broadcaster.cond.Broadcast() |
|
| 148 |
- broadcaster.Unlock() |
|
| 149 |
- |
|
| 150 |
- // Don't return until all writers have caught up. |
|
| 151 |
- broadcaster.wg.Wait() |
|
| 152 |
-} |
|
| 153 |
- |
|
| 154 |
-// Close signals to all observers that the operation has finished. It causes |
|
| 155 |
-// all calls to Wait to return nil. |
|
| 156 |
-func (broadcaster *Broadcaster) Close() {
|
|
| 157 |
- broadcaster.CloseWithError(nil) |
|
| 158 |
-} |
|
| 159 |
- |
|
| 160 |
-// Wait blocks until the operation is marked as completed by the Close method, |
|
| 161 |
-// and all writer goroutines have completed. It returns the argument that was |
|
| 162 |
-// passed to Close. |
|
| 163 |
-func (broadcaster *Broadcaster) Wait() error {
|
|
| 164 |
- <-broadcaster.c |
|
| 165 |
- broadcaster.wg.Wait() |
|
| 166 |
- return broadcaster.result |
|
| 167 |
-} |