Add JobsController implementation with tests 10/162010/3
authorLukasz Wojciechowski <l.wojciechow@partner.samsung.com>
Thu, 9 Nov 2017 17:00:25 +0000 (18:00 +0100)
committerLukasz Wojciechowski <l.wojciechow@partner.samsung.com>
Mon, 4 Dec 2017 08:12:58 +0000 (09:12 +0100)
JobsControllerImpl structure stores actual information about all Jobs
such as status, update time, config and Dryad access. It controls
collision free JobID creation. It stores state of Jobs execution
and saves data to DB. It implements JobsController interface.

Change-Id: Ie3e3c46660c650c48cd80a93038b8cdd05f3fc21
Signed-off-by: Lukasz Wojciechowski <l.wojciechow@partner.samsung.com>
controller/controller_suite_test.go [new file with mode: 0644]
controller/jobscontrollerimpl.go [new file with mode: 0644]
controller/jobscontrollerimpl_test.go [new file with mode: 0644]

diff --git a/controller/controller_suite_test.go b/controller/controller_suite_test.go
new file mode 100644 (file)
index 0000000..f66057e
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ *  Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package controller
+
+import (
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+
+       "testing"
+)
+
+func TestController(t *testing.T) {
+       RegisterFailHandler(Fail)
+       RunSpecs(t, "Controller Suite")
+}
diff --git a/controller/jobscontrollerimpl.go b/controller/jobscontrollerimpl.go
new file mode 100644 (file)
index 0000000..adff3cf
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ *  Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+// File controller/jobscontrollerimpl.go contains JobsController interface
+// implementation.
+
+package controller
+
+import (
+       "sync"
+       "time"
+
+       . "git.tizen.org/tools/weles"
+)
+
+// JobsControllerImpl structure stores Weles' Jobs data. It controls
+// collision-free JobID creation. It stores state of Jobs' execution and saves
+// data to DB. It implements JobsController interface.
+type JobsControllerImpl struct {
+       JobsController
+       // mutex protects JobsControllerImpl structure.
+       mutex *sync.RWMutex
+       // lastID is the last used ID for the Job.
+       lastID JobID
+       // jobs stores information about Weles' Jobs.
+       jobs map[JobID]*Job
+}
+
+// setupLastID initializes last used ID. Value is read from DB meta data.
+func (js *JobsControllerImpl) setupLastID() {
+       // TODO initialize with meta data read from DB.
+       // Current implementation starts with seconds from Epoch to avoid problems with
+       // artifacts database.
+
+       js.lastID = JobID(time.Now().Unix())
+}
+
+// NewJobsController creates and initializes a new instance of Jobs structure.
+// It is the only valid way of creating it.
+func NewJobsController() JobsController {
+       js := &JobsControllerImpl{
+               mutex: new(sync.RWMutex),
+               jobs:  make(map[JobID]*Job),
+       }
+
+       js.setupLastID()
+
+       // TODO load Jobs data from DB.
+
+       return js
+}
+
+// nextID generates and returns ID assigned to a new Job.
+// It also updates lastID and saves the information in DB meta data.
+func (js *JobsControllerImpl) nextID() JobID {
+       js.lastID++
+
+       // TODO save new lastID in DB.
+
+       return js.lastID
+}
+
+// NewJob creates and initializes a new Job.
+func (js *JobsControllerImpl) NewJob(yaml []byte) (JobID, error) {
+       js.mutex.Lock()
+       defer js.mutex.Unlock()
+
+       j := js.nextID()
+
+       now := time.Now()
+       js.jobs[j] = &Job{
+               JobInfo: JobInfo{
+                       JobID:   j,
+                       Created: now,
+                       Updated: now,
+                       Status:  JOB_NEW,
+               },
+               yaml: yaml,
+       }
+
+       // TODO save struct in DB
+
+       return j, nil
+}
+
+// GetYaml returns yaml Job description.
+func (js *JobsControllerImpl) GetYaml(j JobID) ([]byte, error) {
+       js.mutex.RLock()
+       defer js.mutex.RUnlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return nil, ErrJobNotFound
+       }
+
+       return job.yaml, nil
+}
+
+// SetConfig stores config in Jobs structure.
+func (js *JobsControllerImpl) SetConfig(j JobID, conf Config) error {
+       js.mutex.Lock()
+       defer js.mutex.Unlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return ErrJobNotFound
+       }
+
+       job.config = conf
+       job.Updated = time.Now()
+       return nil
+}
+
+// isStatusChangeValid verifies if Job's status change is valid.
+// It is a helper function for SetStatusAndInfo.
+func isStatusChangeValid(oldStatus, newStatus JobStatus) bool {
+       if oldStatus == newStatus {
+               return true
+       }
+       switch oldStatus {
+       case JOB_NEW:
+               switch newStatus {
+               case JOB_PARSING, JOB_CANCELED, JOB_FAILED:
+                       return true
+               }
+       case JOB_PARSING:
+               switch newStatus {
+               case JOB_DOWNLOADING, JOB_CANCELED, JOB_FAILED:
+                       return true
+               }
+       case JOB_DOWNLOADING:
+               switch newStatus {
+               case JOB_WAITING, JOB_CANCELED, JOB_FAILED:
+                       return true
+               }
+       case JOB_WAITING:
+               switch newStatus {
+               case JOB_RUNNING, JOB_CANCELED, JOB_FAILED:
+                       return true
+               }
+       case JOB_RUNNING:
+               switch newStatus {
+               case JOB_COMPLETED, JOB_CANCELED, JOB_FAILED:
+                       return true
+               }
+       }
+       return false
+}
+
+// SetStatusAndInfo changes status of the Job and updates info. Only valid
+// changes are allowed.
+// There are 3 terminal statuses: JOB_FAILED, JOB_CANCELED, JOB_COMPLETED;
+// and 5 non-terminal statuses: JOB_NEW, JOB_PARSING, JOB_DOWNLOADING,
+// JOB_WAITING, JOB_RUNNING.
+// Only below changes of statuses are allowed:
+// * JOB_NEW --> {JOB_PARSING, JOB_CANCELED, JOB_FAILED}
+// * JOB_PARSING --> {JOB_DOWNLOADING, JOB_CANCELED, JOB_FAILED}
+// * JOB_DOWNLOADING --> {JOB_WAITING, JOB_CANCELED, JOB_FAILED}
+// * JOB_WAITING --> {JOB_RUNNING, JOB_CANCELED, JOB_FAILED}
+// * JOB_RUNNING --> {JOB_COMPLETED, JOB_CANCELED, JOB_FAILED}
+func (js *JobsControllerImpl) SetStatusAndInfo(j JobID, newStatus JobStatus, msg string) error {
+       js.mutex.Lock()
+       defer js.mutex.Unlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return ErrJobNotFound
+       }
+
+       if !isStatusChangeValid(job.Status, newStatus) {
+               return ErrJobStatusChangeNotAllowed
+       }
+
+       job.Status = newStatus
+       job.Info = msg
+       job.Updated = time.Now()
+       return nil
+}
+
+// GetConfig returns Job's config.
+func (js *JobsControllerImpl) GetConfig(j JobID) (conf Config, err error) {
+       js.mutex.RLock()
+       defer js.mutex.RUnlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return conf, ErrJobNotFound
+       }
+
+       return job.config, nil
+}
+
+// SetDryad saves access info for acquired Dryad.
+func (js *JobsControllerImpl) SetDryad(j JobID, d Dryad) error {
+       js.mutex.Lock()
+       defer js.mutex.Unlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return ErrJobNotFound
+       }
+
+       job.dryad = d
+       return nil
+}
+
+// GetDryad returns Dryad acquired for the Job.
+func (js *JobsControllerImpl) GetDryad(j JobID) (Dryad, error) {
+       js.mutex.RLock()
+       defer js.mutex.RUnlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return Dryad{}, ErrJobNotFound
+       }
+
+       return job.dryad, nil
+}
diff --git a/controller/jobscontrollerimpl_test.go b/controller/jobscontrollerimpl_test.go
new file mode 100644 (file)
index 0000000..976f6d9
--- /dev/null
@@ -0,0 +1,306 @@
+/*
+ *  Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package controller
+
+import (
+       "fmt"
+       "net"
+       "time"
+
+       . "git.tizen.org/tools/weles"
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+)
+
+var _ = Describe("JobsControllerImpl", func() {
+       Describe("NewJobsController", func() {
+               It("should create a new object", func() {
+                       before := time.Now()
+                       jc := NewJobsController()
+                       after := time.Now()
+
+                       Expect(jc).NotTo(BeNil())
+                       Expect(jc.(*JobsControllerImpl).mutex).NotTo(BeNil())
+                       Expect(jc.(*JobsControllerImpl).jobs).NotTo(BeNil())
+                       Expect(jc.(*JobsControllerImpl).jobs).To(BeEmpty())
+                       Expect(jc.(*JobsControllerImpl).lastID).To(BeNumerically(">=", before.Unix()))
+                       Expect(jc.(*JobsControllerImpl).lastID).To(BeNumerically("<=", after.Unix()))
+               })
+       })
+       Describe("With JobsController initialized", func() {
+               var jc JobsController
+               var initID JobID
+               yaml := []byte("test yaml")
+               BeforeEach(func() {
+                       jc = NewJobsController()
+                       initID = jc.(*JobsControllerImpl).lastID
+               })
+               Describe("NewJob", func() {
+                       It("should create new Job structure", func() {
+                               before := time.Now()
+                               j, err := jc.NewJob(yaml)
+                               after := time.Now()
+
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(j).To(Equal(initID + 1))
+
+                               Expect(jc.(*JobsControllerImpl).lastID).To(Equal(j))
+                               Expect(len(jc.(*JobsControllerImpl).jobs)).To(Equal(1))
+
+                               job, ok := jc.(*JobsControllerImpl).jobs[j]
+                               Expect(ok).To(BeTrue())
+                               Expect(job.JobID).To(Equal(j))
+                               Expect(job.Created).To(Equal(job.Updated))
+                               Expect(job.Created).To(BeTemporally(">=", before))
+                               Expect(job.Created).To(BeTemporally("<=", after))
+                               Expect(job.Status).To(Equal(JOB_NEW))
+                               Expect(job.yaml).To(Equal(yaml))
+                       })
+               })
+               Describe("GetYaml", func() {
+                       It("should return proper yaml for existing job", func() {
+                               j, err := jc.NewJob(yaml)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(j).To(Equal(initID + 1))
+
+                               retyaml, err := jc.GetYaml(j)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(retyaml).To(Equal(yaml))
+                       })
+                       It("should return error for not existing job", func() {
+                               yaml, err := jc.GetYaml(initID + 1)
+                               Expect(err).To(Equal(ErrJobNotFound))
+                               Expect(yaml).To(BeZero())
+                       })
+               })
+               Describe("SetConfig", func() {
+                       It("should set config for existing job", func() {
+                               j, err := jc.NewJob(yaml)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(j).To(Equal(initID + 1))
+
+                               config := Config{JobName: "Test Job"}
+                               before := time.Now()
+                               err = jc.SetConfig(j, config)
+                               after := time.Now()
+                               Expect(err).NotTo(HaveOccurred())
+
+                               Expect(jc.(*JobsControllerImpl).jobs[j].config).To(Equal(config))
+                               Expect(jc.(*JobsControllerImpl).jobs[j].Updated).To(BeTemporally(">=", before))
+                               Expect(jc.(*JobsControllerImpl).jobs[j].Updated).To(BeTemporally("<=", after))
+                       })
+                       It("should return error for not existing job", func() {
+                               config := Config{JobName: "Test Job"}
+                               err := jc.SetConfig(initID+1, config)
+                               Expect(err).To(Equal(ErrJobNotFound))
+                       })
+               })
+               Describe("SetStatus", func() {
+                       allStatus := []JobStatus{JOB_NEW, JOB_PARSING, JOB_DOWNLOADING, JOB_WAITING, JOB_RUNNING, JOB_FAILED, JOB_CANCELED, JOB_COMPLETED}
+                       validChanges := map[JobStatus](map[JobStatus]bool){
+                               JOB_NEW: map[JobStatus]bool{
+                                       JOB_NEW:         true,
+                                       JOB_PARSING:     true,
+                                       JOB_DOWNLOADING: false,
+                                       JOB_WAITING:     false,
+                                       JOB_RUNNING:     false,
+                                       JOB_FAILED:      true,
+                                       JOB_CANCELED:    true,
+                                       JOB_COMPLETED:   false,
+                               },
+                               JOB_PARSING: map[JobStatus]bool{
+                                       JOB_NEW:         false,
+                                       JOB_PARSING:     true,
+                                       JOB_DOWNLOADING: true,
+                                       JOB_WAITING:     false,
+                                       JOB_RUNNING:     false,
+                                       JOB_FAILED:      true,
+                                       JOB_CANCELED:    true,
+                                       JOB_COMPLETED:   false,
+                               },
+                               JOB_DOWNLOADING: map[JobStatus]bool{
+                                       JOB_NEW:         false,
+                                       JOB_PARSING:     false,
+                                       JOB_DOWNLOADING: true,
+                                       JOB_WAITING:     true,
+                                       JOB_RUNNING:     false,
+                                       JOB_FAILED:      true,
+                                       JOB_CANCELED:    true,
+                                       JOB_COMPLETED:   false,
+                               },
+                               JOB_WAITING: map[JobStatus]bool{
+                                       JOB_NEW:         false,
+                                       JOB_PARSING:     false,
+                                       JOB_DOWNLOADING: false,
+                                       JOB_WAITING:     true,
+                                       JOB_RUNNING:     true,
+                                       JOB_FAILED:      true,
+                                       JOB_CANCELED:    true,
+                                       JOB_COMPLETED:   false,
+                               },
+                               JOB_RUNNING: map[JobStatus]bool{
+                                       JOB_NEW:         false,
+                                       JOB_PARSING:     false,
+                                       JOB_DOWNLOADING: false,
+                                       JOB_WAITING:     false,
+                                       JOB_RUNNING:     true,
+                                       JOB_FAILED:      true,
+                                       JOB_CANCELED:    true,
+                                       JOB_COMPLETED:   true,
+                               },
+                               JOB_FAILED: map[JobStatus]bool{
+                                       JOB_NEW:         false,
+                                       JOB_PARSING:     false,
+                                       JOB_DOWNLOADING: false,
+                                       JOB_WAITING:     false,
+                                       JOB_RUNNING:     false,
+                                       JOB_FAILED:      true,
+                                       JOB_CANCELED:    false,
+                                       JOB_COMPLETED:   false,
+                               },
+                               JOB_CANCELED: map[JobStatus]bool{
+                                       JOB_NEW:         false,
+                                       JOB_PARSING:     false,
+                                       JOB_DOWNLOADING: false,
+                                       JOB_WAITING:     false,
+                                       JOB_RUNNING:     false,
+                                       JOB_FAILED:      false,
+                                       JOB_CANCELED:    true,
+                                       JOB_COMPLETED:   false,
+                               },
+                               JOB_COMPLETED: map[JobStatus]bool{
+                                       JOB_NEW:         false,
+                                       JOB_PARSING:     false,
+                                       JOB_DOWNLOADING: false,
+                                       JOB_WAITING:     false,
+                                       JOB_RUNNING:     false,
+                                       JOB_FAILED:      false,
+                                       JOB_CANCELED:    false,
+                                       JOB_COMPLETED:   true,
+                               },
+                       }
+                       It("should return error for not existing job", func() {
+                               for _, status := range allStatus {
+                                       err := jc.SetStatusAndInfo(initID+1, status, "test info")
+                                       Expect(err).To(Equal(ErrJobNotFound))
+                               }
+                       })
+                       It("should fail to set new status if change is not valid", func() {
+                               j, err := jc.NewJob(yaml)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(j).To(Equal(initID + 1))
+
+                               updated := jc.(*JobsControllerImpl).jobs[j].Updated
+                               oldinfo := "test info"
+                               jc.(*JobsControllerImpl).jobs[j].Info = oldinfo
+                               for _, oldStatus := range allStatus {
+                                       for _, newStatus := range allStatus {
+                                               if !validChanges[oldStatus][newStatus] {
+                                                       jc.(*JobsControllerImpl).jobs[j].Status = oldStatus
+                                                       info := fmt.Sprintf("test info oldStatus=%s, newStatus=%s", oldStatus, newStatus)
+                                                       err = jc.SetStatusAndInfo(j, newStatus, info)
+                                                       Expect(err).To(Equal(ErrJobStatusChangeNotAllowed), "oldStatus=%v, newStatus=%v\n", oldStatus, newStatus)
+                                                       Expect(jc.(*JobsControllerImpl).jobs[j].Status).To(Equal(oldStatus), "oldStatus=%v, newStatus=%v\n", oldStatus, newStatus)
+                                                       Expect(jc.(*JobsControllerImpl).jobs[j].Info).To(Equal(oldinfo), "oldStatus=%v, newStatus=%v\n", oldStatus, newStatus)
+                                                       Expect(jc.(*JobsControllerImpl).jobs[j].Updated).To(BeTemporally("==", updated), "oldStatus=%v, newStatus=%v\n", oldStatus, newStatus)
+                                               }
+                                       }
+                               }
+                       })
+                       It("should change status if change is valid", func() {
+                               j, err := jc.NewJob(yaml)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(j).To(Equal(initID + 1))
+
+                               for _, oldStatus := range allStatus {
+                                       for _, newStatus := range allStatus {
+                                               if validChanges[oldStatus][newStatus] {
+                                                       updated := jc.(*JobsControllerImpl).jobs[j].Updated
+                                                       jc.(*JobsControllerImpl).jobs[j].Status = oldStatus
+                                                       info := fmt.Sprintf("test info oldStatus=%s, newStatus=%s", oldStatus, newStatus)
+                                                       err = jc.SetStatusAndInfo(j, newStatus, info)
+                                                       Expect(err).NotTo(HaveOccurred(), "oldStatus=%v, newStatus=%v\n", oldStatus, newStatus)
+                                                       Expect(jc.(*JobsControllerImpl).jobs[j].Status).To(Equal(newStatus), "oldStatus=%v, newStatus=%v\n", oldStatus, newStatus)
+                                                       Expect(jc.(*JobsControllerImpl).jobs[j].Info).To(Equal(info), "oldStatus=%v, newStatus=%v\n", oldStatus, newStatus)
+                                                       Expect(jc.(*JobsControllerImpl).jobs[j].Updated).To(BeTemporally(">=", updated), "oldStatus=%v, newStatus=%v\n", oldStatus, newStatus)
+                                               }
+                                       }
+                               }
+                       })
+               })
+               Describe("GetConfig", func() {
+                       It("should return proper config for existing job", func() {
+                               j, err := jc.NewJob(yaml)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(j).To(Equal(initID + 1))
+
+                               expectedConfig := Config{JobName: "Test config"}
+                               err = jc.SetConfig(j, expectedConfig)
+                               Expect(err).NotTo(HaveOccurred())
+
+                               config, err := jc.GetConfig(j)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(config).To(Equal(expectedConfig))
+                       })
+                       It("should return error for not existing job", func() {
+                               config, err := jc.GetConfig(initID + 1)
+                               Expect(err).To(Equal(ErrJobNotFound))
+                               Expect(config).To(BeZero())
+                       })
+               })
+
+               Describe("SetDryad", func() {
+                       It("should set Dryad for existing job", func() {
+                               j, err := jc.NewJob(yaml)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(j).To(Equal(initID + 1))
+
+                               dryad := Dryad{Addr: &net.IPNet{IP: net.IPv4(1, 2, 3, 4), Mask: net.IPv4Mask(5, 6, 7, 8)}}
+                               err = jc.SetDryad(j, dryad)
+                               Expect(err).NotTo(HaveOccurred())
+
+                               Expect(jc.(*JobsControllerImpl).jobs[j].dryad).To(Equal(dryad))
+                       })
+                       It("should return error for not existing job", func() {
+                               dryad := Dryad{Addr: &net.IPNet{IP: net.IPv4(1, 2, 3, 4), Mask: net.IPv4Mask(5, 6, 7, 8)}}
+                               err := jc.SetDryad(initID+1, dryad)
+                               Expect(err).To(Equal(ErrJobNotFound))
+                       })
+               })
+
+               Describe("GetDryad", func() {
+                       It("should return proper Dryad structure for existing job", func() {
+                               j, err := jc.NewJob(yaml)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(j).To(Equal(initID + 1))
+
+                               expectedDryad := Dryad{Addr: &net.IPNet{IP: net.IPv4(1, 2, 3, 4), Mask: net.IPv4Mask(5, 6, 7, 8)}}
+                               err = jc.SetDryad(j, expectedDryad)
+                               Expect(err).NotTo(HaveOccurred())
+
+                               dryad, err := jc.GetDryad(j)
+                               Expect(err).NotTo(HaveOccurred())
+                               Expect(dryad).To(Equal(expectedDryad))
+                       })
+                       It("should return error for not existing job", func() {
+                               dryad, err := jc.GetDryad(initID + 1)
+                               Expect(err).To(Equal(ErrJobNotFound))
+                               Expect(dryad).To(BeZero())
+                       })
+               })
+       })
+})