File size: 5,943 Bytes
5a5bab3
 
 
79604c9
5a5bab3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79604c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""等待所属同步工作真实终态;内部取消也不能遗留仍读写资源的线程。"""
import asyncio
from functools import wraps
from inspect import Parameter, signature, iscoroutinefunction, isasyncgenfunction, isgeneratorfunction
import threading


async def run_owned_thread(function, *args, **kwargs):
    ownership = threading.Lock()
    finished = threading.Event()
    started = False
    outcome = None

    def abandon_unstarted(result):
        nonlocal outcome
        with ownership:
            if not started and not finished.is_set():
                outcome = result
                finished.set()

    def execute():
        nonlocal started, outcome
        with ownership:
            if finished.is_set():
                return
            started = True
        try:
            result = (True, function(*args, **kwargs))
        except BaseException as error:
            # 同步异常作为数据交回;取消shield不暴露私有异常正文。
            result = (False, error)
        with ownership:
            outcome = result
            finished.set()

    async def finish():
        try:
            await asyncio.to_thread(execute)
        except asyncio.CancelledError:
            raise
        except BaseException as error:
            abandon_unstarted((False, error))

    coroutine = finish()
    try:
        task = asyncio.create_task(coroutine)
    except BaseException:
        coroutine.close()
        raise

    def consume(completed):
        if completed.cancelled():
            # 取消和线程启动竞争时,只有尚未开始的工作能被撤销。
            abandon_unstarted((False, asyncio.CancelledError()))
        else:
            error = completed.exception()
            if error is not None:
                abandon_unstarted((False, error))
    task.add_done_callback(consume)

    cancelled = None
    while not finished.is_set():
        try:
            if not task.done():
                await asyncio.shield(task)
            else:
                # 内部task已取消但线程仍在运行,不占线程池等待其终态。
                await asyncio.sleep(0.01)
        except asyncio.CancelledError as error:
            cancelled = error
    with ownership:
        succeeded, result = outcome
    if cancelled is not None:
        raise cancelled
    if not succeeded:
        raise result
    return result


class OwnedThreadGroup:
    def __init__(self, capacity, unavailable):
        if type(capacity) is not int or capacity < 1:
            raise ValueError('invalid owned thread capacity')
        self._capacity = threading.BoundedSemaphore(capacity)
        self._unavailable = unavailable
        self._gate = threading.Lock()
        self._accepting = True
        self._pending = 0
        self._closers = 0
        self._drained = threading.Event()
        self._drained.set()

    def start(self):
        with self._gate:
            if self._pending or self._closers:
                raise RuntimeError('owned work is still pending')
            self._accepting = True

    def stop(self):
        with self._gate:
            self._accepting = False

    async def close(self):
        with self._gate:
            self._accepting = False
            self._closers += 1
        try:
            cancelled = False
            while not self._drained.is_set():
                try:
                    await asyncio.sleep(.01)
                except asyncio.CancelledError:
                    cancelled = True
            if cancelled:
                raise asyncio.CancelledError
        finally:
            with self._gate:
                self._closers -= 1

    async def run(self, function, *args, **kwargs):
        with self._gate:
            if not self._accepting or not self._capacity.acquire(blocking=False):
                raise self._unavailable()
            self._pending += 1
            self._drained.clear()
        try:
            return await run_owned_thread(function, *args, **kwargs)
        finally:
            with self._gate:
                self._capacity.release()
                self._pending -= 1
                if not self._pending:
                    self._drained.set()


def owned_sync_endpoint(group):
    def decorate(endpoint):
        if iscoroutinefunction(endpoint) or isasyncgenfunction(endpoint) or isgeneratorfunction(endpoint):
            raise TypeError('owned endpoint must be a synchronous function')
        @wraps(endpoint)
        async def invoke(*args, **kwargs):
            return await group.run(endpoint, *args, **kwargs)
        return invoke
    return decorate


def owned_session_endpoint(group, session_factory, session_parameter):
    def decorate(endpoint):
        if iscoroutinefunction(endpoint) or isasyncgenfunction(endpoint) or isgeneratorfunction(endpoint):
            raise TypeError('owned session endpoint must be a synchronous function')
        original = signature(endpoint)
        if session_parameter not in original.parameters:
            raise ValueError('missing owned session parameter')
        if any(parameter.kind not in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
               for parameter in original.parameters.values()):
            raise TypeError('owned session endpoint requires named parameters')
        public = original.replace(parameters=[parameter for name, parameter in original.parameters.items()
                                               if name != session_parameter])
        @wraps(endpoint)
        async def invoke(*args, **kwargs):
            arguments = public.bind(*args, **kwargs).arguments
            def execute():
                with session_factory() as session:
                    return endpoint(**arguments, **{session_parameter: session})
            return await group.run(execute)
        invoke.__signature__ = public
        return invoke
    return decorate