taskflow 5.7.0__py3-none-any.whl → 5.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- taskflow/engines/action_engine/engine.py +18 -6
- taskflow/engines/action_engine/process_executor.py +2 -0
- taskflow/jobs/backends/impl_etcd.py +610 -0
- taskflow/test.py +0 -26
- taskflow/tests/unit/action_engine/test_creation.py +9 -3
- taskflow/tests/unit/action_engine/test_process_executor.py +19 -12
- taskflow/tests/unit/jobs/test_etcd_job.py +421 -0
- taskflow/tests/unit/test_arguments_passing.py +6 -0
- taskflow/tests/unit/test_engines.py +6 -0
- taskflow/tests/unit/test_retries.py +6 -0
- taskflow/tests/unit/test_suspend.py +6 -0
- taskflow/tests/utils.py +10 -0
- {taskflow-5.7.0.dist-info → taskflow-5.9.0.dist-info}/AUTHORS +1 -0
- {taskflow-5.7.0.dist-info → taskflow-5.9.0.dist-info}/METADATA +7 -5
- {taskflow-5.7.0.dist-info → taskflow-5.9.0.dist-info}/RECORD +20 -18
- {taskflow-5.7.0.dist-info → taskflow-5.9.0.dist-info}/entry_points.txt +1 -0
- taskflow-5.9.0.dist-info/pbr.json +1 -0
- taskflow-5.7.0.dist-info/pbr.json +0 -1
- {taskflow-5.7.0.dist-info → taskflow-5.9.0.dist-info}/LICENSE +0 -0
- {taskflow-5.7.0.dist-info → taskflow-5.9.0.dist-info}/WHEEL +0 -0
- {taskflow-5.7.0.dist-info → taskflow-5.9.0.dist-info}/top_level.txt +0 -0
taskflow/test.py
CHANGED
|
@@ -19,9 +19,7 @@ from unittest import mock
|
|
|
19
19
|
import fixtures
|
|
20
20
|
from oslotest import base
|
|
21
21
|
|
|
22
|
-
from testtools import compat
|
|
23
22
|
from testtools import matchers
|
|
24
|
-
from testtools import testcase
|
|
25
23
|
|
|
26
24
|
from taskflow import exceptions
|
|
27
25
|
from taskflow.tests import fixtures as taskflow_fixtures
|
|
@@ -122,30 +120,6 @@ class TestCase(base.BaseTestCase):
|
|
|
122
120
|
|
|
123
121
|
self.assertRaises(exc_class, access_func)
|
|
124
122
|
|
|
125
|
-
def assertRaisesRegex(self, exc_class, pattern, callable_obj,
|
|
126
|
-
*args, **kwargs):
|
|
127
|
-
# TODO(harlowja): submit a pull/review request to testtools to add
|
|
128
|
-
# this method to there codebase instead of having it exist in ours
|
|
129
|
-
# since it really doesn't belong here.
|
|
130
|
-
|
|
131
|
-
class ReRaiseOtherTypes(object):
|
|
132
|
-
def match(self, matchee):
|
|
133
|
-
if not issubclass(matchee[0], exc_class):
|
|
134
|
-
compat.reraise(*matchee)
|
|
135
|
-
|
|
136
|
-
class CaptureMatchee(object):
|
|
137
|
-
def match(self, matchee):
|
|
138
|
-
self.matchee = matchee[1]
|
|
139
|
-
|
|
140
|
-
capture = CaptureMatchee()
|
|
141
|
-
matcher = matchers.Raises(matchers.MatchesAll(ReRaiseOtherTypes(),
|
|
142
|
-
matchers.MatchesException(exc_class,
|
|
143
|
-
pattern),
|
|
144
|
-
capture))
|
|
145
|
-
our_callable = testcase.Nullary(callable_obj, *args, **kwargs)
|
|
146
|
-
self.assertThat(our_callable, matcher)
|
|
147
|
-
return capture.matchee
|
|
148
|
-
|
|
149
123
|
def assertGreater(self, first, second):
|
|
150
124
|
matcher = matchers.GreaterThan(first)
|
|
151
125
|
self.assertThat(second, matcher)
|
|
@@ -19,7 +19,6 @@ import testtools
|
|
|
19
19
|
|
|
20
20
|
from taskflow.engines.action_engine import engine
|
|
21
21
|
from taskflow.engines.action_engine import executor
|
|
22
|
-
from taskflow.engines.action_engine import process_executor
|
|
23
22
|
from taskflow.patterns import linear_flow as lf
|
|
24
23
|
from taskflow.persistence import backends
|
|
25
24
|
from taskflow import test
|
|
@@ -27,6 +26,11 @@ from taskflow.tests import utils
|
|
|
27
26
|
from taskflow.utils import eventlet_utils as eu
|
|
28
27
|
from taskflow.utils import persistence_utils as pu
|
|
29
28
|
|
|
29
|
+
try:
|
|
30
|
+
from taskflow.engines.action_engine import process_executor as pe
|
|
31
|
+
except ImportError:
|
|
32
|
+
pe = None
|
|
33
|
+
|
|
30
34
|
|
|
31
35
|
class ParallelCreationTest(test.TestCase):
|
|
32
36
|
@staticmethod
|
|
@@ -44,11 +48,12 @@ class ParallelCreationTest(test.TestCase):
|
|
|
44
48
|
self.assertIsInstance(eng._task_executor,
|
|
45
49
|
executor.ParallelThreadTaskExecutor)
|
|
46
50
|
|
|
51
|
+
@testtools.skipIf(pe is None, 'process_executor is not available')
|
|
47
52
|
def test_process_string_creation(self):
|
|
48
53
|
for s in ['process', 'processes']:
|
|
49
54
|
eng = self._create_engine(executor=s)
|
|
50
55
|
self.assertIsInstance(eng._task_executor,
|
|
51
|
-
|
|
56
|
+
pe.ParallelProcessTaskExecutor)
|
|
52
57
|
|
|
53
58
|
def test_thread_executor_creation(self):
|
|
54
59
|
with futurist.ThreadPoolExecutor(1) as e:
|
|
@@ -56,11 +61,12 @@ class ParallelCreationTest(test.TestCase):
|
|
|
56
61
|
self.assertIsInstance(eng._task_executor,
|
|
57
62
|
executor.ParallelThreadTaskExecutor)
|
|
58
63
|
|
|
64
|
+
@testtools.skipIf(pe is None, 'process_executor is not available')
|
|
59
65
|
def test_process_executor_creation(self):
|
|
60
66
|
with futurist.ProcessPoolExecutor(1) as e:
|
|
61
67
|
eng = self._create_engine(executor=e)
|
|
62
68
|
self.assertIsInstance(eng._task_executor,
|
|
63
|
-
|
|
69
|
+
pe.ParallelProcessTaskExecutor)
|
|
64
70
|
|
|
65
71
|
@testtools.skipIf(not eu.EVENTLET_AVAILABLE, 'eventlet is not available')
|
|
66
72
|
def test_green_executor_creation(self):
|
|
@@ -13,19 +13,26 @@
|
|
|
13
13
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
14
14
|
# License for the specific language governing permissions and limitations
|
|
15
15
|
# under the License.
|
|
16
|
-
|
|
17
|
-
import asyncore
|
|
18
16
|
import errno
|
|
19
17
|
import socket
|
|
20
18
|
import threading
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
import testtools
|
|
21
|
+
|
|
23
22
|
from taskflow import task
|
|
24
23
|
from taskflow import test
|
|
25
24
|
from taskflow.test import mock
|
|
26
25
|
from taskflow.tests import utils as test_utils
|
|
27
26
|
|
|
27
|
+
try:
|
|
28
|
+
import asyncore
|
|
29
|
+
from taskflow.engines.action_engine import process_executor as pe
|
|
30
|
+
except ImportError:
|
|
31
|
+
asyncore = None
|
|
32
|
+
pe = None
|
|
33
|
+
|
|
28
34
|
|
|
35
|
+
@testtools.skipIf(asyncore is None, 'process_executor is not available')
|
|
29
36
|
class ProcessExecutorHelpersTest(test.TestCase):
|
|
30
37
|
def test_reader(self):
|
|
31
38
|
capture_buf = []
|
|
@@ -33,8 +40,8 @@ class ProcessExecutorHelpersTest(test.TestCase):
|
|
|
33
40
|
def do_capture(identity, message_capture_func):
|
|
34
41
|
capture_buf.append(message_capture_func())
|
|
35
42
|
|
|
36
|
-
r =
|
|
37
|
-
for data in
|
|
43
|
+
r = pe.Reader(b"secret", do_capture)
|
|
44
|
+
for data in pe._encode_message(b"secret", ['hi'], b'me'):
|
|
38
45
|
self.assertEqual(len(data), r.bytes_needed)
|
|
39
46
|
r.feed(data)
|
|
40
47
|
|
|
@@ -42,9 +49,9 @@ class ProcessExecutorHelpersTest(test.TestCase):
|
|
|
42
49
|
self.assertEqual(['hi'], capture_buf[0])
|
|
43
50
|
|
|
44
51
|
def test_bad_hmac_reader(self):
|
|
45
|
-
r =
|
|
46
|
-
in_data = b"".join(
|
|
47
|
-
self.assertRaises(
|
|
52
|
+
r = pe.Reader(b"secret-2", lambda ident, capture_func: capture_func())
|
|
53
|
+
in_data = b"".join(pe._encode_message(b"secret", ['hi'], b'me'))
|
|
54
|
+
self.assertRaises(pe.BadHmacValueError, r.feed, in_data)
|
|
48
55
|
|
|
49
56
|
@mock.patch("socket.socket")
|
|
50
57
|
def test_no_connect_channel(self, mock_socket_factory):
|
|
@@ -52,7 +59,7 @@ class ProcessExecutorHelpersTest(test.TestCase):
|
|
|
52
59
|
mock_socket_factory.return_value = mock_sock
|
|
53
60
|
mock_sock.connect.side_effect = socket.error(errno.ECONNREFUSED,
|
|
54
61
|
'broken')
|
|
55
|
-
c =
|
|
62
|
+
c = pe.Channel(2222, b"me", b"secret")
|
|
56
63
|
self.assertRaises(socket.error, c.send, "hi")
|
|
57
64
|
self.assertTrue(c.dead)
|
|
58
65
|
self.assertTrue(mock_sock.close.called)
|
|
@@ -65,7 +72,7 @@ class ProcessExecutorHelpersTest(test.TestCase):
|
|
|
65
72
|
task.EVENT_UPDATE_PROGRESS,
|
|
66
73
|
lambda _event_type, details: details_capture.append(details))
|
|
67
74
|
|
|
68
|
-
d =
|
|
75
|
+
d = pe.Dispatcher({}, b'secret', b'server-josh')
|
|
69
76
|
d.setup()
|
|
70
77
|
d.targets[b'child-josh'] = t
|
|
71
78
|
|
|
@@ -73,7 +80,7 @@ class ProcessExecutorHelpersTest(test.TestCase):
|
|
|
73
80
|
s.start()
|
|
74
81
|
self.addCleanup(s.join)
|
|
75
82
|
|
|
76
|
-
c =
|
|
83
|
+
c = pe.Channel(d.port, b'child-josh', b'secret')
|
|
77
84
|
self.addCleanup(c.close)
|
|
78
85
|
|
|
79
86
|
send_what = [
|
|
@@ -87,7 +94,7 @@ class ProcessExecutorHelpersTest(test.TestCase):
|
|
|
87
94
|
{'progress': 0.8},
|
|
88
95
|
{'progress': 0.9},
|
|
89
96
|
]
|
|
90
|
-
e_s =
|
|
97
|
+
e_s = pe.EventSender(c)
|
|
91
98
|
for details in send_what:
|
|
92
99
|
e_s(task.EVENT_UPDATE_PROGRESS, details)
|
|
93
100
|
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Copyright (C) Red Hat
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
|
+
# not use this file except in compliance with the License. You may obtain
|
|
5
|
+
# a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
11
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
12
|
+
# License for the specific language governing permissions and limitations
|
|
13
|
+
# under the License.
|
|
14
|
+
|
|
15
|
+
from unittest import mock
|
|
16
|
+
|
|
17
|
+
from oslo_serialization import jsonutils
|
|
18
|
+
from oslo_utils import uuidutils
|
|
19
|
+
import testtools
|
|
20
|
+
|
|
21
|
+
from taskflow import exceptions as exc
|
|
22
|
+
from taskflow.jobs.backends import impl_etcd
|
|
23
|
+
from taskflow.jobs import base as jobs_base
|
|
24
|
+
from taskflow import test
|
|
25
|
+
from taskflow.tests.unit.jobs import base
|
|
26
|
+
from taskflow.tests import utils as test_utils
|
|
27
|
+
|
|
28
|
+
ETCD_AVAILABLE = test_utils.etcd_available()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EtcdJobBoardMixin:
|
|
32
|
+
def create_board(self, conf=None, persistence=None):
|
|
33
|
+
self.path = f"test-{uuidutils.generate_uuid()}"
|
|
34
|
+
board_conf = {
|
|
35
|
+
"path": self.path,
|
|
36
|
+
}
|
|
37
|
+
if conf:
|
|
38
|
+
board_conf.update(conf)
|
|
39
|
+
board = impl_etcd.EtcdJobBoard("etcd", board_conf,
|
|
40
|
+
persistence=persistence)
|
|
41
|
+
return board._client, board
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MockedEtcdJobBoard(test.TestCase, EtcdJobBoardMixin):
|
|
45
|
+
|
|
46
|
+
def test_create_board(self):
|
|
47
|
+
_, jobboard = self.create_board()
|
|
48
|
+
self.assertEqual(f"/taskflow/jobs/{self.path}", jobboard._root_path)
|
|
49
|
+
|
|
50
|
+
_, jobboard = self.create_board({"path": "/testpath"})
|
|
51
|
+
self.assertEqual("/taskflow/jobs/testpath", jobboard._root_path)
|
|
52
|
+
|
|
53
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard.incr")
|
|
54
|
+
@mock.patch("threading.Condition")
|
|
55
|
+
@mock.patch("oslo_utils.uuidutils.generate_uuid")
|
|
56
|
+
@mock.patch("oslo_utils.timeutils.utcnow")
|
|
57
|
+
def test_post(self,
|
|
58
|
+
mock_utcnow: mock.Mock,
|
|
59
|
+
mock_generated_uuid: mock.Mock,
|
|
60
|
+
mock_cond: mock.Mock,
|
|
61
|
+
mock_incr: mock.Mock):
|
|
62
|
+
mock_incr.return_value = 12
|
|
63
|
+
mock_generated_uuid.return_value = "uuid1"
|
|
64
|
+
mock_utcnow.return_value = "utcnow1"
|
|
65
|
+
|
|
66
|
+
mock_book = mock.Mock()
|
|
67
|
+
mock_book.name = "book1_name"
|
|
68
|
+
mock_book.uuid = "book1_uuid"
|
|
69
|
+
mock_details = mock.Mock()
|
|
70
|
+
|
|
71
|
+
_, jobboard = self.create_board()
|
|
72
|
+
jobboard._client = mock.Mock()
|
|
73
|
+
job = jobboard.post("post1", book=mock_book,
|
|
74
|
+
details=mock_details,
|
|
75
|
+
priority=jobs_base.JobPriority.NORMAL)
|
|
76
|
+
|
|
77
|
+
expected_key = (
|
|
78
|
+
f"/taskflow/jobs/{self.path}/job12")
|
|
79
|
+
expected_data_key = expected_key + jobboard.DATA_POSTFIX
|
|
80
|
+
expected_book_data = {
|
|
81
|
+
"name": "book1_name",
|
|
82
|
+
"uuid": "book1_uuid"
|
|
83
|
+
}
|
|
84
|
+
expected_job_posting = {
|
|
85
|
+
"uuid": "uuid1",
|
|
86
|
+
"name": "post1",
|
|
87
|
+
"priority": "NORMAL",
|
|
88
|
+
"created_on": "utcnow1",
|
|
89
|
+
"details": mock_details,
|
|
90
|
+
"book": expected_book_data,
|
|
91
|
+
"sequence": 12,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
mock_incr.assert_called_with(f"/taskflow/jobs/{self.path}/sequence")
|
|
95
|
+
|
|
96
|
+
jobboard._client.create.assert_called_with(
|
|
97
|
+
expected_data_key, jsonutils.dumps(expected_job_posting))
|
|
98
|
+
|
|
99
|
+
self.assertEqual("post1", job.name)
|
|
100
|
+
self.assertEqual(expected_key, job.key)
|
|
101
|
+
self.assertEqual(mock_details, job.details)
|
|
102
|
+
self.assertEqual(mock_book, job.book)
|
|
103
|
+
self.assertEqual(expected_book_data, job._book_data)
|
|
104
|
+
self.assertEqual(jobs_base.JobPriority.NORMAL, job.priority)
|
|
105
|
+
self.assertEqual(12, job.sequence)
|
|
106
|
+
|
|
107
|
+
self.assertEqual(1, len(jobboard._job_cache))
|
|
108
|
+
self.assertEqual(job, jobboard._job_cache[expected_key])
|
|
109
|
+
|
|
110
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
111
|
+
"set_last_modified")
|
|
112
|
+
def test_claim(self, mock_set_last_modified):
|
|
113
|
+
who = "owner1"
|
|
114
|
+
lease_id = uuidutils.generate_uuid()
|
|
115
|
+
|
|
116
|
+
_, jobboard = self.create_board(conf={"ttl": 37})
|
|
117
|
+
jobboard._client = mock.Mock()
|
|
118
|
+
|
|
119
|
+
mock_lease = mock.Mock(id=lease_id)
|
|
120
|
+
jobboard._client.lease.return_value = mock_lease
|
|
121
|
+
jobboard._client.create.return_value = True
|
|
122
|
+
jobboard._client.get.return_value = [mock.Mock()]
|
|
123
|
+
|
|
124
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
125
|
+
"job7",
|
|
126
|
+
jobboard._client,
|
|
127
|
+
f"/taskflow/jobs/{self.path}/job7",
|
|
128
|
+
uuid=uuidutils.generate_uuid(),
|
|
129
|
+
details=mock.Mock(),
|
|
130
|
+
backend="etcd",
|
|
131
|
+
book=mock.Mock(),
|
|
132
|
+
book_data=mock.Mock(),
|
|
133
|
+
priority=jobs_base.JobPriority.NORMAL,
|
|
134
|
+
sequence=7,
|
|
135
|
+
created_on="date")
|
|
136
|
+
|
|
137
|
+
jobboard.claim(job, who)
|
|
138
|
+
|
|
139
|
+
jobboard._client.lease.assert_called_once_with(ttl=37)
|
|
140
|
+
|
|
141
|
+
jobboard._client.create.assert_called_once_with(
|
|
142
|
+
f"{job.key}{jobboard.LOCK_POSTFIX}",
|
|
143
|
+
jsonutils.dumps({"owner": who,
|
|
144
|
+
"lease_id": lease_id}),
|
|
145
|
+
lease=mock_lease)
|
|
146
|
+
|
|
147
|
+
jobboard._client.get.assert_called_once_with(
|
|
148
|
+
job.key + jobboard.DATA_POSTFIX)
|
|
149
|
+
mock_lease.revoke.assert_not_called()
|
|
150
|
+
|
|
151
|
+
mock_set_last_modified.assert_called_once_with(job)
|
|
152
|
+
|
|
153
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
154
|
+
"set_last_modified")
|
|
155
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
156
|
+
"find_owner")
|
|
157
|
+
def test_claim_already_claimed(self, mock_find_owner,
|
|
158
|
+
mock_set_last_modified):
|
|
159
|
+
who = "owner1"
|
|
160
|
+
lease_id = uuidutils.generate_uuid()
|
|
161
|
+
|
|
162
|
+
mock_find_owner.return_value = who
|
|
163
|
+
|
|
164
|
+
_, jobboard = self.create_board({"ttl": 37})
|
|
165
|
+
jobboard._client = mock.Mock()
|
|
166
|
+
|
|
167
|
+
mock_lease = mock.Mock(id=lease_id)
|
|
168
|
+
jobboard._client.lease.return_value = mock_lease
|
|
169
|
+
jobboard._client.create.return_value = False
|
|
170
|
+
jobboard._client.get.return_value = []
|
|
171
|
+
|
|
172
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
173
|
+
"job7",
|
|
174
|
+
jobboard._client,
|
|
175
|
+
f"/taskflow/jobs/{self.path}/job7",
|
|
176
|
+
uuid=uuidutils.generate_uuid(),
|
|
177
|
+
details=mock.Mock(),
|
|
178
|
+
backend="etcd",
|
|
179
|
+
book=mock.Mock(),
|
|
180
|
+
book_data=mock.Mock(),
|
|
181
|
+
priority=jobs_base.JobPriority.NORMAL,
|
|
182
|
+
sequence=7,
|
|
183
|
+
created_on="date")
|
|
184
|
+
|
|
185
|
+
self.assertRaisesRegex(exc.UnclaimableJob, "already claimed by",
|
|
186
|
+
jobboard.claim, job, who)
|
|
187
|
+
|
|
188
|
+
jobboard._client.lease.assert_called_once_with(ttl=37)
|
|
189
|
+
|
|
190
|
+
jobboard._client.create.assert_called_once_with(
|
|
191
|
+
f"{job.key}{jobboard.LOCK_POSTFIX}",
|
|
192
|
+
jsonutils.dumps({"owner": who,
|
|
193
|
+
"lease_id": lease_id}),
|
|
194
|
+
lease=mock_lease)
|
|
195
|
+
|
|
196
|
+
mock_lease.revoke.assert_called_once()
|
|
197
|
+
|
|
198
|
+
mock_set_last_modified.assert_not_called()
|
|
199
|
+
|
|
200
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
201
|
+
"set_last_modified")
|
|
202
|
+
def test_claim_deleted(self, mock_set_last_modified):
|
|
203
|
+
who = "owner1"
|
|
204
|
+
lease_id = uuidutils.generate_uuid()
|
|
205
|
+
|
|
206
|
+
_, jobboard = self.create_board({"ttl": 37})
|
|
207
|
+
jobboard._client = mock.Mock()
|
|
208
|
+
|
|
209
|
+
mock_lease = mock.Mock(id=lease_id)
|
|
210
|
+
jobboard._client.lease.return_value = mock_lease
|
|
211
|
+
jobboard._client.create.return_value = True
|
|
212
|
+
jobboard._client.get.return_value = []
|
|
213
|
+
|
|
214
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
215
|
+
"job7",
|
|
216
|
+
jobboard._client,
|
|
217
|
+
f"/taskflow/jobs/{self.path}/job7",
|
|
218
|
+
uuid=uuidutils.generate_uuid(),
|
|
219
|
+
details=mock.Mock(),
|
|
220
|
+
backend="etcd",
|
|
221
|
+
book=mock.Mock(),
|
|
222
|
+
book_data=mock.Mock(),
|
|
223
|
+
priority=jobs_base.JobPriority.NORMAL,
|
|
224
|
+
sequence=7,
|
|
225
|
+
created_on="date")
|
|
226
|
+
|
|
227
|
+
self.assertRaisesRegex(exc.UnclaimableJob, "already deleted",
|
|
228
|
+
jobboard.claim, job, who)
|
|
229
|
+
|
|
230
|
+
jobboard._client.lease.assert_called_once_with(ttl=37)
|
|
231
|
+
|
|
232
|
+
jobboard._client.create.assert_called_once_with(
|
|
233
|
+
f"{job.key}{jobboard.LOCK_POSTFIX}",
|
|
234
|
+
jsonutils.dumps({"owner": who,
|
|
235
|
+
"lease_id": lease_id}),
|
|
236
|
+
lease=mock_lease)
|
|
237
|
+
|
|
238
|
+
jobboard._client.get.assert_called_once_with(
|
|
239
|
+
job.key + jobboard.DATA_POSTFIX)
|
|
240
|
+
mock_lease.revoke.assert_called_once()
|
|
241
|
+
|
|
242
|
+
mock_set_last_modified.assert_not_called()
|
|
243
|
+
|
|
244
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
245
|
+
"get_owner_and_data")
|
|
246
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
247
|
+
"_remove_job_from_cache")
|
|
248
|
+
def test_consume(self, mock__remove_job_from_cache,
|
|
249
|
+
mock_get_owner_and_data):
|
|
250
|
+
mock_get_owner_and_data.return_value = ["owner1", mock.Mock()]
|
|
251
|
+
|
|
252
|
+
_, jobboard = self.create_board()
|
|
253
|
+
jobboard._client = mock.Mock()
|
|
254
|
+
|
|
255
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
256
|
+
"job7",
|
|
257
|
+
jobboard._client,
|
|
258
|
+
f"/taskflow/jobs/{self.path}/job7")
|
|
259
|
+
jobboard.consume(job, "owner1")
|
|
260
|
+
|
|
261
|
+
jobboard._client.delete_prefix.assert_called_once_with(job.key + ".")
|
|
262
|
+
mock__remove_job_from_cache.assert_called_once_with(job.key)
|
|
263
|
+
|
|
264
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
265
|
+
"get_owner_and_data")
|
|
266
|
+
def test_consume_bad_owner(self, mock_get_owner_and_data):
|
|
267
|
+
mock_get_owner_and_data.return_value = ["owner2", mock.Mock()]
|
|
268
|
+
|
|
269
|
+
_, jobboard = self.create_board()
|
|
270
|
+
jobboard._client = mock.Mock()
|
|
271
|
+
|
|
272
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
273
|
+
"job7",
|
|
274
|
+
jobboard._client,
|
|
275
|
+
f"/taskflow/jobs/{self.path}/job7")
|
|
276
|
+
self.assertRaisesRegex(exc.JobFailure, "which is not owned",
|
|
277
|
+
jobboard.consume, job, "owner1")
|
|
278
|
+
|
|
279
|
+
jobboard._client.delete_prefix.assert_not_called()
|
|
280
|
+
|
|
281
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
282
|
+
"get_owner_and_data")
|
|
283
|
+
def test_abandon(self, mock_get_owner_and_data):
|
|
284
|
+
mock_get_owner_and_data.return_value = ["owner1", mock.Mock()]
|
|
285
|
+
|
|
286
|
+
_, jobboard = self.create_board()
|
|
287
|
+
jobboard._client = mock.Mock()
|
|
288
|
+
|
|
289
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
290
|
+
"job7",
|
|
291
|
+
jobboard._client,
|
|
292
|
+
f"/taskflow/jobs/{self.path}/job7")
|
|
293
|
+
jobboard.abandon(job, "owner1")
|
|
294
|
+
|
|
295
|
+
jobboard._client.delete.assert_called_once_with(
|
|
296
|
+
f"{job.key}{jobboard.LOCK_POSTFIX}")
|
|
297
|
+
|
|
298
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
299
|
+
"get_owner_and_data")
|
|
300
|
+
def test_abandon_bad_owner(self, mock_get_owner_and_data):
|
|
301
|
+
mock_get_owner_and_data.return_value = ["owner2", mock.Mock()]
|
|
302
|
+
|
|
303
|
+
_, jobboard = self.create_board()
|
|
304
|
+
jobboard._client = mock.Mock()
|
|
305
|
+
|
|
306
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
307
|
+
"job7",
|
|
308
|
+
jobboard._client,
|
|
309
|
+
f"/taskflow/jobs/{self.path}/job7")
|
|
310
|
+
self.assertRaisesRegex(exc.JobFailure, "which is not owned",
|
|
311
|
+
jobboard.abandon, job, "owner1")
|
|
312
|
+
|
|
313
|
+
jobboard._client.delete.assert_not_called()
|
|
314
|
+
|
|
315
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
316
|
+
"get_owner_and_data")
|
|
317
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
318
|
+
"_remove_job_from_cache")
|
|
319
|
+
def test_trash(self, mock__remove_job_from_cache,
|
|
320
|
+
mock_get_owner_and_data):
|
|
321
|
+
mock_get_owner_and_data.return_value = ["owner1", mock.Mock()]
|
|
322
|
+
|
|
323
|
+
_, jobboard = self.create_board()
|
|
324
|
+
jobboard._client = mock.Mock()
|
|
325
|
+
|
|
326
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
327
|
+
"job7",
|
|
328
|
+
jobboard._client,
|
|
329
|
+
f"/taskflow/jobs/{self.path}/job7")
|
|
330
|
+
jobboard.trash(job, "owner1")
|
|
331
|
+
|
|
332
|
+
jobboard._client.create.assert_called_once_with(
|
|
333
|
+
f"/taskflow/.trash/{self.path}/job7", mock.ANY)
|
|
334
|
+
jobboard._client.delete_prefix.assert_called_once_with(job.key + ".")
|
|
335
|
+
mock__remove_job_from_cache.assert_called_once_with(job.key)
|
|
336
|
+
|
|
337
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
338
|
+
"get_owner_and_data")
|
|
339
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
340
|
+
"_remove_job_from_cache")
|
|
341
|
+
def test_trash_bad_owner(self, mock__remove_job_from_cache,
|
|
342
|
+
mock_get_owner_and_data):
|
|
343
|
+
mock_get_owner_and_data.return_value = ["owner2", mock.Mock()]
|
|
344
|
+
|
|
345
|
+
_, jobboard = self.create_board()
|
|
346
|
+
jobboard._client = mock.Mock()
|
|
347
|
+
|
|
348
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
349
|
+
"job7",
|
|
350
|
+
jobboard._client,
|
|
351
|
+
f"/taskflow/jobs/{self.path}/job7")
|
|
352
|
+
self.assertRaisesRegex(exc.JobFailure, "which is not owned",
|
|
353
|
+
jobboard.trash, job, "owner1")
|
|
354
|
+
|
|
355
|
+
jobboard._client.create.assert_not_called()
|
|
356
|
+
jobboard._client.delete_prefix.assert_not_called()
|
|
357
|
+
mock__remove_job_from_cache.assert_not_called()
|
|
358
|
+
|
|
359
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
360
|
+
"get_owner_and_data")
|
|
361
|
+
@mock.patch("taskflow.jobs.backends.impl_etcd.EtcdJobBoard."
|
|
362
|
+
"_remove_job_from_cache")
|
|
363
|
+
def test_trash_deleted_job(self, mock__remove_job_from_cache,
|
|
364
|
+
mock_get_owner_and_data):
|
|
365
|
+
mock_get_owner_and_data.return_value = ["owner1", None]
|
|
366
|
+
|
|
367
|
+
_, jobboard = self.create_board()
|
|
368
|
+
jobboard._client = mock.Mock()
|
|
369
|
+
|
|
370
|
+
job = impl_etcd.EtcdJob(jobboard,
|
|
371
|
+
"job7",
|
|
372
|
+
jobboard._client,
|
|
373
|
+
f"/taskflow/jobs/{self.path}/job7")
|
|
374
|
+
self.assertRaisesRegex(exc.NotFound, "Cannot find job",
|
|
375
|
+
jobboard.trash, job, "owner1")
|
|
376
|
+
|
|
377
|
+
jobboard._client.create.assert_not_called()
|
|
378
|
+
jobboard._client.delete_prefix.assert_not_called()
|
|
379
|
+
mock__remove_job_from_cache.assert_not_called()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@testtools.skipIf(not ETCD_AVAILABLE, 'Etcd is not available')
|
|
383
|
+
class EtcdJobBoardTest(test.TestCase, base.BoardTestMixin, EtcdJobBoardMixin):
|
|
384
|
+
def setUp(self):
|
|
385
|
+
super().setUp()
|
|
386
|
+
self.client, self.board = self.create_board()
|
|
387
|
+
|
|
388
|
+
def test__incr(self):
|
|
389
|
+
key = uuidutils.generate_uuid()
|
|
390
|
+
|
|
391
|
+
self.board.connect()
|
|
392
|
+
self.addCleanup(self.board.close)
|
|
393
|
+
self.addCleanup(self.board._client.delete, key)
|
|
394
|
+
|
|
395
|
+
self.assertEqual(1, self.board.incr(key))
|
|
396
|
+
self.assertEqual(2, self.board.incr(key))
|
|
397
|
+
self.assertEqual(3, self.board.incr(key))
|
|
398
|
+
|
|
399
|
+
self.assertEqual(b'3', self.board.get_one(key))
|
|
400
|
+
self.board.close()
|
|
401
|
+
|
|
402
|
+
def test_get_one(self):
|
|
403
|
+
key1 = uuidutils.generate_uuid()
|
|
404
|
+
|
|
405
|
+
self.board.connect()
|
|
406
|
+
self.addCleanup(self.board._client.delete, key1)
|
|
407
|
+
|
|
408
|
+
# put data and get it
|
|
409
|
+
self.board._client.put(key1, "testset1")
|
|
410
|
+
self.assertEqual(b"testset1", self.board.get_one(key1))
|
|
411
|
+
|
|
412
|
+
# delete data and check that it's not found
|
|
413
|
+
self.board._client.delete(key1)
|
|
414
|
+
self.assertIsNone(self.board.get_one(key1))
|
|
415
|
+
|
|
416
|
+
# get a non-existant data
|
|
417
|
+
key2 = uuidutils.generate_uuid()
|
|
418
|
+
# (ensure it doesn't exist)
|
|
419
|
+
self.board._client.delete(key2)
|
|
420
|
+
self.assertIsNone(self.board.get_one(key2))
|
|
421
|
+
self.board.close()
|
|
@@ -23,6 +23,11 @@ from taskflow import test
|
|
|
23
23
|
from taskflow.tests import utils
|
|
24
24
|
from taskflow.utils import eventlet_utils as eu
|
|
25
25
|
|
|
26
|
+
try:
|
|
27
|
+
from taskflow.engines.action_engine import process_executor as pe
|
|
28
|
+
except ImportError:
|
|
29
|
+
pe = None
|
|
30
|
+
|
|
26
31
|
|
|
27
32
|
class ArgumentsPassingTest(utils.EngineTestBase):
|
|
28
33
|
|
|
@@ -221,6 +226,7 @@ class ParallelEngineWithEventletTest(ArgumentsPassingTest, test.TestCase):
|
|
|
221
226
|
executor=executor)
|
|
222
227
|
|
|
223
228
|
|
|
229
|
+
@testtools.skipIf(pe is None, 'process_executor is not available')
|
|
224
230
|
class ParallelEngineWithProcessTest(ArgumentsPassingTest, test.TestCase):
|
|
225
231
|
_EXECUTOR_WORKERS = 2
|
|
226
232
|
|
|
@@ -41,6 +41,11 @@ from taskflow.utils import eventlet_utils as eu
|
|
|
41
41
|
from taskflow.utils import persistence_utils as p_utils
|
|
42
42
|
from taskflow.utils import threading_utils as tu
|
|
43
43
|
|
|
44
|
+
try:
|
|
45
|
+
from taskflow.engines.action_engine import process_executor as pe
|
|
46
|
+
except ImportError:
|
|
47
|
+
pe = None
|
|
48
|
+
|
|
44
49
|
|
|
45
50
|
# Expected engine transitions when empty workflows are ran...
|
|
46
51
|
_EMPTY_TRANSITIONS = [
|
|
@@ -1494,6 +1499,7 @@ class ParallelEngineWithEventletTest(EngineTaskTest,
|
|
|
1494
1499
|
store=store, **kwargs)
|
|
1495
1500
|
|
|
1496
1501
|
|
|
1502
|
+
@testtools.skipIf(pe is None, 'process_executor is not available')
|
|
1497
1503
|
class ParallelEngineWithProcessTest(EngineTaskTest,
|
|
1498
1504
|
EngineMultipleResultsTest,
|
|
1499
1505
|
EngineLinearFlowTest,
|
|
@@ -28,6 +28,11 @@ from taskflow.tests import utils
|
|
|
28
28
|
from taskflow.types import failure
|
|
29
29
|
from taskflow.utils import eventlet_utils as eu
|
|
30
30
|
|
|
31
|
+
try:
|
|
32
|
+
from taskflow.engines.action_engine import process_executor as pe
|
|
33
|
+
except ImportError:
|
|
34
|
+
pe = None
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
class FailingRetry(retry.Retry):
|
|
33
38
|
|
|
@@ -1313,6 +1318,7 @@ class ParallelEngineWithEventletTest(RetryTest, test.TestCase):
|
|
|
1313
1318
|
defer_reverts=defer_reverts)
|
|
1314
1319
|
|
|
1315
1320
|
|
|
1321
|
+
@testtools.skipIf(pe is None, 'process_executor is not available')
|
|
1316
1322
|
class ParallelEngineWithProcessTest(RetryTest, test.TestCase):
|
|
1317
1323
|
_EXECUTOR_WORKERS = 2
|
|
1318
1324
|
|
|
@@ -25,6 +25,11 @@ from taskflow import test
|
|
|
25
25
|
from taskflow.tests import utils
|
|
26
26
|
from taskflow.utils import eventlet_utils as eu
|
|
27
27
|
|
|
28
|
+
try:
|
|
29
|
+
from taskflow.engines.action_engine import process_executor as pe
|
|
30
|
+
except ImportError:
|
|
31
|
+
pe = None
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
class SuspendingListener(utils.CaptureListener):
|
|
30
35
|
|
|
@@ -224,6 +229,7 @@ class ParallelEngineWithEventletTest(SuspendTest, test.TestCase):
|
|
|
224
229
|
executor=executor)
|
|
225
230
|
|
|
226
231
|
|
|
232
|
+
@testtools.skipIf(pe is None, 'process_executor is not available')
|
|
227
233
|
class ParallelEngineWithProcessTest(SuspendTest, test.TestCase):
|
|
228
234
|
_EXECUTOR_WORKERS = 2
|
|
229
235
|
|