File size: 3,830 Bytes
b9d11ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""资料文件I/O在后台线程执行;准入在排队前,已开始工作取消不提前释放容量。"""
import asyncio
import threading
from fastapi import HTTPException

MAX_PROFILE_IO = 8

class ProfileIO:
    def __init__(self, capacity=MAX_PROFILE_IO):
        self.capacity = threading.BoundedSemaphore(capacity)
        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("profile work is still pending")
            self._accepting = True

    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(0.01)
                except asyncio.CancelledError:
                    cancelled = True
            if cancelled:
                raise asyncio.CancelledError
        finally:
            with self._gate:
                self._closers -= 1

    def _finish(self):
        with self._gate:
            self.capacity.release()
            self._pending -= 1
            if not self._pending:
                self._drained.set()

    async def run(self, function, *args, **kwargs):
        with self._gate:
            if not self._accepting or not self.capacity.acquire(blocking=False):
                raise HTTPException(status_code=503, detail="用户数据暂不可用")
            self._pending += 1
            self._drained.clear()

        # 内部任务也可能在宿主关闭时被取消。锁区分未开始与已开始的工作,
        # 保证取消和线程启动竞争时只释放一次,且取消后不再开始业务写入。
        ownership = threading.Lock()
        started = False
        released = False

        def release_unstarted():
            nonlocal released
            with ownership:
                if not started and not released:
                    released = True
                    self._finish()

        def execute():
            nonlocal started, released
            with ownership:
                if released:
                    return None
                started = True
            try:
                return function(*args, **kwargs)
            finally:
                with ownership:
                    released = True
                    self._finish()

        coroutine = asyncio.to_thread(execute)
        try:
            task = asyncio.create_task(coroutine)
        except BaseException:
            coroutine.close(); release_unstarted()
            raise

        def consume(completed):
            if completed.cancelled():
                release_unstarted()
            else:
                # 提交默认线程池本身也可能失败,尚未开始的准入必须收回。
                release_unstarted()
                completed.exception()
        task.add_done_callback(consume)
        # 外层请求取消不取消所属任务;已开始的工作只在线程终态释放容量。
        return await asyncio.shield(task)

_profile_io = ProfileIO()
run_profile_io = _profile_io.run

start_profile_io = _profile_io.start
close_profile_io = _profile_io.close