github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/fortress/fortress.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package fortress 5 6 import ( 7 "sync" 8 9 "gopkg.in/tomb.v2" 10 ) 11 12 // fortress coordinates between clients that access it as a Guard and as a Guest. 13 type fortress struct { 14 tomb tomb.Tomb 15 guardTickets chan guardTicket 16 guestTickets chan guestTicket 17 } 18 19 // newFortress returns a new, locked, fortress. The caller is responsible for 20 // ensuring it somehow gets Kill()ed, and for handling any error returned by 21 // Wait(). 22 func newFortress() *fortress { 23 f := &fortress{ 24 guardTickets: make(chan guardTicket), 25 guestTickets: make(chan guestTicket), 26 } 27 f.tomb.Go(f.loop) 28 return f 29 } 30 31 // Kill is part of the worker.Worker interface. 32 func (f *fortress) Kill() { 33 f.tomb.Kill(nil) 34 } 35 36 // Wait is part of the worker.Worker interface. 37 func (f *fortress) Wait() error { 38 return f.tomb.Wait() 39 } 40 41 // Unlock is part of the Guard interface. 42 func (f *fortress) Unlock() error { 43 return f.allowGuests(true, nil) 44 } 45 46 // Lockdown is part of the Guard interface. 47 func (f *fortress) Lockdown(abort Abort) error { 48 return f.allowGuests(false, abort) 49 } 50 51 // Visit is part of the Guest interface. 52 func (f *fortress) Visit(visit Visit, abort Abort) error { 53 result := make(chan error) 54 select { 55 case <-f.tomb.Dying(): 56 return ErrShutdown 57 case <-abort: 58 return ErrAborted 59 case f.guestTickets <- guestTicket{visit, result}: 60 return <-result 61 } 62 } 63 64 // allowGuests communicates Guard-interface requests to the main loop. 65 func (f *fortress) allowGuests(allowGuests bool, abort Abort) error { 66 result := make(chan error) 67 select { 68 case <-f.tomb.Dying(): 69 return ErrShutdown 70 case f.guardTickets <- guardTicket{allowGuests, abort, result}: 71 return <-result 72 } 73 } 74 75 // loop waits for a Guard to unlock the fortress, and then runs visit funcs in 76 // parallel until a Guard locks it down again; at which point, it waits for all 77 // outstanding visits to complete, and reverts to its original state. 78 func (f *fortress) loop() error { 79 var active sync.WaitGroup 80 defer active.Wait() 81 82 // guestTickets will be set on Unlock and cleared at the start of Lockdown. 83 var guestTickets <-chan guestTicket 84 for { 85 select { 86 case <-f.tomb.Dying(): 87 return tomb.ErrDying 88 case ticket := <-guestTickets: 89 active.Add(1) 90 go ticket.complete(active.Done) 91 case ticket := <-f.guardTickets: 92 // guard ticket requests are idempotent; it's not worth building 93 // the extra mechanism needed to (1) complain about abuse but 94 // (2) remain comprehensible and functional in the face of aborted 95 // Lockdowns. 96 if ticket.allowGuests { 97 guestTickets = f.guestTickets 98 } else { 99 guestTickets = nil 100 } 101 go ticket.complete(active.Wait) 102 } 103 } 104 } 105 106 // guardTicket communicates between the Guard interface and the main loop. 107 type guardTicket struct { 108 allowGuests bool 109 abort Abort 110 result chan<- error 111 } 112 113 // complete unconditionally sends a single value on ticket.result; either nil 114 // (when the desired state is reached) or ErrAborted (when the ticket's Abort 115 // is closed). It should be called on its own goroutine. 116 func (ticket guardTicket) complete(waitLockedDown func()) { 117 var result error 118 defer func() { 119 ticket.result <- result 120 }() 121 122 done := make(chan struct{}) 123 go func() { 124 // If we're locking down, we should wait for all Visits to complete. 125 // If not, Visits are already being accepted and we're already done. 126 if !ticket.allowGuests { 127 waitLockedDown() 128 } 129 close(done) 130 }() 131 select { 132 case <-done: 133 case <-ticket.abort: 134 result = ErrAborted 135 } 136 } 137 138 // guestTicket communicates between the Guest interface and the main loop. 139 type guestTicket struct { 140 visit Visit 141 result chan<- error 142 } 143 144 // complete unconditionally sends any error returned from the Visit func, then 145 // calls the finished func. It should be called on its own goroutine. 146 func (ticket guestTicket) complete(finished func()) { 147 defer finished() 148 ticket.result <- ticket.visit() 149 }