--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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())
+ })
+ })
+ })
+})