--- /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 matcher/validmatcher.go provides ValidMatcher structure which implements
+// Matcher interface. It should be used for handling events caused by validation
+// of requests after ValidAfter time is passed.
+
+package matcher
+
+import (
+ "time"
+
+ . "git.tizen.org/tools/boruta"
+)
+
+// ValidMatcher implements Matcher interface for handling requests validation.
+type ValidMatcher struct {
+ Matcher
+ // requests provides internal boruta access to requests.
+ requests RequestsManager
+ // workers provides internal boruta access to workers.
+ workers WorkersManager
+ // jobs provides internal boruta access to jobs.
+ jobs JobsManager
+}
+
+// NewValidMatcher creates a new ValidMatcher structure.
+func NewValidMatcher(r RequestsManager, w WorkersManager, j JobsManager) *ValidMatcher {
+ return &ValidMatcher{
+ requests: r,
+ workers: w,
+ jobs: j,
+ }
+}
+
+// Notify implements Matcher interface. This method reacts on events passed to
+// matcher. In this implementation requests' IDs are ignored as requests must be
+// matched in order they are placed in requests priority queue.
+func (m ValidMatcher) Notify([]ReqID) {
+ // Repeat verification until iterateRequests() returns false indicating that
+ // there is no more job to be done.
+ for m.iterateRequests() {
+ }
+}
+
+// iterateRequests visits all requests in order they are placed in requests
+// priority queue, verifies if they can be run and tries to match an idle worker.
+// Method returns true if iteration should be repeated or false if there is
+// nothing more to be done.
+func (m ValidMatcher) iterateRequests() bool {
+
+ err := m.requests.InitIteration()
+ if err != nil {
+ // TODO log critical logic error. InitIterations should return no error
+ // as no iterations should by run by any other goroutine.
+ panic("Critical logic error. No iterations over requests collection should be running.")
+ }
+ defer m.requests.TerminateIteration()
+
+ now := time.Now()
+ // Iterate on requests priority queue.
+ for rid, rok := m.requests.Next(); rok; rid, rok = m.requests.Next() {
+ // Verify if request is ready to be run.
+ if !m.requests.VerifyIfReady(rid, now) {
+ continue
+ }
+ // Request is ready to be run. Get full information about it.
+ req, err := m.requests.Get(rid)
+ if err != nil {
+ continue
+ }
+
+ // Try finding an idle worker matching requests requirements.
+ if m.matchWorkers(req) {
+ // A match was made. Restarting iterations to process other requests.
+ return true
+ }
+ }
+ // All requests have been analyzed. No repetition is required.
+ return false
+}
+
+// matchWorkers tries to find the best of the idle workers matching capabilities
+// and groups of the requests. Best worker is the one with least matching penalty.
+// If such worker is found a job is created and the request is processed.
+func (m ValidMatcher) matchWorkers(req ReqInfo) bool {
+
+ worker, err := m.workers.TakeBestMatchingWorker(req.Owner.Groups, req.Caps)
+ if err != nil {
+ // No matching worker was found.
+ return false
+ }
+ // Match found.
+ err = m.jobs.Create(req.ID, worker)
+ if err != nil {
+ // TODO log error.
+ goto fail
+ }
+ err = m.requests.Run(req.ID, worker)
+ if err != nil {
+ // TODO log error.
+ goto fail
+ }
+ return true
+
+fail:
+ // Creating job failed. Bringing worker back to IDLE state.
+ m.workers.PrepareWorker(worker, false)
+ return false
+}
--- /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 matcher
+
+import (
+ "errors"
+ "time"
+
+ . "git.tizen.org/tools/boruta"
+ "git.tizen.org/tools/boruta/workers"
+
+ gomock "github.com/golang/mock/gomock"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("ValidMatcher", func() {
+ var ctrl *gomock.Controller
+ var r *MockRequestsManager
+ var w *MockWorkersManager
+ var j *MockJobsManager
+ var m Matcher
+ var pre time.Time
+
+ zeroReq := ReqID(0)
+ req := ReqID(101)
+ groups := Groups{"A", "B", "C"}
+ caps := Capabilities{"keyX": "valX", "keyY": "valY", "keyZ": "valZ"}
+ worker := WorkerUUID("Test worker")
+ reqInfo := ReqInfo{ID: req, Caps: caps, Owner: UserInfo{Groups: groups}}
+
+ checkTime := func(_ ReqID, t time.Time) {
+ Expect(t).To(BeTemporally(">=", pre))
+ Expect(t).To(BeTemporally("<=", time.Now()))
+ }
+
+ BeforeEach(func() {
+ ctrl = gomock.NewController(GinkgoT())
+ r = NewMockRequestsManager(ctrl)
+ w = NewMockWorkersManager(ctrl)
+ j = NewMockJobsManager(ctrl)
+ pre = time.Now()
+ })
+ AfterEach(func() {
+ ctrl.Finish()
+ })
+ Describe("NewValidMatcher", func() {
+ It("should not use requests, workers nor jobs", func() {
+ m := NewValidMatcher(r, w, j)
+ Expect(m).NotTo(BeNil())
+ })
+ })
+ Describe("Notify", func() {
+ BeforeEach(func() {
+ m = NewValidMatcher(r, w, j)
+ })
+ It("should not iterate over requests when InitIteration fails", func() {
+ anyError := errors.New("test error")
+ r.EXPECT().InitIteration().Return(anyError)
+
+ Expect(func() {
+ m.Notify(nil)
+ }).To(Panic())
+ })
+ It("should run only Lock, Unlock, First on empty requests", func() {
+ gomock.InOrder(
+ r.EXPECT().InitIteration(),
+ r.EXPECT().Next().Return(zeroReq, false),
+ r.EXPECT().TerminateIteration(),
+ )
+
+ m.Notify(nil)
+ })
+ It("should ignore not-ready requests", func() {
+ gomock.InOrder(
+ r.EXPECT().InitIteration(),
+ r.EXPECT().Next().Return(req, true),
+ r.EXPECT().VerifyIfReady(req, gomock.Any()).Do(checkTime).Return(false),
+ r.EXPECT().Next().Return(zeroReq, false),
+ r.EXPECT().TerminateIteration(),
+ )
+
+ m.Notify(nil)
+ })
+ It("should continue iterating over requests when Get fails", func() {
+ gomock.InOrder(
+ r.EXPECT().InitIteration(),
+ r.EXPECT().Next().Return(req, true),
+ r.EXPECT().VerifyIfReady(req, gomock.Any()).Do(checkTime).Return(true),
+ r.EXPECT().Get(req).Return(ReqInfo{}, NotFoundError("Request")),
+ r.EXPECT().Next().Return(zeroReq, false),
+ r.EXPECT().TerminateIteration(),
+ )
+
+ m.Notify(nil)
+ })
+ It("should match workers when Get succeeds", func() {
+ gomock.InOrder(
+ r.EXPECT().InitIteration(),
+ r.EXPECT().Next().Return(req, true),
+ r.EXPECT().VerifyIfReady(req, gomock.Any()).Do(checkTime).Return(true),
+ r.EXPECT().Get(req).Return(reqInfo, nil),
+ w.EXPECT().TakeBestMatchingWorker(groups, caps).Return(WorkerUUID(""), workers.ErrWorkerNotFound),
+ r.EXPECT().Next().Return(zeroReq, false),
+ r.EXPECT().TerminateIteration(),
+ )
+
+ m.Notify(nil)
+ })
+ It("should prepare worker without key generation when job creation fails", func() {
+ gomock.InOrder(
+ r.EXPECT().InitIteration(),
+ r.EXPECT().Next().Return(req, true),
+ r.EXPECT().VerifyIfReady(req, gomock.Any()).Do(checkTime).Return(true),
+ r.EXPECT().Get(req).Return(reqInfo, nil),
+ w.EXPECT().TakeBestMatchingWorker(groups, caps).Return(worker, nil),
+ j.EXPECT().Create(req, worker).Return(ErrJobAlreadyExists),
+ w.EXPECT().PrepareWorker(worker, false),
+ r.EXPECT().Next().Return(zeroReq, false),
+ r.EXPECT().TerminateIteration(),
+ )
+
+ m.Notify(nil)
+ })
+ It("should prepare worker without key generation when running request fails", func() {
+ gomock.InOrder(
+ r.EXPECT().InitIteration(),
+ r.EXPECT().Next().Return(req, true),
+ r.EXPECT().VerifyIfReady(req, gomock.Any()).Do(checkTime).Return(true),
+ r.EXPECT().Get(req).Return(reqInfo, nil),
+ w.EXPECT().TakeBestMatchingWorker(groups, caps).Return(worker, nil),
+ j.EXPECT().Create(req, worker),
+ r.EXPECT().Run(req, worker).Return(NotFoundError("Request")),
+ w.EXPECT().PrepareWorker(worker, false),
+ r.EXPECT().Next().Return(zeroReq, false),
+ r.EXPECT().TerminateIteration(),
+ )
+
+ m.Notify(nil)
+ })
+ It("should create job when match is found and run request", func() {
+ gomock.InOrder(
+ r.EXPECT().InitIteration(),
+ r.EXPECT().Next().Return(req, true),
+ r.EXPECT().VerifyIfReady(req, gomock.Any()).Do(checkTime).Return(true),
+ r.EXPECT().Get(req).Return(reqInfo, nil),
+ w.EXPECT().TakeBestMatchingWorker(groups, caps).Return(worker, nil),
+ j.EXPECT().Create(req, worker),
+ r.EXPECT().Run(req, worker),
+ r.EXPECT().TerminateIteration(),
+ r.EXPECT().InitIteration(),
+ r.EXPECT().Next().Return(zeroReq, false),
+ r.EXPECT().TerminateIteration(),
+ )
+
+ m.Notify(nil)
+ })
+ })
+})