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  }