Skip to content

Commit df992bf

Browse files
authored
Merge pull request #6 from CodersOfTheNight/event_augment
Event augment
2 parents fd3d5cf + 158ffc3 commit df992bf

File tree

14 files changed

+273
-78
lines changed

14 files changed

+273
-78
lines changed

AUTHORS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Maintainers
2+
============
3+
Šarūnas Navickas (zaibacu@gmail.com): 2016 - now

CONTRIBUTING.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
New agents
2+
==========
3+
Most natural way of contributing to this project is creating new agents.
4+
What is agent? It is basically a metrics collector for specific service,
5+
eg. `oshino-redis` is an agent which runs `info` command as a Redis client,
6+
parses received metrics and gives them to `oshino` as events.
7+
8+
To make life easier, there's a `cookiecutter` template [oshino-cookiecutter](https://github.com/CodersOfTheNight/oshino-cookiecutter)
9+
which generates all boilerplate code, you just need to modify it.
10+
11+
Already existing custom agents are mostly listed here: [Third party Agents](https://github.com/CodersOfTheNight/oshino/blob/master/docs/thirdparty.md)
12+
13+
Contributing to the Core
14+
========================
15+
It should mostly consist of reporting/solving issues.
16+
17+
General Guidelines
18+
===================
19+
- Code must pass `flake8` (PEP8) coding style test
20+
- All code (with few exceptions) must be non-blocking - asyncio compatible
21+
- New parameters should be covered in documentation section

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,7 @@ Documentation about additional agents can be found [here](docs/thirdparty.md)
5656
More documentation
5757
==================
5858
More documentation can be found under [docs](docs/index.md) directory
59+
60+
Contributing
61+
============
62+
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)

docs/config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Base Config
66
- `riemann.transport` - set transport protocol for riemann (default: TCPTransport)
77
- `loglevel` - level of logging (default: INFO)
88
- `sentry-dsn` - sentry address for error reporting (default: `None`)
9+
- `executor` - allows to define executor class, default one is `concurrent.futures.ThreadPoolExecutor`
10+
- `executors-count` - set worker count for executor, default: 10
911

1012
General Config for Agents
1113
------------------------

oshino/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@ def agents(self):
111111
def sentry_dsn(self):
112112
return self._data.get("sentry-dsn", None)
113113

114+
@property
115+
def executors_count(self):
116+
return self._data.get("executors-count", 10)
117+
118+
@property
119+
def executor_class(self):
120+
raw = self._data.get(
121+
"executor",
122+
"concurrent.futures.ThreadPoolExecutor"
123+
)
124+
return dynamic_import(raw)
125+
114126

115127
def load(config_file):
116128
"""

oshino/core/heart.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ async def main_loop(cfg: Config,
8686
transport = transport_cls(riemann.host, riemann.port)
8787
client = processor.QClient(transport)
8888
agents = create_agents(cfg.agents)
89+
executor = cfg.executor_class(max_workers=cfg.executors_count)
90+
loop.set_default_executor(executor)
8991

9092
init(agents)
9193

@@ -105,7 +107,7 @@ async def main_loop(cfg: Config,
105107
len(client.queue.events),
106108
len(pending))
107109

108-
processor.flush(client, transport, logger)
110+
await processor.flush(client, transport, logger)
109111
if continue_fn():
110112
await asyncio.sleep(cfg.interval - int(td), loop=loop)
111113
else:

oshino/core/processor.py

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,107 @@
1+
import asyncio
2+
3+
from copy import copy
4+
from time import sleep
5+
6+
from logbook import Logger
7+
from queue import Queue
8+
19
from riemann_client.client import QueuedClient
210

311

12+
logger = Logger("Processor")
13+
14+
15+
class StopEvent(object):
16+
pass
17+
18+
419
class AugmentFixture(object):
5-
def apply_augment(self, **data):
6-
if "service" not in data:
20+
21+
def apply_augment(self, data):
22+
if "service" not in data or hasattr(self, "stopped"):
723
return
824

925
key = data["service"]
10-
if key in self.augments:
11-
subscribers = self.augments[key]
26+
if key not in self.augments:
27+
return
28+
29+
subscribers = self.augments[key]
30+
for sub in subscribers:
31+
sub.put_nowait(data)
32+
33+
def on_stop(self):
34+
if hasattr(self, "stopped"):
35+
return
36+
37+
logger.info("Stopping all augments")
38+
39+
self.stopped = True
40+
for _, subscribers in self.augments.items():
41+
for sub in subscribers:
42+
sub.put_nowait(StopEvent())
43+
44+
for _, subscribers in self.augments.items():
1245
for sub in subscribers:
13-
sub.send(data)
46+
while not sub.empty():
47+
sleep(0.1)
1448

1549

1650
class QClient(QueuedClient, AugmentFixture):
1751
def __init__(self, *args, **kwargs):
1852
super(QClient, self).__init__(*args, **kwargs)
1953
self.augments = {}
54+
self.tasks = []
2055

2156
def event(self, **data):
22-
self.apply_augment(**data)
57+
self.tasks.append(self.apply_augment(copy(data)))
2358
super(QClient, self).event(**data)
2459

2560

26-
def flush(client, transport, logger):
27-
try:
28-
transport.connect()
29-
client.flush()
30-
transport.disconnect()
31-
except ConnectionRefusedError as ce:
32-
logger.warn(ce)
61+
async def flush(client, transport, logger):
62+
future = asyncio.Future()
63+
64+
async def process_async(future):
65+
try:
66+
transport.connect()
67+
client.flush()
68+
transport.disconnect()
69+
70+
future.set_result(True)
71+
except ConnectionRefusedError as ce:
72+
logger.warn(ce)
3373

74+
future.set_result(False)
3475

35-
def register_augment(client, key, generator, logger):
76+
asyncio.ensure_future(process_async(future))
77+
await future
78+
return future.result()
79+
80+
81+
def register_augment(client, key, augment_fn, logger):
3682
if key not in client.augments:
3783
client.augments[key] = []
3884

39-
next(generator)
85+
loop = asyncio.get_event_loop()
86+
87+
def generator(q):
88+
stopped = False
89+
while not (stopped and q.empty()):
90+
logger.debug("Waiting for event for {0}".format(augment_fn))
91+
event = q.get(block=not stopped)
92+
if isinstance(event, StopEvent):
93+
logger.debug("Stopping event generator")
94+
stopped = True
95+
continue
96+
yield event
97+
raise StopIteration
98+
99+
q = Queue()
100+
g = generator(q)
101+
102+
def execute_in_thread(fn, client, g):
103+
fn(client, g)
104+
105+
loop.run_in_executor(None, execute_in_thread, augment_fn, client, g)
40106

41-
generator.send(client)
42-
client.augments[key].append(generator)
107+
client.augments[key].append(q)

pytest.ini

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
[pytest]
22
testpaths = tests
33
mock_use_standalone_module = true
4-
4+
markers =
5+
slow: mark a slow test
6+
addopts = -m "not webtest"

tests/data/test_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
interval: 10
33
loglevel: DEBUG
4+
executor: concurrent.futures.ProcessPoolExecutor
45
riemann:
56
host: localhost
67
port: 5555

tests/fixtures.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
import asyncio
2+
3+
from concurrent.futures import ThreadPoolExecutor
4+
15
from pytest import fixture
6+
27
from .mocks import MockClient, MockTransport
38

49

10+
511
@fixture
6-
def mock_client():
7-
return MockClient()
12+
def mock_client(request):
13+
client = MockClient()
14+
request.addfinalizer(client.on_stop)
15+
return client
816

917

1018
@fixture
@@ -15,3 +23,17 @@ def mock_transport():
1523
@fixture
1624
def broken_transport():
1725
return MockTransport(broken=True)
26+
27+
@fixture(scope="session")
28+
def executor(request):
29+
loop = asyncio.get_event_loop()
30+
print("Loop: {0}".format(loop))
31+
ex = ThreadPoolExecutor(max_workers=3)
32+
def on_stop():
33+
ex.shutdown(wait=True)
34+
loop.close()
35+
print("done closing")
36+
37+
loop.set_default_executor(ex)
38+
request.addfinalizer(on_stop)
39+
return ex

0 commit comments

Comments
 (0)