Add ValidMatcher with tests
authorLukasz Wojciechowski <l.wojciechow@partner.samsung.com>
Fri, 6 Oct 2017 19:34:06 +0000 (21:34 +0200)
committerLukasz Wojciechowski <l.wojciechow@partner.samsung.com>
Fri, 27 Apr 2018 15:34:20 +0000 (17:34 +0200)
ValidMatcher is a Matcher interface implementation for handling events
related to validation of requests after ValidAfter time is passed.
It matches pending, ready to be run requests with idle workers that are
capable to fulfill request capabilities and belong to group for which
request owner has rights.

Tests base on using MockRequestsManager, MockWorkersManager
and MockJobsManager for mocking up RequestsManager, WorkersManager
and JobsManager interfaces.

Change-Id: Ib654f1ef276eecb14dc4ad3114afcccd83a7bf5d
Signed-off-by: Lukasz Wojciechowski <l.wojciechow@partner.samsung.com>
matcher/validmatcher.go [new file with mode: 0644]
matcher/validmatcher_test.go [new file with mode: 0644]

diff --git a/matcher/validmatcher.go b/matcher/validmatcher.go
new file mode 100644 (file)
index 0000000..c89abf1
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ *  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
+}
diff --git a/matcher/validmatcher_test.go b/matcher/validmatcher_test.go
new file mode 100644 (file)
index 0000000..f642daf
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ *  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)
+               })
+       })
+})