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