k3wsjobd 0.1.1__tar.gz
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.
- k3wsjobd-0.1.1/LICENSE +21 -0
- k3wsjobd-0.1.1/PKG-INFO +101 -0
- k3wsjobd-0.1.1/README.md +61 -0
- k3wsjobd-0.1.1/__init__.py +33 -0
- k3wsjobd-0.1.1/k3wsjobd.egg-info/PKG-INFO +101 -0
- k3wsjobd-0.1.1/k3wsjobd.egg-info/SOURCES.txt +16 -0
- k3wsjobd-0.1.1/k3wsjobd.egg-info/dependency_links.txt +1 -0
- k3wsjobd-0.1.1/k3wsjobd.egg-info/requires.txt +20 -0
- k3wsjobd-0.1.1/k3wsjobd.egg-info/top_level.txt +1 -0
- k3wsjobd-0.1.1/pyproject.toml +63 -0
- k3wsjobd-0.1.1/setup.cfg +4 -0
- k3wsjobd-0.1.1/synopsis.py +17 -0
- k3wsjobd-0.1.1/test/test_doctest.py +8 -0
- k3wsjobd-0.1.1/test/test_k3wsjobd.py +472 -0
- k3wsjobd-0.1.1/wsjobd.py +355 -0
k3wsjobd-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 张炎泼
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
k3wsjobd-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: k3wsjobd
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Gevent-based WebSocket server for async job processing
|
|
5
|
+
Author-email: Zhang Yanpo <drdr.xp@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pykit3/k3wsjobd
|
|
8
|
+
Project-URL: Documentation, https://k3wsjobd.readthedocs.io
|
|
9
|
+
Keywords: websocket,gevent,job,async,server
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: gevent-websocket
|
|
23
|
+
Requires-Dist: psutil
|
|
24
|
+
Requires-Dist: k3thread
|
|
25
|
+
Requires-Dist: k3utfjson
|
|
26
|
+
Requires-Dist: k3jobq
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
30
|
+
Requires-Dist: coverage; extra == "dev"
|
|
31
|
+
Provides-Extra: publish
|
|
32
|
+
Requires-Dist: build; extra == "publish"
|
|
33
|
+
Requires-Dist: twine; extra == "publish"
|
|
34
|
+
Requires-Dist: pk3; extra == "publish"
|
|
35
|
+
Provides-Extra: docs
|
|
36
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
37
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
38
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# k3wsjobd
|
|
42
|
+
|
|
43
|
+
[](https://github.com/pykit3/k3wsjobd/actions/workflows/python-package.yml)
|
|
44
|
+
[](https://travis-ci.com/pykit3/k3wsjobd)
|
|
45
|
+
[](https://k3wsjobd.readthedocs.io/en/stable/?badge=stable)
|
|
46
|
+
[](https://pypi.org/project/k3wsjobd)
|
|
47
|
+
|
|
48
|
+
This module is a gevent based websocket server. When the server receives a job description from a client, it runs that job asynchronously in a thread, and reports the progress back to the client periodically.
|
|
49
|
+
|
|
50
|
+
k3wsjobd is a component of [pykit3] project: a python3 toolkit set.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
This module is a gevent based websocket server. When the server receives a job description from a client,
|
|
54
|
+
it runs that job asynchronously in a thread, and reports the progress back to the client periodically.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Install
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
pip install k3wsjobd
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
# Synopsis
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
|
|
69
|
+
from geventwebsocket import Resource, WebSocketServer
|
|
70
|
+
import k3wsjobd
|
|
71
|
+
from k3wsjobd import logging
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run():
|
|
75
|
+
k3wsjobd.run(ip='127.0.0.1', port=33445)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
logger = logging.getLogger()
|
|
80
|
+
logger.setLevel(logging.INFO)
|
|
81
|
+
|
|
82
|
+
file_handler = logging.FileHandler('wsjobd.log')
|
|
83
|
+
formatter = logging.Formatter('[%(asctime)s, %(levelname)s] %(message)s')
|
|
84
|
+
file_handler.setFormatter(formatter)
|
|
85
|
+
logger.addHandler(file_handler)
|
|
86
|
+
run()
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
# Author
|
|
91
|
+
|
|
92
|
+
Zhang Yanpo (张炎泼) <drdr.xp@gmail.com>
|
|
93
|
+
|
|
94
|
+
# Copyright and License
|
|
95
|
+
|
|
96
|
+
The MIT License (MIT)
|
|
97
|
+
|
|
98
|
+
Copyright (c) 2015 Zhang Yanpo (张炎泼) <drdr.xp@gmail.com>
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
[pykit3]: https://github.com/pykit3
|
k3wsjobd-0.1.1/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# k3wsjobd
|
|
2
|
+
|
|
3
|
+
[](https://github.com/pykit3/k3wsjobd/actions/workflows/python-package.yml)
|
|
4
|
+
[](https://travis-ci.com/pykit3/k3wsjobd)
|
|
5
|
+
[](https://k3wsjobd.readthedocs.io/en/stable/?badge=stable)
|
|
6
|
+
[](https://pypi.org/project/k3wsjobd)
|
|
7
|
+
|
|
8
|
+
This module is a gevent based websocket server. When the server receives a job description from a client, it runs that job asynchronously in a thread, and reports the progress back to the client periodically.
|
|
9
|
+
|
|
10
|
+
k3wsjobd is a component of [pykit3] project: a python3 toolkit set.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
This module is a gevent based websocket server. When the server receives a job description from a client,
|
|
14
|
+
it runs that job asynchronously in a thread, and reports the progress back to the client periodically.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Install
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install k3wsjobd
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
# Synopsis
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
|
|
29
|
+
from geventwebsocket import Resource, WebSocketServer
|
|
30
|
+
import k3wsjobd
|
|
31
|
+
from k3wsjobd import logging
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run():
|
|
35
|
+
k3wsjobd.run(ip='127.0.0.1', port=33445)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
logger = logging.getLogger()
|
|
40
|
+
logger.setLevel(logging.INFO)
|
|
41
|
+
|
|
42
|
+
file_handler = logging.FileHandler('wsjobd.log')
|
|
43
|
+
formatter = logging.Formatter('[%(asctime)s, %(levelname)s] %(message)s')
|
|
44
|
+
file_handler.setFormatter(formatter)
|
|
45
|
+
logger.addHandler(file_handler)
|
|
46
|
+
run()
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
# Author
|
|
51
|
+
|
|
52
|
+
Zhang Yanpo (张炎泼) <drdr.xp@gmail.com>
|
|
53
|
+
|
|
54
|
+
# Copyright and License
|
|
55
|
+
|
|
56
|
+
The MIT License (MIT)
|
|
57
|
+
|
|
58
|
+
Copyright (c) 2015 Zhang Yanpo (张炎泼) <drdr.xp@gmail.com>
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
[pykit3]: https://github.com/pykit3
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is a gevent based websocket server. When the server receives a job description from a client,
|
|
3
|
+
it runs that job asynchronously in a thread, and reports the progress back to the client periodically.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
|
|
9
|
+
__version__ = version("k3wsjobd")
|
|
10
|
+
|
|
11
|
+
from .wsjobd import (
|
|
12
|
+
JobdWebSocketApplication,
|
|
13
|
+
Job,
|
|
14
|
+
run,
|
|
15
|
+
SystemOverloadError,
|
|
16
|
+
JobError,
|
|
17
|
+
InvalidMessageError,
|
|
18
|
+
InvalidProgressError,
|
|
19
|
+
LoadingError,
|
|
20
|
+
JobNotInSessionError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"JobdWebSocketApplication",
|
|
25
|
+
"Job",
|
|
26
|
+
"run",
|
|
27
|
+
"SystemOverloadError",
|
|
28
|
+
"JobError",
|
|
29
|
+
"InvalidMessageError",
|
|
30
|
+
"InvalidProgressError",
|
|
31
|
+
"LoadingError",
|
|
32
|
+
"JobNotInSessionError",
|
|
33
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: k3wsjobd
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Gevent-based WebSocket server for async job processing
|
|
5
|
+
Author-email: Zhang Yanpo <drdr.xp@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pykit3/k3wsjobd
|
|
8
|
+
Project-URL: Documentation, https://k3wsjobd.readthedocs.io
|
|
9
|
+
Keywords: websocket,gevent,job,async,server
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: gevent-websocket
|
|
23
|
+
Requires-Dist: psutil
|
|
24
|
+
Requires-Dist: k3thread
|
|
25
|
+
Requires-Dist: k3utfjson
|
|
26
|
+
Requires-Dist: k3jobq
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
30
|
+
Requires-Dist: coverage; extra == "dev"
|
|
31
|
+
Provides-Extra: publish
|
|
32
|
+
Requires-Dist: build; extra == "publish"
|
|
33
|
+
Requires-Dist: twine; extra == "publish"
|
|
34
|
+
Requires-Dist: pk3; extra == "publish"
|
|
35
|
+
Provides-Extra: docs
|
|
36
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
37
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
38
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# k3wsjobd
|
|
42
|
+
|
|
43
|
+
[](https://github.com/pykit3/k3wsjobd/actions/workflows/python-package.yml)
|
|
44
|
+
[](https://travis-ci.com/pykit3/k3wsjobd)
|
|
45
|
+
[](https://k3wsjobd.readthedocs.io/en/stable/?badge=stable)
|
|
46
|
+
[](https://pypi.org/project/k3wsjobd)
|
|
47
|
+
|
|
48
|
+
This module is a gevent based websocket server. When the server receives a job description from a client, it runs that job asynchronously in a thread, and reports the progress back to the client periodically.
|
|
49
|
+
|
|
50
|
+
k3wsjobd is a component of [pykit3] project: a python3 toolkit set.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
This module is a gevent based websocket server. When the server receives a job description from a client,
|
|
54
|
+
it runs that job asynchronously in a thread, and reports the progress back to the client periodically.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Install
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
pip install k3wsjobd
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
# Synopsis
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
|
|
69
|
+
from geventwebsocket import Resource, WebSocketServer
|
|
70
|
+
import k3wsjobd
|
|
71
|
+
from k3wsjobd import logging
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run():
|
|
75
|
+
k3wsjobd.run(ip='127.0.0.1', port=33445)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
logger = logging.getLogger()
|
|
80
|
+
logger.setLevel(logging.INFO)
|
|
81
|
+
|
|
82
|
+
file_handler = logging.FileHandler('wsjobd.log')
|
|
83
|
+
formatter = logging.Formatter('[%(asctime)s, %(levelname)s] %(message)s')
|
|
84
|
+
file_handler.setFormatter(formatter)
|
|
85
|
+
logger.addHandler(file_handler)
|
|
86
|
+
run()
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
# Author
|
|
91
|
+
|
|
92
|
+
Zhang Yanpo (张炎泼) <drdr.xp@gmail.com>
|
|
93
|
+
|
|
94
|
+
# Copyright and License
|
|
95
|
+
|
|
96
|
+
The MIT License (MIT)
|
|
97
|
+
|
|
98
|
+
Copyright (c) 2015 Zhang Yanpo (张炎泼) <drdr.xp@gmail.com>
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
[pykit3]: https://github.com/pykit3
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
__init__.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
synopsis.py
|
|
6
|
+
wsjobd.py
|
|
7
|
+
./__init__.py
|
|
8
|
+
./synopsis.py
|
|
9
|
+
./wsjobd.py
|
|
10
|
+
k3wsjobd.egg-info/PKG-INFO
|
|
11
|
+
k3wsjobd.egg-info/SOURCES.txt
|
|
12
|
+
k3wsjobd.egg-info/dependency_links.txt
|
|
13
|
+
k3wsjobd.egg-info/requires.txt
|
|
14
|
+
k3wsjobd.egg-info/top_level.txt
|
|
15
|
+
test/test_doctest.py
|
|
16
|
+
test/test_k3wsjobd.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
k3wsjobd
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "k3wsjobd"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Gevent-based WebSocket server for async job processing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Zhang Yanpo", email = "drdr.xp@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["websocket", "gevent", "job", "async", "server"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"gevent-websocket",
|
|
29
|
+
"psutil",
|
|
30
|
+
"k3thread",
|
|
31
|
+
"k3utfjson",
|
|
32
|
+
"k3jobq",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/pykit3/k3wsjobd"
|
|
37
|
+
Documentation = "https://k3wsjobd.readthedocs.io"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=7.0",
|
|
42
|
+
"ruff",
|
|
43
|
+
"coverage",
|
|
44
|
+
]
|
|
45
|
+
publish = [
|
|
46
|
+
"build",
|
|
47
|
+
"twine",
|
|
48
|
+
"pk3",
|
|
49
|
+
]
|
|
50
|
+
docs = [
|
|
51
|
+
"mkdocs>=1.5",
|
|
52
|
+
"mkdocs-material>=9.0",
|
|
53
|
+
"mkdocstrings[python]>=0.24",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools]
|
|
57
|
+
packages = ["k3wsjobd"]
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.package-dir]
|
|
60
|
+
k3wsjobd = "."
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 120
|
k3wsjobd-0.1.1/setup.cfg
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import k3wsjobd
|
|
2
|
+
from k3wsjobd import logging
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def run():
|
|
6
|
+
k3wsjobd.run(ip="127.0.0.1", port=33445)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
logger = logging.getLogger()
|
|
11
|
+
logger.setLevel(logging.INFO)
|
|
12
|
+
|
|
13
|
+
file_handler = logging.FileHandler("wsjobd.log")
|
|
14
|
+
formatter = logging.Formatter("[%(asctime)s, %(levelname)s] %(message)s")
|
|
15
|
+
file_handler.setFormatter(formatter)
|
|
16
|
+
logger.addHandler(file_handler)
|
|
17
|
+
run()
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
import websocket
|
|
10
|
+
|
|
11
|
+
import k3proc
|
|
12
|
+
import k3utfjson
|
|
13
|
+
import k3ut
|
|
14
|
+
from k3wsjobd import Job
|
|
15
|
+
from k3wsjobd.test.wsjobd_server import PORT
|
|
16
|
+
|
|
17
|
+
dd = k3ut.dd
|
|
18
|
+
|
|
19
|
+
random.seed(time.time())
|
|
20
|
+
this_base = os.path.dirname(__file__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def subproc(script, env=None):
|
|
24
|
+
if env is None:
|
|
25
|
+
env = dict(
|
|
26
|
+
PYTHONPATH=this_base + "/../..",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return k3proc.shell_script(script, env=env)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestWsjobd(unittest.TestCase):
|
|
33
|
+
@classmethod
|
|
34
|
+
def _clean(cls):
|
|
35
|
+
try:
|
|
36
|
+
subproc("python {b}/wsjobd_server.py stop".format(b=this_base))
|
|
37
|
+
except Exception as e:
|
|
38
|
+
dd("failed to stop wsjobd server: " + repr(e))
|
|
39
|
+
|
|
40
|
+
time.sleep(0.1)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def setUpClass(cls):
|
|
44
|
+
cls._clean()
|
|
45
|
+
subproc("python {b}/wsjobd_server.py start".format(b=this_base))
|
|
46
|
+
time.sleep(1)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def tearDownClass(cls):
|
|
50
|
+
cls._clean()
|
|
51
|
+
|
|
52
|
+
def _create_client(self):
|
|
53
|
+
ws = websocket.WebSocket()
|
|
54
|
+
ws.connect("ws://127.0.0.1:%d" % PORT)
|
|
55
|
+
ws.timeout = 6
|
|
56
|
+
return ws
|
|
57
|
+
|
|
58
|
+
def setUp(self):
|
|
59
|
+
self.ws = self._create_client()
|
|
60
|
+
|
|
61
|
+
def tearDown(self):
|
|
62
|
+
self.ws.close()
|
|
63
|
+
|
|
64
|
+
def get_random_ident(self):
|
|
65
|
+
return "random_ident_%d" % random.randint(10000, 99999)
|
|
66
|
+
|
|
67
|
+
def _wait_for_result(self, ws):
|
|
68
|
+
# wait for test_job_echo.run to fillin resp['result']
|
|
69
|
+
for _ in range(3):
|
|
70
|
+
resp = k3utfjson.load(ws.recv())
|
|
71
|
+
if "result" in resp:
|
|
72
|
+
break
|
|
73
|
+
time.sleep(0.1)
|
|
74
|
+
return resp
|
|
75
|
+
|
|
76
|
+
def test_invalid_jobdesc(self):
|
|
77
|
+
cases = (
|
|
78
|
+
("foo", "not json"),
|
|
79
|
+
(k3utfjson.dump("foo"), "not dict"),
|
|
80
|
+
(k3utfjson.dump({}), "no func"),
|
|
81
|
+
(k3utfjson.dump({"func": "foo"}), "no ident"),
|
|
82
|
+
(k3utfjson.dump({"ident": "bar"}), "no func"),
|
|
83
|
+
(k3utfjson.dump({"ident": "bar", "func": {}}), "invalid func"),
|
|
84
|
+
(k3utfjson.dump({"ident": 44, "func": "foo"}), "invalid ident"),
|
|
85
|
+
(k3utfjson.dump({"ident": "foo", "func": "foo", "jobs_dir": {}}), "invalid jobs_dir"),
|
|
86
|
+
)
|
|
87
|
+
for msg, desc in cases:
|
|
88
|
+
ws = self._create_client()
|
|
89
|
+
ws.send(msg)
|
|
90
|
+
resp = k3utfjson.load(ws.recv())
|
|
91
|
+
self.assertIn("err", resp, desc)
|
|
92
|
+
ws.close()
|
|
93
|
+
|
|
94
|
+
def test_normal_job(self):
|
|
95
|
+
job_desc = {
|
|
96
|
+
"func": "test_job_normal.run",
|
|
97
|
+
"ident": self.get_random_ident(),
|
|
98
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
102
|
+
|
|
103
|
+
resp = self._wait_for_result(self.ws)
|
|
104
|
+
dd(resp)
|
|
105
|
+
self.assertEqual("foo", resp["result"], "test get result")
|
|
106
|
+
|
|
107
|
+
def test_report_interval(self):
|
|
108
|
+
job_desc = {
|
|
109
|
+
"func": "test_job_loop_10.run",
|
|
110
|
+
"ident": self.get_random_ident(),
|
|
111
|
+
"progress": {
|
|
112
|
+
"interval": 0.5,
|
|
113
|
+
},
|
|
114
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
118
|
+
|
|
119
|
+
self.ws.recv()
|
|
120
|
+
first_report_time = time.time()
|
|
121
|
+
|
|
122
|
+
self.ws.recv()
|
|
123
|
+
second_report_time = time.time()
|
|
124
|
+
|
|
125
|
+
actual_interval = second_report_time - first_report_time
|
|
126
|
+
|
|
127
|
+
diff = actual_interval - job_desc["progress"]["interval"]
|
|
128
|
+
|
|
129
|
+
# tolerate 0.1 second of difference
|
|
130
|
+
self.assertLess(diff, 0.1)
|
|
131
|
+
|
|
132
|
+
def test_invalid_progress_key(self):
|
|
133
|
+
job_desc = {
|
|
134
|
+
"func": "test_job_progress_key.run",
|
|
135
|
+
"ident": self.get_random_ident(),
|
|
136
|
+
"progress": {
|
|
137
|
+
"key": "inexistent",
|
|
138
|
+
},
|
|
139
|
+
"report_system_load": True,
|
|
140
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
144
|
+
|
|
145
|
+
resp = self.ws.recv()
|
|
146
|
+
dd(repr(resp))
|
|
147
|
+
resp = k3utfjson.load(resp)
|
|
148
|
+
self.assertIsNone(resp)
|
|
149
|
+
|
|
150
|
+
def test_progress_key(self):
|
|
151
|
+
job_desc = {
|
|
152
|
+
"func": "test_job_progress_key.run",
|
|
153
|
+
"ident": self.get_random_ident(),
|
|
154
|
+
"progress": {
|
|
155
|
+
"key": "foo",
|
|
156
|
+
},
|
|
157
|
+
# progress_sender may try to read data["foo"] before job starts.
|
|
158
|
+
# need to preset a value.
|
|
159
|
+
"foo": "0%",
|
|
160
|
+
"report_system_load": True,
|
|
161
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
165
|
+
|
|
166
|
+
resp = self.ws.recv()
|
|
167
|
+
dd(repr(resp))
|
|
168
|
+
resp = k3utfjson.load(resp)
|
|
169
|
+
# progress may be set by job runner or job desc
|
|
170
|
+
self.assertIn(resp, ("80%", "0%"))
|
|
171
|
+
|
|
172
|
+
def test_check_system_load(self):
|
|
173
|
+
job_desc = {
|
|
174
|
+
"func": "test_job_normal.run",
|
|
175
|
+
"ident": self.get_random_ident(),
|
|
176
|
+
"check_load": {
|
|
177
|
+
"mem_low_threshold": 100 * 1024**3,
|
|
178
|
+
"cpu_low_threshold": 0,
|
|
179
|
+
},
|
|
180
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
184
|
+
|
|
185
|
+
resp = self.ws.recv()
|
|
186
|
+
resp = k3utfjson.load(resp)
|
|
187
|
+
self.assertEqual("SystemOverloadError", resp["err"])
|
|
188
|
+
|
|
189
|
+
job_desc = {
|
|
190
|
+
"func": "test_job_normal.run",
|
|
191
|
+
"ident": self.get_random_ident(),
|
|
192
|
+
"check_load": {
|
|
193
|
+
"cpu_low_threshold": 100.1,
|
|
194
|
+
"mem_low_threshold": 0,
|
|
195
|
+
},
|
|
196
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
ws2 = self._create_client()
|
|
200
|
+
ws2.send(k3utfjson.dump(job_desc))
|
|
201
|
+
|
|
202
|
+
resp = ws2.recv()
|
|
203
|
+
resp = k3utfjson.load(resp)
|
|
204
|
+
ws2.close()
|
|
205
|
+
self.assertEqual("SystemOverloadError", resp["err"])
|
|
206
|
+
|
|
207
|
+
def test_max_client_number(self):
|
|
208
|
+
job_desc = {
|
|
209
|
+
"func": "test_job_loop_10.run",
|
|
210
|
+
"ident": self.get_random_ident(),
|
|
211
|
+
"check_load": {
|
|
212
|
+
"max_client_number": 1,
|
|
213
|
+
"cpu_low_threshold": 0,
|
|
214
|
+
"mem_low_threshold": 0,
|
|
215
|
+
},
|
|
216
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
220
|
+
|
|
221
|
+
resp = self.ws.recv()
|
|
222
|
+
resp = k3utfjson.load(resp)
|
|
223
|
+
self.assertNotIn("err", resp)
|
|
224
|
+
|
|
225
|
+
job_desc = {
|
|
226
|
+
"func": "test_job_loop_10.run",
|
|
227
|
+
"ident": self.get_random_ident(),
|
|
228
|
+
"check_load": {
|
|
229
|
+
"max_client_number": 1,
|
|
230
|
+
"cpu_low_threshold": 0,
|
|
231
|
+
"mem_low_threshold": 0,
|
|
232
|
+
},
|
|
233
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
ws2 = self._create_client()
|
|
237
|
+
ws2.send(k3utfjson.dump(job_desc))
|
|
238
|
+
|
|
239
|
+
resp = ws2.recv()
|
|
240
|
+
resp = k3utfjson.load(resp)
|
|
241
|
+
self.assertEqual("SystemOverloadError", resp["err"])
|
|
242
|
+
ws2.close()
|
|
243
|
+
|
|
244
|
+
def test_module_not_exists(self):
|
|
245
|
+
job_desc = {
|
|
246
|
+
"func": "foo.bar",
|
|
247
|
+
"ident": self.get_random_ident(),
|
|
248
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
252
|
+
|
|
253
|
+
resp = k3utfjson.load(self.ws.recv())
|
|
254
|
+
self.assertIn("err", resp)
|
|
255
|
+
|
|
256
|
+
def test_report_system_load(self):
|
|
257
|
+
job_desc = {
|
|
258
|
+
"func": "test_job_normal.run",
|
|
259
|
+
"ident": self.get_random_ident(),
|
|
260
|
+
"report_system_load": True,
|
|
261
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
265
|
+
|
|
266
|
+
resp = k3utfjson.load(self.ws.recv())
|
|
267
|
+
self.assertIn("mem_available", resp["system_load"])
|
|
268
|
+
self.assertIn("cpu_idle_percent", resp["system_load"])
|
|
269
|
+
self.assertIn("client_number", resp["system_load"])
|
|
270
|
+
|
|
271
|
+
def test_same_ident_same_job(self):
|
|
272
|
+
ident = self.get_random_ident()
|
|
273
|
+
job_desc = {
|
|
274
|
+
"func": "test_job_echo.run",
|
|
275
|
+
"ident": ident,
|
|
276
|
+
"echo": "foo",
|
|
277
|
+
"sleep_time": 10,
|
|
278
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
282
|
+
|
|
283
|
+
resp = k3utfjson.load(self.ws.recv())
|
|
284
|
+
self.assertEqual("foo", resp["result"])
|
|
285
|
+
|
|
286
|
+
job_desc = {
|
|
287
|
+
"func": "test_job_echo.run",
|
|
288
|
+
"ident": ident,
|
|
289
|
+
"echo": "bar",
|
|
290
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
ws = self._create_client()
|
|
294
|
+
ws.send(k3utfjson.dump(job_desc))
|
|
295
|
+
|
|
296
|
+
resp = k3utfjson.load(ws.recv())
|
|
297
|
+
ws.close()
|
|
298
|
+
# not bar, because the ident is same as the first job,
|
|
299
|
+
# if job exists, it will not create a new one
|
|
300
|
+
self.assertEqual("foo", resp["result"])
|
|
301
|
+
self.assertEqual("foo", resp["echo"])
|
|
302
|
+
|
|
303
|
+
def test_same_ident_different_job(self):
|
|
304
|
+
ident = self.get_random_ident()
|
|
305
|
+
job_desc = {
|
|
306
|
+
"func": "test_job_echo.run",
|
|
307
|
+
"ident": ident,
|
|
308
|
+
"echo": "foo",
|
|
309
|
+
"sleep_time": 0.1,
|
|
310
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
314
|
+
|
|
315
|
+
resp = self._wait_for_result(self.ws)
|
|
316
|
+
self.assertEqual("foo", resp["result"])
|
|
317
|
+
|
|
318
|
+
time.sleep(0.2)
|
|
319
|
+
|
|
320
|
+
job_desc = {
|
|
321
|
+
"func": "test_job_echo.run",
|
|
322
|
+
"ident": ident,
|
|
323
|
+
"echo": "bar",
|
|
324
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
ws2 = self._create_client()
|
|
328
|
+
ws2.send(k3utfjson.dump(job_desc))
|
|
329
|
+
|
|
330
|
+
resp = self._wait_for_result(ws2)
|
|
331
|
+
ws2.close()
|
|
332
|
+
# old job with the same ident has exit, it will create a new one
|
|
333
|
+
self.assertEqual("bar", resp["result"])
|
|
334
|
+
self.assertEqual("bar", resp["echo"])
|
|
335
|
+
|
|
336
|
+
def test_client_close(self):
|
|
337
|
+
ident = self.get_random_ident()
|
|
338
|
+
job_desc = {
|
|
339
|
+
"func": "test_job_echo.run",
|
|
340
|
+
"ident": ident,
|
|
341
|
+
"echo": "foo",
|
|
342
|
+
"time_sleep": 10,
|
|
343
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
347
|
+
self.ws.close()
|
|
348
|
+
self.ws = self._create_client()
|
|
349
|
+
|
|
350
|
+
job_desc = {
|
|
351
|
+
"func": "test_job_echo.run",
|
|
352
|
+
"ident": ident,
|
|
353
|
+
"echo": "bar",
|
|
354
|
+
"time_sleep": 10,
|
|
355
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
359
|
+
|
|
360
|
+
resp = self.ws.recv()
|
|
361
|
+
resp = k3utfjson.load(resp)
|
|
362
|
+
self.assertEqual("foo", resp["result"])
|
|
363
|
+
|
|
364
|
+
def test_invalid_cpu_sample_interval(self):
|
|
365
|
+
job_desc = {
|
|
366
|
+
"func": "test_job_normal.run",
|
|
367
|
+
"ident": self.get_random_ident(),
|
|
368
|
+
"cpu_sample_interval": "foo",
|
|
369
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
373
|
+
|
|
374
|
+
resp = k3utfjson.load(self.ws.recv())
|
|
375
|
+
self.assertEqual("InvalidMessageError", resp["err"])
|
|
376
|
+
|
|
377
|
+
def test_invalid_check_load_args(self):
|
|
378
|
+
cases = (
|
|
379
|
+
{"check_load": {"mem_low_threshold": "foo", "cpu_low_threshold": 0}},
|
|
380
|
+
{"check_load": {"cpu_low_threshold": None, "mem_low_threshold": 0}},
|
|
381
|
+
{"check_load": {"max_client_number": {}, "cpu_low_threshold": 0, "mem_low_threshold": 0}},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
job_desc = {
|
|
385
|
+
"func": "test_job_normal.run",
|
|
386
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for case in cases:
|
|
390
|
+
case.update(job_desc)
|
|
391
|
+
case["ident"] = self.get_random_ident()
|
|
392
|
+
|
|
393
|
+
ws = self._create_client()
|
|
394
|
+
ws.send(k3utfjson.dump(case))
|
|
395
|
+
|
|
396
|
+
resp = k3utfjson.load(ws.recv())
|
|
397
|
+
ws.close()
|
|
398
|
+
self.assertEqual("InvalidMessageError", resp["err"])
|
|
399
|
+
|
|
400
|
+
def test_func_not_exists(self):
|
|
401
|
+
ident = self.get_random_ident()
|
|
402
|
+
job_desc = {
|
|
403
|
+
"func": "test_job_echo.func_not_exists",
|
|
404
|
+
"ident": ident,
|
|
405
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
409
|
+
resp = self.ws.recv()
|
|
410
|
+
resp = k3utfjson.load(resp)
|
|
411
|
+
self.assertEqual("LoadingError", resp["err"])
|
|
412
|
+
|
|
413
|
+
def test_worker_exception(self):
|
|
414
|
+
ident = self.get_random_ident()
|
|
415
|
+
job_desc = {
|
|
416
|
+
"func": "test_job_worker_exception.run",
|
|
417
|
+
"ident": ident,
|
|
418
|
+
"jobs_dir": "k3wsjobd/test/test_jobs",
|
|
419
|
+
"progress": {
|
|
420
|
+
"interval": 0.1,
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
self.ws.send(k3utfjson.dump(job_desc))
|
|
425
|
+
|
|
426
|
+
for i in range(10):
|
|
427
|
+
resp = self.ws.recv()
|
|
428
|
+
self.assertNotIn("err", resp)
|
|
429
|
+
|
|
430
|
+
with self.assertRaises(Exception):
|
|
431
|
+
for i in range(3):
|
|
432
|
+
self.ws.recv()
|
|
433
|
+
|
|
434
|
+
def test_create_job(self):
|
|
435
|
+
self.assertEqual(0, len(Job.sessions))
|
|
436
|
+
|
|
437
|
+
def f(self):
|
|
438
|
+
time.sleep(0.2)
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
Job("channel", {"ident": "a"}, f)
|
|
442
|
+
joba = Job.sessions["a"]
|
|
443
|
+
|
|
444
|
+
self.assertEqual(1, len(Job.sessions))
|
|
445
|
+
|
|
446
|
+
Job("channel", {"ident": "a"}, f)
|
|
447
|
+
joba1 = Job.sessions["a"]
|
|
448
|
+
|
|
449
|
+
# joba already exists
|
|
450
|
+
self.assertEqual(joba1, joba)
|
|
451
|
+
self.assertEqual(1, len(Job.sessions))
|
|
452
|
+
|
|
453
|
+
time.sleep(0.15)
|
|
454
|
+
Job("channel", {"ident": "b"}, f)
|
|
455
|
+
|
|
456
|
+
self.assertEqual(2, len(Job.sessions))
|
|
457
|
+
|
|
458
|
+
time.sleep(0.15)
|
|
459
|
+
self.assertEqual(1, len(Job.sessions))
|
|
460
|
+
|
|
461
|
+
time.sleep(0.1)
|
|
462
|
+
self.assertEqual(0, len(Job.sessions))
|
|
463
|
+
|
|
464
|
+
# test use same ident after first one exit
|
|
465
|
+
Job("channel", {"ident": "a"}, f)
|
|
466
|
+
joba1 = Job.sessions["a"]
|
|
467
|
+
|
|
468
|
+
time.sleep(0.25)
|
|
469
|
+
|
|
470
|
+
Job("channel", {"ident": "a"}, f)
|
|
471
|
+
joba2 = Job.sessions["a"]
|
|
472
|
+
self.assertNotEqual(joba1, joba2)
|
k3wsjobd-0.1.1/wsjobd.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
|
|
8
|
+
import psutil
|
|
9
|
+
from geventwebsocket import Resource
|
|
10
|
+
from geventwebsocket import WebSocketApplication
|
|
11
|
+
from geventwebsocket import WebSocketError
|
|
12
|
+
from geventwebsocket import WebSocketServer
|
|
13
|
+
|
|
14
|
+
import k3thread
|
|
15
|
+
import k3utfjson
|
|
16
|
+
import k3jobq
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
MEM_AVAILABLE = "mem_available"
|
|
21
|
+
CPU_IDLE_PERCENT = "cpu_idle_percent"
|
|
22
|
+
CLIENT_NUMBER = "client_number"
|
|
23
|
+
JOBS_DIR = "jobs"
|
|
24
|
+
|
|
25
|
+
# - `mem_low_threshold`
|
|
26
|
+
# set the min size of available memory the system should have, if not satified, return error. the default is 500M
|
|
27
|
+
#
|
|
28
|
+
# - `cpu_low_threshold`
|
|
29
|
+
# set the min idle percent of the system cpu, if not satified, return error. the default is 3
|
|
30
|
+
#
|
|
31
|
+
# - `max_client_number`
|
|
32
|
+
# set the max client number, if this number reached, new connection will be refused. the default is 1000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
CHECK_LOAD_PARAMS = {
|
|
36
|
+
"mem_low_threshold": {
|
|
37
|
+
"load_name": MEM_AVAILABLE,
|
|
38
|
+
"default": 500 * 1024**2, # 500M
|
|
39
|
+
"greater": True,
|
|
40
|
+
},
|
|
41
|
+
"cpu_low_threshold": {
|
|
42
|
+
"load_name": CPU_IDLE_PERCENT,
|
|
43
|
+
"default": 3, # 3%
|
|
44
|
+
"greater": True,
|
|
45
|
+
},
|
|
46
|
+
"max_client_number": {
|
|
47
|
+
"load_name": CLIENT_NUMBER,
|
|
48
|
+
"default": 1000,
|
|
49
|
+
"greater": False,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SystemOverloadError(Exception):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class JobError(Exception):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InvalidMessageError(JobError):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class InvalidProgressError(InvalidMessageError):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LoadingError(JobError):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class JobNotInSessionError(JobError):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Job(object):
|
|
79
|
+
lock = threading.RLock()
|
|
80
|
+
sessions = {}
|
|
81
|
+
|
|
82
|
+
def __init__(self, channel, msg, func):
|
|
83
|
+
"""
|
|
84
|
+
Job description is a string formatted in json, it is used to tell wsjobd what to do,
|
|
85
|
+
it can contain the following fields:
|
|
86
|
+
:param channel:
|
|
87
|
+
report_system_load: a boolean to indicate whether to report system load, if set to true and the progress info
|
|
88
|
+
is a dict, then the system load info will be add to progress dict by key `system_load`, the value is also a dict
|
|
89
|
+
, which contains three fields: `mem_available`, `cpu_idle_percent`, `client_number`.
|
|
90
|
+
cpu_sample_interval: set the interval used by psutil.cpu_times_percent, the default is 0.02
|
|
91
|
+
:param msg:
|
|
92
|
+
check_load: a dict to enable system load check, also to set customed thresholds, the can contain the following fields
|
|
93
|
+
ident : required. the identifier of a job, it is used to prevent from creating the same job repeatedly.
|
|
94
|
+
progress: a dict to set progress reporting related settings, it can contain the following fields:
|
|
95
|
+
- `interval`: the interval of progress reporting, default is 5 seconds
|
|
96
|
+
`key`: the sub field in which the progress info located
|
|
97
|
+
:param func: required. the function of job, it contain module name and function name, seperated by a dot,
|
|
98
|
+
the module shoud in the `jobs` directory.
|
|
99
|
+
"""
|
|
100
|
+
self.ident = msg["ident"]
|
|
101
|
+
self.channel = channel
|
|
102
|
+
self.data = msg
|
|
103
|
+
self.worker = func
|
|
104
|
+
self.ctx = {}
|
|
105
|
+
self.err = None
|
|
106
|
+
self.progress_available = threading.Event()
|
|
107
|
+
|
|
108
|
+
if self.ident in self.sessions:
|
|
109
|
+
logger.info(
|
|
110
|
+
"job: %s already exists, created by chennel %s" % (self.ident, repr(self.sessions[self.ident].channel))
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
else:
|
|
114
|
+
self.sessions[self.ident] = self
|
|
115
|
+
logger.info(
|
|
116
|
+
("inserted job: %s to sessions by channel %s, " + "there are %d jobs in sessions now")
|
|
117
|
+
% (self.ident, repr(self.channel), len(self.sessions))
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
self.thread = k3thread.start(target=self.work, args=(), daemon=True)
|
|
121
|
+
|
|
122
|
+
def work(self):
|
|
123
|
+
logger.info("job %s started, the data is: %s" % (self.ident, self.data))
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
self.worker(self)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.exception("job %s got exception: %s" % (self.ident, repr(e)))
|
|
129
|
+
self.err = e
|
|
130
|
+
finally:
|
|
131
|
+
logger.info("job %s ended" % self.ident)
|
|
132
|
+
self.close()
|
|
133
|
+
|
|
134
|
+
def close(self):
|
|
135
|
+
with self.lock:
|
|
136
|
+
del self.sessions[self.ident]
|
|
137
|
+
logger.info(
|
|
138
|
+
("removed job: %s from sessions, there are %d " + "jobs in sessions now")
|
|
139
|
+
% (self.ident, len(self.sessions))
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_or_create_job(channel, msg, func):
|
|
144
|
+
with Job.lock:
|
|
145
|
+
Job(channel, msg, func)
|
|
146
|
+
|
|
147
|
+
job = Job.sessions.get(msg["ident"])
|
|
148
|
+
|
|
149
|
+
return job
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def progress_sender(job, channel, interval=5, stat=None):
|
|
153
|
+
stat = stat or (lambda data: data)
|
|
154
|
+
data = job.data
|
|
155
|
+
|
|
156
|
+
i = 10
|
|
157
|
+
try:
|
|
158
|
+
while True:
|
|
159
|
+
# if thread died due to some reason, still send 10 stats
|
|
160
|
+
if not job.thread.is_alive():
|
|
161
|
+
logger.info("job %s died: %s" % (job.ident, repr(job.err)))
|
|
162
|
+
if i == 0:
|
|
163
|
+
channel.ws.close()
|
|
164
|
+
break
|
|
165
|
+
i -= 1
|
|
166
|
+
|
|
167
|
+
logger.info("jod %s on channel %s send progress: %s" % (job.ident, repr(channel), repr(stat(data))))
|
|
168
|
+
|
|
169
|
+
to_send = stat(data)
|
|
170
|
+
if channel.report_system_load and isinstance(to_send, dict):
|
|
171
|
+
to_send["system_load"] = channel.get_system_load()
|
|
172
|
+
|
|
173
|
+
channel.ws.send(k3utfjson.dump(to_send))
|
|
174
|
+
|
|
175
|
+
if job.progress_available.wait(interval):
|
|
176
|
+
job.progress_available.clear()
|
|
177
|
+
|
|
178
|
+
except WebSocketError as e:
|
|
179
|
+
if channel.ws.closed:
|
|
180
|
+
logger.info("the client has closed the connection")
|
|
181
|
+
else:
|
|
182
|
+
logger.exception(
|
|
183
|
+
("got websocket error when sending progress on" + " channel %s: %s") % (repr(channel), repr(e))
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.exception("got exception when sending progress on channel %s: %s" % (repr(channel), repr(e)))
|
|
188
|
+
channel.ws.close()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class JobdWebSocketApplication(WebSocketApplication):
|
|
192
|
+
jobq_mgr = None
|
|
193
|
+
|
|
194
|
+
def on_open(self):
|
|
195
|
+
logger.info("on open, the channel is: " + repr(self))
|
|
196
|
+
self.ignore_message = False
|
|
197
|
+
|
|
198
|
+
def _parse_request(self, message):
|
|
199
|
+
try:
|
|
200
|
+
try:
|
|
201
|
+
msg = k3utfjson.load(message)
|
|
202
|
+
except Exception:
|
|
203
|
+
raise InvalidMessageError("message is not a vaild json string: %s" % message)
|
|
204
|
+
|
|
205
|
+
self._check_msg(msg)
|
|
206
|
+
|
|
207
|
+
self.report_system_load = msg.get("report_system_load") is True
|
|
208
|
+
self.cpu_sample_interval = msg.get("cpu_sample_interval", 0.02)
|
|
209
|
+
|
|
210
|
+
if not isinstance(self.cpu_sample_interval, (int, float)):
|
|
211
|
+
raise InvalidMessageError("cpu_sample_interval is not a number")
|
|
212
|
+
|
|
213
|
+
check_load = msg.get("check_load")
|
|
214
|
+
if isinstance(check_load, dict):
|
|
215
|
+
self._check_system_load(check_load)
|
|
216
|
+
|
|
217
|
+
self.jobs_dir = msg.get("jobs_dir", JOBS_DIR)
|
|
218
|
+
|
|
219
|
+
self._setup_response(msg)
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
except SystemOverloadError as e:
|
|
223
|
+
logger.info("system overload on chennel %s, %s" % (repr(self), repr(e)))
|
|
224
|
+
self._send_err_and_close(e)
|
|
225
|
+
|
|
226
|
+
except JobError as e:
|
|
227
|
+
logger.info("error on channel %s while handling message, %s" % (repr(self), repr(e)))
|
|
228
|
+
self._send_err_and_close(e)
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
logger.exception(("exception on channel %s while handling " + "message, %s") % (repr(self), repr(e)))
|
|
232
|
+
self._send_err_and_close(e)
|
|
233
|
+
|
|
234
|
+
def on_message(self, message):
|
|
235
|
+
logger.info("on message, the channel is: %s, the message is: %s" % (repr(self), message))
|
|
236
|
+
if self.ignore_message:
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
else:
|
|
240
|
+
self.ignore_message = True
|
|
241
|
+
|
|
242
|
+
self.jobq_mgr.put((self, message))
|
|
243
|
+
|
|
244
|
+
def _send_err_and_close(self, err):
|
|
245
|
+
try:
|
|
246
|
+
err_msg = {
|
|
247
|
+
"err": err.__class__.__name__,
|
|
248
|
+
"val": err.args,
|
|
249
|
+
}
|
|
250
|
+
self.ws.send(k3utfjson.dump(err_msg))
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.error(("error on channel %s while sending back error " + "message, %s") % (repr(self), repr(e)))
|
|
253
|
+
|
|
254
|
+
def get_system_load(self):
|
|
255
|
+
return {
|
|
256
|
+
MEM_AVAILABLE: psutil.virtual_memory().available,
|
|
257
|
+
CPU_IDLE_PERCENT: psutil.cpu_times_percent(self.cpu_sample_interval).idle,
|
|
258
|
+
CLIENT_NUMBER: len(self.protocol.server.clients),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
def _check_system_load(self, check_load):
|
|
262
|
+
system_load = self.get_system_load()
|
|
263
|
+
|
|
264
|
+
for param_name, param_attr in CHECK_LOAD_PARAMS.items():
|
|
265
|
+
param_value = check_load.get(param_name, param_attr["default"])
|
|
266
|
+
|
|
267
|
+
if not isinstance(param_value, (int, float)):
|
|
268
|
+
raise InvalidMessageError("%s is not a number" % param_name)
|
|
269
|
+
|
|
270
|
+
load_name = param_attr["load_name"]
|
|
271
|
+
diff = system_load[load_name] - param_value
|
|
272
|
+
|
|
273
|
+
if not param_attr["greater"]:
|
|
274
|
+
diff = 0 - diff
|
|
275
|
+
|
|
276
|
+
if diff < 0:
|
|
277
|
+
raise SystemOverloadError(
|
|
278
|
+
"%s: %d is %s than: %d"
|
|
279
|
+
% (load_name, system_load[load_name], param_attr["greater"] and "less" or "greater", param_value)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _check_msg(self, msg):
|
|
283
|
+
if not isinstance(msg, dict):
|
|
284
|
+
raise InvalidMessageError("message is not dictionary")
|
|
285
|
+
|
|
286
|
+
if "ident" not in msg:
|
|
287
|
+
raise InvalidMessageError("'ident' is not in message")
|
|
288
|
+
|
|
289
|
+
if "func" not in msg:
|
|
290
|
+
raise InvalidMessageError("'func' is not in message")
|
|
291
|
+
|
|
292
|
+
def _setup_response(self, msg):
|
|
293
|
+
func = self._get_func_by_name(msg)
|
|
294
|
+
channel = self
|
|
295
|
+
job = get_or_create_job(channel, msg, func)
|
|
296
|
+
|
|
297
|
+
if job is None:
|
|
298
|
+
raise JobNotInSessionError("job not in sessions: " + repr(Job.sessions))
|
|
299
|
+
|
|
300
|
+
progress = msg.get("progress", {})
|
|
301
|
+
|
|
302
|
+
if progress in (None, False):
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
if not isinstance(progress, dict):
|
|
306
|
+
raise InvalidProgressError("the progress in message is not a dictionary")
|
|
307
|
+
|
|
308
|
+
interval = progress.get("interval", 5)
|
|
309
|
+
progress_key = progress.get("key")
|
|
310
|
+
|
|
311
|
+
def get_progress(r):
|
|
312
|
+
if progress_key is None:
|
|
313
|
+
return r
|
|
314
|
+
return r.get(progress_key)
|
|
315
|
+
|
|
316
|
+
k3thread.start(target=progress_sender, args=(job, channel, interval, get_progress), daemon=True)
|
|
317
|
+
|
|
318
|
+
def _get_func_by_name(self, msg):
|
|
319
|
+
mod_func = self.jobs_dir.split("/") + msg["func"].split(".")
|
|
320
|
+
mod_path = ".".join(mod_func[:-1])
|
|
321
|
+
func_name = mod_func[-1]
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
mod = __import__(mod_path)
|
|
325
|
+
except (ImportError, SyntaxError) as e:
|
|
326
|
+
raise LoadingError("failed to import %s: %s" % (mod_path, repr(e)))
|
|
327
|
+
|
|
328
|
+
for mod_name in mod_path.split(".")[1:]:
|
|
329
|
+
mod = getattr(mod, mod_name)
|
|
330
|
+
|
|
331
|
+
logger.info("mod imported from: " + repr(mod.__file__))
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
func = getattr(mod, func_name)
|
|
335
|
+
except AttributeError:
|
|
336
|
+
raise LoadingError("function not found: " + repr(func_name))
|
|
337
|
+
|
|
338
|
+
return func
|
|
339
|
+
|
|
340
|
+
def on_close(self, reason):
|
|
341
|
+
logger.info("on close, the channel is: " + repr(self))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _parse_request(args):
|
|
345
|
+
app, msg = args
|
|
346
|
+
app._parse_request(msg)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def run(ip="127.0.0.1", port=63482, jobq_thread_count=10):
|
|
350
|
+
JobdWebSocketApplication.jobq_mgr = k3jobq.JobManager([(_parse_request, jobq_thread_count)])
|
|
351
|
+
|
|
352
|
+
WebSocketServer(
|
|
353
|
+
(ip, port),
|
|
354
|
+
Resource(OrderedDict({"/": JobdWebSocketApplication})),
|
|
355
|
+
).serve_forever()
|