Add JobsController implementation with tests 10/162010/8
authorLukasz Wojciechowski <l.wojciechow@partner.samsung.com>
Thu, 9 Nov 2017 17:00:25 +0000 (18:00 +0100)
committerPawel Wieczorek <p.wieczorek2@samsung.com>
Wed, 18 Apr 2018 16:32:31 +0000 (18:32 +0200)
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.

Patches enhancing tests, written by Aleksander Mistewicz squashed into
this patch.

Change-Id: Ie3e3c46660c650c48cd80a93038b8cdd05f3fc21
Signed-off-by: Lukasz Wojciechowski <l.wojciechow@partner.samsung.com>
Signed-off-by: Aleksander Mistewicz <a.mistewicz@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..23c198e
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ *  Copyright (c) 2017-2018 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..6878a6b
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ *  Copyright (c) 2017-2018 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 weles.JobID
+       // jobs stores information about Weles' Jobs.
+       jobs map[weles.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 = weles.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[weles.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() weles.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) (weles.JobID, error) {
+       js.mutex.Lock()
+       defer js.mutex.Unlock()
+
+       j := js.nextID()
+
+       now := time.Now()
+       js.jobs[j] = &Job{
+               JobInfo: weles.JobInfo{
+                       JobID:   j,
+                       Created: now,
+                       Updated: now,
+                       Status:  weles.JOB_NEW,
+               },
+               yaml: yaml,
+       }
+
+       // TODO save struct in DB
+
+       return j, nil
+}
+
+// GetYaml returns yaml Job description.
+func (js *JobsControllerImpl) GetYaml(j weles.JobID) ([]byte, error) {
+       js.mutex.RLock()
+       defer js.mutex.RUnlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return nil, weles.ErrJobNotFound
+       }
+
+       return job.yaml, nil
+}
+
+// SetConfig stores config in Jobs structure.
+func (js *JobsControllerImpl) SetConfig(j weles.JobID, conf weles.Config) error {
+       js.mutex.Lock()
+       defer js.mutex.Unlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return weles.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 weles.JobStatus) bool {
+       if oldStatus == newStatus {
+               return true
+       }
+       switch oldStatus {
+       case weles.JOB_NEW:
+               switch newStatus {
+               case weles.JOB_PARSING, weles.JOB_CANCELED, weles.JOB_FAILED:
+                       return true
+               }
+       case weles.JOB_PARSING:
+               switch newStatus {
+               case weles.JOB_DOWNLOADING, weles.JOB_CANCELED, weles.JOB_FAILED:
+                       return true
+               }
+       case weles.JOB_DOWNLOADING:
+               switch newStatus {
+               case weles.JOB_WAITING, weles.JOB_CANCELED, weles.JOB_FAILED:
+                       return true
+               }
+       case weles.JOB_WAITING:
+               switch newStatus {
+               case weles.JOB_RUNNING, weles.JOB_CANCELED, weles.JOB_FAILED:
+                       return true
+               }
+       case weles.JOB_RUNNING:
+               switch newStatus {
+               case weles.JOB_COMPLETED, weles.JOB_CANCELED, weles.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 weles.JobID, newStatus weles.JobStatus, msg string) error {
+       js.mutex.Lock()
+       defer js.mutex.Unlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return weles.ErrJobNotFound
+       }
+
+       if !isStatusChangeValid(job.Status, newStatus) {
+               return weles.ErrJobStatusChangeNotAllowed
+       }
+
+       job.Status = newStatus
+       job.Info = msg
+       job.Updated = time.Now()
+       return nil
+}
+
+// GetConfig returns Job's config.
+func (js *JobsControllerImpl) GetConfig(j weles.JobID) (weles.Config, error) {
+       js.mutex.RLock()
+       defer js.mutex.RUnlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return weles.Config{}, weles.ErrJobNotFound
+       }
+
+       return job.config, nil
+}
+
+// SetDryad saves access info for acquired Dryad.
+func (js *JobsControllerImpl) SetDryad(j weles.JobID, d weles.Dryad) error {
+       js.mutex.Lock()
+       defer js.mutex.Unlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return weles.ErrJobNotFound
+       }
+
+       job.dryad = d
+       return nil
+}
+
+// GetDryad returns Dryad acquired for the Job.
+func (js *JobsControllerImpl) GetDryad(j weles.JobID) (weles.Dryad, error) {
+       js.mutex.RLock()
+       defer js.mutex.RUnlock()
+
+       job, ok := js.jobs[j]
+       if !ok {
+               return weles.Dryad{}, weles.ErrJobNotFound
+       }
+
+       return job.dryad, nil
+}
diff --git a/controller/jobscontrollerimpl_test.go b/controller/jobscontrollerimpl_test.go
new file mode 100644 (file)
index 0000000..2d3613d
--- /dev/null
@@ -0,0 +1,249 @@
+/*
+ *  Copyright (c) 2017-2018 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, j weles.JobID
+               ipAddr := &net.IPNet{IP: net.IPv4(1, 2, 3, 4), Mask: net.IPv4Mask(5, 6, 7, 8)}
+               yaml := []byte("test yaml")
+               var invalidID weles.JobID
+
+               BeforeEach(func() {
+                       jc = NewJobsController()
+                       initID = jc.(*JobsControllerImpl).lastID
+
+                       var err error
+                       j, err = jc.NewJob(yaml)
+                       Expect(err).NotTo(HaveOccurred())
+                       Expect(j).To(Equal(initID + 1))
+
+                       invalidID = initID - 1
+               })
+               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 + 2))
+
+                               Expect(jc.(*JobsControllerImpl).lastID).To(Equal(j))
+                               Expect(len(jc.(*JobsControllerImpl).jobs)).To(Equal(2))
+
+                               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(weles.JOB_NEW))
+                               Expect(job.yaml).To(Equal(yaml))
+                       })
+               })
+               Describe("GetYaml", func() {
+                       It("should return proper yaml for existing job", func() {
+                               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(invalidID)
+                               Expect(err).To(Equal(weles.ErrJobNotFound))
+                               Expect(yaml).To(BeZero())
+                       })
+               })
+               Describe("SetStatus", func() {
+                       allStatus := []weles.JobStatus{
+                               weles.JOB_NEW,
+                               weles.JOB_PARSING,
+                               weles.JOB_DOWNLOADING,
+                               weles.JOB_WAITING,
+                               weles.JOB_RUNNING,
+                               weles.JOB_FAILED,
+                               weles.JOB_CANCELED,
+                               weles.JOB_COMPLETED,
+                       }
+                       validChanges := map[weles.JobStatus](map[weles.JobStatus]bool){
+                               weles.JOB_NEW: map[weles.JobStatus]bool{
+                                       weles.JOB_NEW:      true,
+                                       weles.JOB_PARSING:  true,
+                                       weles.JOB_FAILED:   true,
+                                       weles.JOB_CANCELED: true,
+                               },
+                               weles.JOB_PARSING: map[weles.JobStatus]bool{
+                                       weles.JOB_PARSING:     true,
+                                       weles.JOB_DOWNLOADING: true,
+                                       weles.JOB_FAILED:      true,
+                                       weles.JOB_CANCELED:    true,
+                               },
+                               weles.JOB_DOWNLOADING: map[weles.JobStatus]bool{
+                                       weles.JOB_DOWNLOADING: true,
+                                       weles.JOB_WAITING:     true,
+                                       weles.JOB_FAILED:      true,
+                                       weles.JOB_CANCELED:    true,
+                               },
+                               weles.JOB_WAITING: map[weles.JobStatus]bool{
+                                       weles.JOB_WAITING:  true,
+                                       weles.JOB_RUNNING:  true,
+                                       weles.JOB_FAILED:   true,
+                                       weles.JOB_CANCELED: true,
+                               },
+                               weles.JOB_RUNNING: map[weles.JobStatus]bool{
+                                       weles.JOB_RUNNING:   true,
+                                       weles.JOB_FAILED:    true,
+                                       weles.JOB_CANCELED:  true,
+                                       weles.JOB_COMPLETED: true,
+                               },
+                               weles.JOB_FAILED: map[weles.JobStatus]bool{
+                                       weles.JOB_FAILED: true,
+                               },
+                               weles.JOB_CANCELED: map[weles.JobStatus]bool{
+                                       weles.JOB_CANCELED: true,
+                               },
+                               weles.JOB_COMPLETED: map[weles.JobStatus]bool{
+                                       weles.JOB_COMPLETED: true,
+                               },
+                       }
+                       It("should return error for not existing job", func() {
+                               for _, status := range allStatus {
+                                       err := jc.SetStatusAndInfo(invalidID, status, "test info")
+                                       Expect(err).To(Equal(weles.ErrJobNotFound))
+                               }
+                       })
+                       It("should work to change status only for valid transitions", func() {
+                               job := jc.(*JobsControllerImpl).jobs[j]
+                               for _, oldStatus := range allStatus {
+                                       for _, newStatus := range allStatus {
+                                               job.Status = oldStatus
+                                               if _, ok := validChanges[oldStatus][newStatus]; !ok {
+                                                       info := fmt.Sprintf("failing to change from '%s' to '%s'", oldStatus, newStatus)
+                                                       By(info, func() {
+                                                               oldJob := *job
+                                                               err := jc.SetStatusAndInfo(j, newStatus, info)
+                                                               Expect(err).To(Equal(weles.ErrJobStatusChangeNotAllowed))
+                                                               Expect(job).To(Equal(&oldJob))
+                                                       })
+                                               } else {
+                                                       info := fmt.Sprintf("changing from '%s' to '%s'", oldStatus, newStatus)
+                                                       oldUpdated := job.Updated
+                                                       By(info, func() {
+                                                               err := jc.SetStatusAndInfo(j, newStatus, info)
+                                                               Expect(err).NotTo(HaveOccurred())
+                                                               Expect(job.Status).To(Equal(newStatus))
+                                                               Expect(job.Info).To(Equal(info))
+                                                               Expect(job.Updated).To(BeTemporally(">=", oldUpdated))
+                                                       })
+                                               }
+                                       }
+                               }
+                       })
+               })
+               Describe("SetConfig", func() {
+                       It("should set config for existing job", func() {
+                               config := weles.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 := weles.Config{JobName: "Test Job"}
+                               err := jc.SetConfig(invalidID, config)
+                               Expect(err).To(Equal(weles.ErrJobNotFound))
+                       })
+               })
+               Describe("GetConfig", func() {
+                       It("should return proper config for existing job", func() {
+                               expectedConfig := weles.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(invalidID)
+                               Expect(err).To(Equal(weles.ErrJobNotFound))
+                               Expect(config).To(BeZero())
+                       })
+               })
+
+               Describe("SetDryad", func() {
+                       It("should set Dryad for existing job", func() {
+                               dryad := weles.Dryad{Addr: ipAddr}
+                               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 := weles.Dryad{Addr: ipAddr}
+                               err := jc.SetDryad(invalidID, dryad)
+                               Expect(err).To(Equal(weles.ErrJobNotFound))
+                       })
+               })
+
+               Describe("GetDryad", func() {
+                       It("should return proper Dryad structure for existing job", func() {
+                               expectedDryad := weles.Dryad{Addr: ipAddr}
+                               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(invalidID)
+                               Expect(err).To(Equal(weles.ErrJobNotFound))
+                               Expect(dryad).To(BeZero())
+                       })
+               })
+       })
+})