mrok 0.4.6__py3-none-any.whl → 0.5.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.
- mrok/agent/devtools/inspector/app.py +2 -2
- mrok/agent/sidecar/app.py +61 -35
- mrok/agent/sidecar/main.py +5 -5
- mrok/agent/ziticorn.py +2 -2
- mrok/cli/commands/__init__.py +2 -2
- mrok/cli/commands/{proxy → frontend}/__init__.py +1 -1
- mrok/cli/commands/{proxy → frontend}/run.py +4 -4
- mrok/constants.py +0 -2
- mrok/controller/openapi/examples.py +13 -0
- mrok/frontend/__init__.py +3 -0
- mrok/frontend/app.py +75 -0
- mrok/{proxy → frontend}/main.py +4 -10
- mrok/proxy/__init__.py +0 -3
- mrok/proxy/app.py +157 -83
- mrok/proxy/backend.py +43 -0
- mrok/{http → proxy}/config.py +3 -3
- mrok/{datastructures.py → proxy/datastructures.py} +43 -10
- mrok/proxy/exceptions.py +22 -0
- mrok/proxy/lifespan.py +10 -0
- mrok/{master.py → proxy/master.py} +35 -38
- mrok/{metrics.py → proxy/metrics.py} +37 -49
- mrok/{http → proxy}/middlewares.py +47 -26
- mrok/proxy/streams.py +45 -0
- mrok/proxy/types.py +15 -0
- mrok/{http → proxy}/utils.py +1 -1
- {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/METADATA +2 -5
- {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/RECORD +33 -31
- mrok/http/__init__.py +0 -0
- mrok/http/forwarder.py +0 -354
- mrok/http/lifespan.py +0 -39
- mrok/http/pool.py +0 -239
- mrok/http/types.py +0 -18
- /mrok/{http → proxy}/constants.py +0 -0
- /mrok/{http → proxy}/protocol.py +0 -0
- /mrok/{http → proxy}/server.py +0 -0
- {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/WHEEL +0 -0
- {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/entry_points.txt +0 -0
- {mrok-0.4.6.dist-info → mrok-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
mrok/http/pool.py
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import contextlib
|
|
3
|
-
import logging
|
|
4
|
-
import time
|
|
5
|
-
from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine
|
|
6
|
-
from contextlib import asynccontextmanager
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from cachetools import TTLCache
|
|
10
|
-
|
|
11
|
-
from mrok.http.types import StreamPair
|
|
12
|
-
|
|
13
|
-
PoolItem = tuple[asyncio.StreamReader, asyncio.StreamWriter, float]
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger("mrok.proxy")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ConnectionPool:
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
pool_name: str,
|
|
22
|
-
factory: Callable[[], Awaitable[StreamPair]],
|
|
23
|
-
*,
|
|
24
|
-
initial_connections: int = 0,
|
|
25
|
-
max_size: int = 10,
|
|
26
|
-
idle_timeout: float = 30.0,
|
|
27
|
-
reaper_interval: float = 5.0,
|
|
28
|
-
) -> None:
|
|
29
|
-
if initial_connections < 0:
|
|
30
|
-
raise ValueError("initial_connections must be >= 0")
|
|
31
|
-
if max_size < 1:
|
|
32
|
-
raise ValueError("max_size must be >= 1")
|
|
33
|
-
if initial_connections > max_size:
|
|
34
|
-
raise ValueError("initial_connections cannot exceed max_size")
|
|
35
|
-
self.pool_name = pool_name
|
|
36
|
-
self.factory = factory
|
|
37
|
-
self.initial_connections = initial_connections
|
|
38
|
-
self.max_size = max_size
|
|
39
|
-
self.idle_timeout = idle_timeout
|
|
40
|
-
self.reaper_interval = reaper_interval
|
|
41
|
-
|
|
42
|
-
self._pool: list[PoolItem] = []
|
|
43
|
-
self._in_use = 0
|
|
44
|
-
self._lock = asyncio.Lock()
|
|
45
|
-
self._cond = asyncio.Condition()
|
|
46
|
-
self._stop_event = asyncio.Event()
|
|
47
|
-
|
|
48
|
-
self._started = False
|
|
49
|
-
self._reaper_task: asyncio.Task | None = None
|
|
50
|
-
|
|
51
|
-
async def start(self) -> None:
|
|
52
|
-
if self._started:
|
|
53
|
-
return
|
|
54
|
-
self._reaper_task = asyncio.create_task(self._reaper())
|
|
55
|
-
await self._prewarm()
|
|
56
|
-
self._started = True
|
|
57
|
-
|
|
58
|
-
async def stop(self) -> None:
|
|
59
|
-
self._stop_event.set()
|
|
60
|
-
if self._reaper_task is not None:
|
|
61
|
-
self._reaper_task.cancel()
|
|
62
|
-
with contextlib.suppress(Exception):
|
|
63
|
-
await self._reaper_task
|
|
64
|
-
|
|
65
|
-
to_close: list[asyncio.StreamWriter] = []
|
|
66
|
-
async with self._lock:
|
|
67
|
-
to_close = [writer for _, writer, _ in self._pool]
|
|
68
|
-
self._pool.clear()
|
|
69
|
-
for w in to_close:
|
|
70
|
-
with contextlib.suppress(Exception):
|
|
71
|
-
w.close()
|
|
72
|
-
await w.wait_closed()
|
|
73
|
-
|
|
74
|
-
async with self._cond:
|
|
75
|
-
self._cond.notify_all()
|
|
76
|
-
|
|
77
|
-
@asynccontextmanager
|
|
78
|
-
async def acquire(self) -> AsyncGenerator[StreamPair]:
|
|
79
|
-
if not self._started:
|
|
80
|
-
await self.start()
|
|
81
|
-
reader, writer = await self._acquire()
|
|
82
|
-
logger.info(
|
|
83
|
-
f"Acquire stats for pool {self.pool_name}: "
|
|
84
|
-
f"in_use={self._in_use}, size={len(self._pool)}"
|
|
85
|
-
)
|
|
86
|
-
try:
|
|
87
|
-
yield (reader, writer)
|
|
88
|
-
finally:
|
|
89
|
-
await self._release(reader, writer)
|
|
90
|
-
|
|
91
|
-
async def _prewarm(self) -> None:
|
|
92
|
-
conns: list[PoolItem] = []
|
|
93
|
-
needed = max(0, self.initial_connections - (self._in_use + len(self._pool)))
|
|
94
|
-
for _ in range(needed):
|
|
95
|
-
reader, writer = await self.factory()
|
|
96
|
-
conns.append((reader, writer, time.time()))
|
|
97
|
-
if conns:
|
|
98
|
-
async with self._lock:
|
|
99
|
-
self._pool.extend(conns)
|
|
100
|
-
# notify any waiters
|
|
101
|
-
async with self._cond:
|
|
102
|
-
self._cond.notify_all()
|
|
103
|
-
|
|
104
|
-
async def _acquire(self) -> StreamPair: # type: ignore
|
|
105
|
-
to_close: list[asyncio.StreamWriter] = []
|
|
106
|
-
create_new = False
|
|
107
|
-
while True:
|
|
108
|
-
need_prewarm = False
|
|
109
|
-
async with self._cond:
|
|
110
|
-
now = time.time()
|
|
111
|
-
if not self._pool:
|
|
112
|
-
need_prewarm = True
|
|
113
|
-
while self._pool:
|
|
114
|
-
reader, writer, ts = self._pool.pop()
|
|
115
|
-
if now - ts <= self.idle_timeout and not writer.is_closing():
|
|
116
|
-
self._in_use += 1
|
|
117
|
-
return reader, writer
|
|
118
|
-
to_close.append(writer)
|
|
119
|
-
|
|
120
|
-
total = self._in_use + len(self._pool)
|
|
121
|
-
if total < self.max_size:
|
|
122
|
-
self._in_use += 1
|
|
123
|
-
create_new = True
|
|
124
|
-
break
|
|
125
|
-
await self._cond.wait()
|
|
126
|
-
|
|
127
|
-
if need_prewarm:
|
|
128
|
-
await self._prewarm()
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
for w in to_close:
|
|
132
|
-
with contextlib.suppress(Exception):
|
|
133
|
-
w.close()
|
|
134
|
-
await w.wait_closed()
|
|
135
|
-
|
|
136
|
-
if create_new:
|
|
137
|
-
try:
|
|
138
|
-
reader, writer = await self.factory()
|
|
139
|
-
except Exception:
|
|
140
|
-
async with self._cond:
|
|
141
|
-
if self._in_use > 0:
|
|
142
|
-
self._in_use -= 1
|
|
143
|
-
self._cond.notify()
|
|
144
|
-
raise
|
|
145
|
-
return reader, writer
|
|
146
|
-
|
|
147
|
-
async def _release(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
148
|
-
async with self._cond:
|
|
149
|
-
if self._in_use > 0:
|
|
150
|
-
self._in_use -= 1
|
|
151
|
-
|
|
152
|
-
if not writer.is_closing():
|
|
153
|
-
self._pool.append((reader, writer, time.time()))
|
|
154
|
-
|
|
155
|
-
self._cond.notify()
|
|
156
|
-
logger.info(
|
|
157
|
-
f"Release stats for pool {self.pool_name}: "
|
|
158
|
-
f"in_use={self._in_use}, size={len(self._pool)}"
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
async def _reaper(self) -> None:
|
|
162
|
-
try:
|
|
163
|
-
while not self._stop_event.is_set():
|
|
164
|
-
await asyncio.sleep(self.reaper_interval)
|
|
165
|
-
to_close: list[asyncio.StreamWriter] = []
|
|
166
|
-
now = time.time()
|
|
167
|
-
async with self._lock:
|
|
168
|
-
new_pool: list[PoolItem] = []
|
|
169
|
-
for reader, writer, ts in self._pool:
|
|
170
|
-
if now - ts > self.idle_timeout or writer.is_closing():
|
|
171
|
-
to_close.append(writer)
|
|
172
|
-
else:
|
|
173
|
-
new_pool.append((reader, writer, ts))
|
|
174
|
-
self._pool = new_pool
|
|
175
|
-
|
|
176
|
-
for w in to_close:
|
|
177
|
-
with contextlib.suppress(Exception):
|
|
178
|
-
w.close()
|
|
179
|
-
await w.wait_closed()
|
|
180
|
-
|
|
181
|
-
async with self._cond:
|
|
182
|
-
self._cond.notify_all()
|
|
183
|
-
except asyncio.CancelledError:
|
|
184
|
-
pass
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
class SlidingTTLCache(TTLCache):
|
|
188
|
-
def __init__(
|
|
189
|
-
self,
|
|
190
|
-
*,
|
|
191
|
-
maxsize: float,
|
|
192
|
-
ttl: float,
|
|
193
|
-
on_evict: Callable[[Any], Coroutine[Any, Any, None]] | None,
|
|
194
|
-
) -> None:
|
|
195
|
-
super().__init__(maxsize=maxsize, ttl=ttl)
|
|
196
|
-
self.on_evict = on_evict
|
|
197
|
-
|
|
198
|
-
def __getitem__(self, key: Any) -> Any:
|
|
199
|
-
value = super().__getitem__(key)
|
|
200
|
-
super().__setitem__(key, value)
|
|
201
|
-
return value
|
|
202
|
-
|
|
203
|
-
def popitem(self) -> Any:
|
|
204
|
-
key, value = super().popitem()
|
|
205
|
-
if self.on_evict:
|
|
206
|
-
asyncio.create_task(self.on_evict(value))
|
|
207
|
-
return key, value
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
class PoolManager:
|
|
211
|
-
def __init__(
|
|
212
|
-
self,
|
|
213
|
-
pool_factory: Callable[[Any], Awaitable[ConnectionPool]],
|
|
214
|
-
idle_timeout: int = 300,
|
|
215
|
-
):
|
|
216
|
-
self.factory = pool_factory
|
|
217
|
-
self.cache = SlidingTTLCache(
|
|
218
|
-
maxsize=float("inf"),
|
|
219
|
-
ttl=idle_timeout,
|
|
220
|
-
on_evict=self._close_pool,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
async def _close_pool(self, pool: ConnectionPool):
|
|
224
|
-
with contextlib.suppress(Exception):
|
|
225
|
-
await pool.stop()
|
|
226
|
-
|
|
227
|
-
async def get_pool(self, key) -> ConnectionPool:
|
|
228
|
-
try:
|
|
229
|
-
return self.cache[key]
|
|
230
|
-
except KeyError:
|
|
231
|
-
pool = await self.factory(key)
|
|
232
|
-
await pool.start()
|
|
233
|
-
self.cache[key] = pool
|
|
234
|
-
return pool
|
|
235
|
-
|
|
236
|
-
async def shutdown(self) -> None:
|
|
237
|
-
pools = list(self.cache.values())
|
|
238
|
-
for pool in pools:
|
|
239
|
-
await self._close_pool(pool)
|
mrok/http/types.py
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from collections.abc import Awaitable, Callable, MutableMapping
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from mrok.datastructures import HTTPRequest, HTTPResponse
|
|
8
|
-
|
|
9
|
-
Scope = MutableMapping[str, Any]
|
|
10
|
-
Message = MutableMapping[str, Any]
|
|
11
|
-
|
|
12
|
-
ASGIReceive = Callable[[], Awaitable[Message]]
|
|
13
|
-
ASGISend = Callable[[Message], Awaitable[None]]
|
|
14
|
-
ASGIApp = Callable[[Scope, ASGIReceive, ASGISend], Awaitable[None]]
|
|
15
|
-
RequestCompleteCallback = Callable[[HTTPRequest], Awaitable | None]
|
|
16
|
-
ResponseCompleteCallback = Callable[[HTTPResponse], Awaitable | None]
|
|
17
|
-
|
|
18
|
-
StreamPair = tuple[asyncio.StreamReader, asyncio.StreamWriter]
|
|
File without changes
|
/mrok/{http → proxy}/protocol.py
RENAMED
|
File without changes
|
/mrok/{http → proxy}/server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|