launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/worker/uniter/relation/relation.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // relation implements persistent local storage of a unit's relation state, and
     5  // translation of relation changes into hooks that need to be run.
     6  package relation
     7  
     8  import (
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"launchpad.net/errgo/errors"
    16  	"launchpad.net/juju-core/charm/hooks"
    17  	"launchpad.net/juju-core/utils"
    18  	"launchpad.net/juju-core/worker/uniter/hook"
    19  )
    20  
    21  var mask = errors.Mask
    22  
    23  // State describes the state of a relation.
    24  type State struct {
    25  	// RelationId identifies the relation.
    26  	RelationId int
    27  
    28  	// Members is a map from unit name to the last change version
    29  	// for which a hook.Info was delivered on the output channel.
    30  	Members map[string]int64
    31  
    32  	// ChangedPending indicates that a "relation-changed" hook for the given
    33  	// unit name must be the first hook.Info to be sent to the output channel.
    34  	ChangedPending string
    35  }
    36  
    37  // copy returns an independent copy of the state.
    38  func (s *State) copy() *State {
    39  	copy := &State{
    40  		RelationId:     s.RelationId,
    41  		ChangedPending: s.ChangedPending,
    42  	}
    43  	if s.Members != nil {
    44  		copy.Members = map[string]int64{}
    45  		for m, v := range s.Members {
    46  			copy.Members[m] = v
    47  		}
    48  	}
    49  	return copy
    50  }
    51  
    52  // Validate returns an error if the supplied hook.Info does not represent
    53  // a valid change to the relation state. Hooks must always be validated
    54  // against the current state before they are run, to ensure that the system
    55  // meets its guarantees about hook execution order.
    56  func (s *State) Validate(hi hook.Info) (err error) {
    57  	defer utils.ErrorContextf(&err, "inappropriate %q for %q", hi.Kind, hi.RemoteUnit)
    58  	if hi.RelationId != s.RelationId {
    59  		return errors.Newf("expected relation %d, got relation %d", s.RelationId, hi.RelationId)
    60  	}
    61  	if s.Members == nil {
    62  		return errors.Newf(`relation is broken and cannot be changed further`)
    63  	}
    64  	unit, kind := hi.RemoteUnit, hi.Kind
    65  	if kind == hooks.RelationBroken {
    66  		if len(s.Members) == 0 {
    67  			return nil
    68  		}
    69  		return errors.Newf(`cannot run "relation-broken" while units still present`)
    70  	}
    71  	if s.ChangedPending != "" {
    72  		if unit != s.ChangedPending || kind != hooks.RelationChanged {
    73  			return errors.Newf(`expected "relation-changed" for %q`, s.ChangedPending)
    74  		}
    75  	} else if _, joined := s.Members[unit]; joined && kind == hooks.RelationJoined {
    76  		return errors.Newf("unit already joined")
    77  	} else if !joined && kind != hooks.RelationJoined {
    78  		return errors.Newf("unit has not joined")
    79  	}
    80  	return nil
    81  }
    82  
    83  // StateDir is a filesystem-backed representation of the state of a
    84  // relation. Concurrent modifications to the underlying state directory
    85  // will have undefined consequences.
    86  type StateDir struct {
    87  	// path identifies the directory holding persistent state.
    88  	path string
    89  
    90  	// state is the cached state of the directory, which is guaranteed
    91  	// to be synchronized with the true state so long as no concurrent
    92  	// changes are made to the directory.
    93  	state State
    94  }
    95  
    96  // State returns the current state of the relation.
    97  func (d *StateDir) State() *State {
    98  	return d.state.copy()
    99  }
   100  
   101  // ReadStateDir loads a StateDir from the subdirectory of dirPath named
   102  // for the supplied RelationId. If the directory does not exist, no error
   103  // is returned,
   104  func ReadStateDir(dirPath string, relationId int) (d *StateDir, err error) {
   105  	d = &StateDir{
   106  		filepath.Join(dirPath, strconv.Itoa(relationId)),
   107  		State{relationId, map[string]int64{}, ""},
   108  	}
   109  	defer utils.ErrorContextf(&err, "cannot load relation state from %q", d.path)
   110  	if _, err := os.Stat(d.path); os.IsNotExist(err) {
   111  		return d, nil
   112  	} else if err != nil {
   113  		return nil, mask(err)
   114  	}
   115  	fis, err := ioutil.ReadDir(d.path)
   116  	if err != nil {
   117  		return nil, mask(err)
   118  	}
   119  	for _, fi := range fis {
   120  		// Entries with names ending in "-" followed by an integer must be
   121  		// files containing valid unit data; all other names are ignored.
   122  		name := fi.Name()
   123  		i := strings.LastIndex(name, "-")
   124  		if i == -1 {
   125  			continue
   126  		}
   127  		svcName := name[:i]
   128  		unitId := name[i+1:]
   129  		if _, err := strconv.Atoi(unitId); err != nil {
   130  			continue
   131  		}
   132  		unitName := svcName + "/" + unitId
   133  		var info diskInfo
   134  		if err = utils.ReadYaml(filepath.Join(d.path, name), &info); err != nil {
   135  			return nil, errors.Notef(err, "invalid unit file %q", name)
   136  		}
   137  		if info.ChangeVersion == nil {
   138  			return nil, errors.Newf(`invalid unit file %q: "changed-version" not set`, name)
   139  		}
   140  		d.state.Members[unitName] = *info.ChangeVersion
   141  		if info.ChangedPending {
   142  			if d.state.ChangedPending != "" {
   143  				return nil, errors.Newf("%q and %q both have pending changed hooks", d.state.ChangedPending, unitName)
   144  			}
   145  			d.state.ChangedPending = unitName
   146  		}
   147  	}
   148  	return d, nil
   149  }
   150  
   151  // ReadAllStateDirs loads and returns every StateDir persisted directly inside
   152  // the supplied dirPath. If dirPath does not exist, no error is returned.
   153  func ReadAllStateDirs(dirPath string) (dirs map[int]*StateDir, err error) {
   154  	defer utils.ErrorContextf(&err, "cannot load relations state from %q", dirPath)
   155  	if _, err := os.Stat(dirPath); os.IsNotExist(err) {
   156  		return nil, nil
   157  	} else if err != nil {
   158  		return nil, mask(err)
   159  	}
   160  	fis, err := ioutil.ReadDir(dirPath)
   161  	if err != nil {
   162  		return nil, mask(err)
   163  	}
   164  	dirs = map[int]*StateDir{}
   165  	for _, fi := range fis {
   166  		// Entries with integer names must be directories containing StateDir
   167  		// data; all other names will be ignored.
   168  		relationId, err := strconv.Atoi(fi.Name())
   169  		if err != nil {
   170  			// This doesn't look like a relation.
   171  			continue
   172  		}
   173  		dir, err := ReadStateDir(dirPath, relationId)
   174  		if err != nil {
   175  			return nil, mask(err)
   176  		}
   177  		dirs[relationId] = dir
   178  	}
   179  	return dirs, nil
   180  }
   181  
   182  // Ensure creates the directory if it does not already exist.
   183  func (d *StateDir) Ensure() error {
   184  	return os.MkdirAll(d.path, 0755)
   185  }
   186  
   187  // Write atomically writes to disk the relation state change in hi.
   188  // It must be called after the respective hook was executed successfully.
   189  // Write doesn't validate hi but guarantees that successive writes of
   190  // the same hi are idempotent.
   191  func (d *StateDir) Write(hi hook.Info) (err error) {
   192  	defer utils.ErrorContextf(&err, "failed to write %q hook info for %q on state directory", hi.Kind, hi.RemoteUnit)
   193  	if hi.Kind == hooks.RelationBroken {
   194  		return d.Remove()
   195  	}
   196  	name := strings.Replace(hi.RemoteUnit, "/", "-", 1)
   197  	path := filepath.Join(d.path, name)
   198  	if hi.Kind == hooks.RelationDeparted {
   199  		if err = os.Remove(path); err != nil && !os.IsNotExist(err) {
   200  			return err
   201  		}
   202  		// If atomic delete succeeded, update own state.
   203  		delete(d.state.Members, hi.RemoteUnit)
   204  		return nil
   205  	}
   206  	di := diskInfo{&hi.ChangeVersion, hi.Kind == hooks.RelationJoined}
   207  	if err := utils.WriteYaml(path, &di); err != nil {
   208  		return mask(err)
   209  	}
   210  
   211  	// If write was successful, update own state.
   212  	d.state.Members[hi.RemoteUnit] = hi.ChangeVersion
   213  	if hi.Kind == hooks.RelationJoined {
   214  		d.state.ChangedPending = hi.RemoteUnit
   215  	} else {
   216  		d.state.ChangedPending = ""
   217  	}
   218  	return nil
   219  }
   220  
   221  // Remove removes the directory if it exists and is empty.
   222  func (d *StateDir) Remove() error {
   223  	if err := os.Remove(d.path); err != nil && !os.IsNotExist(err) {
   224  		return err
   225  	}
   226  	// If atomic delete succeeded, update own state.
   227  	d.state.Members = nil
   228  	return nil
   229  }
   230  
   231  // diskInfo defines the relation unit data serialization.
   232  type diskInfo struct {
   233  	ChangeVersion  *int64 `yaml:"change-version"`
   234  	ChangedPending bool   `yaml:"changed-pending,omitempty"`
   235  }