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