overlord/state: introduce Task and Change.NewTask #578

Merged
merged 4 commits into from Mar 4, 2016
@@ -39,6 +39,7 @@ type Change struct {
kind string
summary string
data customData
+ taskIDs map[string]bool
}
func newChange(state *State, id, kind, summary string) *Change {
@@ -48,6 +49,7 @@ func newChange(state *State, id, kind, summary string) *Change {
kind: kind,
summary: summary,
data: make(customData),
+ taskIDs: make(map[string]bool),
}
}
@@ -56,6 +58,7 @@ type marshalledChange struct {
Kind string `json:"kind"`
Summary string `json:"summary"`
Data map[string]*json.RawMessage `json:"data"`
+ TaskIDs map[string]bool `json:"task-ids"`
}
// MarshalJSON makes Change a json.Marshaller
@@ -66,11 +69,15 @@ func (c *Change) MarshalJSON() ([]byte, error) {
Kind: c.kind,
Summary: c.summary,
Data: c.data,
+ TaskIDs: c.taskIDs,
})
}
// UnmarshalJSON makes Change a json.Unmarshaller
func (c *Change) UnmarshalJSON(data []byte) error {
+ if c.state != nil {
+ c.state.ensureLocked()
+ }
var unmarshalled marshalledChange
err := json.Unmarshal(data, &unmarshalled)
if err != nil {
@@ -80,6 +87,7 @@ func (c *Change) UnmarshalJSON(data []byte) error {
c.kind = unmarshalled.Kind
c.summary = unmarshalled.Summary
c.data = unmarshalled.Data
+ c.taskIDs = unmarshalled.TaskIDs
return nil
}
@@ -111,3 +119,26 @@ func (c *Change) Get(key string, value interface{}) error {
c.state.ensureLocked()
return c.data.get(key, value)
}
+
+// NewTask creates a new task and registers it as a required task for the
+// state change to be accomplished.
+func (c *Change) NewTask(kind, summary string) *Task {
+ c.state.ensureLocked()
+ id := c.state.genID()
+ t := newTask(c.state, id, kind, summary)
+ c.state.tasks[id] = t
+ c.taskIDs[id] = true
+ return t
+}
+
+// TODO: AddTask
+
+// Tasks returns all the tasks this state change depends on.
+func (c *Change) Tasks() []*Task {
+ c.state.ensureLocked()
+ res := make([]*Task, 0, len(c.taskIDs))
+ for tid := range c.taskIDs {
+ res = append(res, c.state.tasks[tid])
+ }
+ return res
+}
@@ -72,3 +72,44 @@ func (cs *changeSuite) TestSetNeedsLock(c *C) {
c.Assert(func() { chg.Set("a", 1) }, PanicMatches, "internal error: accessing state without lock")
}
+
+func (cs *changeSuite) TestNewTaskAndTasks(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.NewChange("install", "...")
+
+ t1 := chg.NewTask("download", "1...")
+ t2 := chg.NewTask("verify", "2...")
+
+ tasks := chg.Tasks()
+ c.Check(tasks, HasLen, 2)
+
+ expected := map[string]*state.Task{
+ t1.ID(): t1,
+ t2.ID(): t2,
+ }
+
+ for _, t := range tasks {
+ c.Check(t, Equals, expected[t.ID()])
+ }
+}
+
+func (cs *changeSuite) TestNewTaskNeedsLocked(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ st.Unlock()
+
+ c.Assert(func() { chg.NewTask("download", "...") }, PanicMatches, "internal error: accessing state without lock")
+}
+
+func (cs *changeSuite) TestTasksNeedsLocked(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ chg := st.NewChange("install", "...")
+ st.Unlock()
+
+ c.Assert(func() { chg.Tasks() }, PanicMatches, "internal error: accessing state without lock")
+}
@@ -77,6 +77,7 @@ type State struct {
backend Backend
data customData
changes map[string]*Change
+ tasks map[string]*Task
}
// New returns a new empty state.
@@ -85,6 +86,7 @@ func New(backend Backend) *State {
backend: backend,
data: make(customData),
changes: make(map[string]*Change),
+ tasks: make(map[string]*Task),
}
}
@@ -109,6 +111,7 @@ func (s *State) unlock() {
type marshalledState struct {
Data map[string]*json.RawMessage `json:"data"`
Changes map[string]*Change `json:"changes"`
+ Tasks map[string]*Task `json:"tasks"`
}
// MarshalJSON makes State a json.Marshaller
@@ -117,6 +120,7 @@ func (s *State) MarshalJSON() ([]byte, error) {
return json.Marshal(marshalledState{
Data: s.data,
Changes: s.changes,
+ Tasks: s.tasks,
})
}
@@ -130,7 +134,11 @@ func (s *State) UnmarshalJSON(data []byte) error {
}
s.data = unmarshalled.Data
s.changes = unmarshalled.Changes
+ s.tasks = unmarshalled.Tasks
// backlink state again
+ for _, t := range s.tasks {
+ t.state = s
+ }
for _, chg := range s.changes {
chg.state = s
}
@@ -305,3 +305,46 @@ func (ss *stateSuite) TestNewChangeAndCheckpoint(c *C) {
err = chg0.Get("a", &v)
c.Check(v, Equals, 1)
}
+
+func (ss *stateSuite) TestNewTaskAndCheckpoint(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+ st.Lock()
+
+ chg := st.NewChange("install", "summary")
+ c.Assert(chg, NotNil)
+
+ t1 := chg.NewTask("download", "1...")
+ t1ID := t1.ID()
+ t1.Set("a", 1)
+
+ // implicit checkpoint
+ st.Unlock()
+
+ c.Assert(b.checkpoints, HasLen, 1)
+
+ buf := bytes.NewBuffer(b.checkpoints[0])
+
+ st2, err := state.ReadState(nil, buf)
+ c.Assert(err, IsNil)
+ c.Assert(st2, NotNil)
+
+ st2.Lock()
+ defer st2.Unlock()
+
+ chgs := st2.Changes()
+ c.Assert(chgs, HasLen, 1)
+ chg0 := chgs[0]
+
+ tasks0 := chg0.Tasks()
+ c.Assert(tasks0, HasLen, 1)
+
+ task0_1 := tasks0[0]
+ c.Check(task0_1.ID(), Equals, t1ID)
+ c.Check(task0_1.Kind(), Equals, "download")
+ c.Check(task0_1.Summary(), Equals, "1...")
+
+ var v int
+ err = task0_1.Get("a", &v)
+ c.Check(v, Equals, 1)
+}
View
@@ -0,0 +1,110 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package state
+
+import (
+ "encoding/json"
+)
+
+// Task represents an individual operation to be performed
+// for accomplishing one or more state changes.
+//
+// See Change for more details.
+type Task struct {
+ state *State
+ id string
+ kind string
+ summary string
+ data customData
+}
+
+func newTask(state *State, id, kind, summary string) *Task {
+ return &Task{
+ state: state,
+ id: id,
+ kind: kind,
+ summary: summary,
+ data: make(customData),
+ }
+}
+
+type marshalledTask struct {
+ ID string `json:"id"`
+ Kind string `json:"kind"`
+ Summary string `json:"summary"`
+ Data map[string]*json.RawMessage `json:"data"`
+}
+
+// MarshalJSON makes Task a json.Marshaller
+func (t *Task) MarshalJSON() ([]byte, error) {
+ t.state.ensureLocked()
+ return json.Marshal(marshalledTask{
+ ID: t.id,
+ Kind: t.kind,
+ Summary: t.summary,
+ Data: t.data,
+ })
+}
+
+// UnmarshalJSON makes Task a json.Unmarshaller
+func (t *Task) UnmarshalJSON(data []byte) error {
@niemeyer

niemeyer Mar 3, 2016

Contributor

ensureLocked

+ if t.state != nil {
+ t.state.ensureLocked()
+ }
+ var unmarshalled marshalledTask
+ err := json.Unmarshal(data, &unmarshalled)
+ if err != nil {
+ return err
+ }
+ t.id = unmarshalled.ID
+ t.kind = unmarshalled.Kind
+ t.summary = unmarshalled.Summary
+ t.data = unmarshalled.Data
+ return nil
+}
+
+// ID returns the individual random key for this task.
+func (t *Task) ID() string {
+ return t.id
+}
+
+// Kind returns the nature of this task for managers to know how to handle it.
+func (t *Task) Kind() string {
+ return t.kind
+}
+
+// Summary returns a summary describing what the task is about.
+func (t *Task) Summary() string {
+ return t.summary
+}
+
+// Set associates value with key for future consulting by managers.
+// The provided value must properly marshal and unmarshal with encoding/json.
+func (t *Task) Set(key string, value interface{}) {
+ t.state.ensureLocked()
+ t.data.set(key, value)
+}
+
+// Get unmarshals the stored value associated with the provided key
+// into the value parameter.
+func (t *Task) Get(key string, value interface{}) error {
+ t.state.ensureLocked()
+ return t.data.get(key, value)
+}
Oops, something went wrong.