DefStream 0.1.5__tar.gz → 0.1.6__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.
Files changed (93) hide show
  1. {defstream-0.1.5 → defstream-0.1.6}/DefStream.egg-info/PKG-INFO +6 -1
  2. {defstream-0.1.5 → defstream-0.1.6}/DefStream.egg-info/SOURCES.txt +3 -0
  3. {defstream-0.1.5 → defstream-0.1.6}/PKG-INFO +6 -1
  4. {defstream-0.1.5 → defstream-0.1.6}/README.en.md +5 -0
  5. {defstream-0.1.5 → defstream-0.1.6}/README.md +5 -0
  6. defstream-0.1.6/issues/fix/3/IMPLEMENTS.md +133 -0
  7. defstream-0.1.6/issues/fix/3/README.md +47 -0
  8. defstream-0.1.6/issues/fix/3/REQUIREMENTS.md +52 -0
  9. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Connection.py +1 -0
  10. {defstream-0.1.5 → defstream-0.1.6}/src/Worker/Worker.py +3 -11
  11. {defstream-0.1.5 → defstream-0.1.6}/src/Worker/__init__.py +0 -2
  12. {defstream-0.1.5 → defstream-0.1.6}/test/test_inspector.py +1 -2
  13. {defstream-0.1.5 → defstream-0.1.6}/test/test_lifecycle_hooks.py +1 -1
  14. {defstream-0.1.5 → defstream-0.1.6}/test/test_protocol_contracts.py +60 -10
  15. {defstream-0.1.5 → defstream-0.1.6}/test/test_runner_hooks.py +1 -2
  16. {defstream-0.1.5 → defstream-0.1.6}/test/test_worker.py +1 -2
  17. {defstream-0.1.5 → defstream-0.1.6}/test/test_worker_startup.py +1 -1
  18. {defstream-0.1.5 → defstream-0.1.6}/.github/workflows/ci.yml +0 -0
  19. {defstream-0.1.5 → defstream-0.1.6}/.gitignore +0 -0
  20. {defstream-0.1.5 → defstream-0.1.6}/DefStream.egg-info/dependency_links.txt +0 -0
  21. {defstream-0.1.5 → defstream-0.1.6}/DefStream.egg-info/requires.txt +0 -0
  22. {defstream-0.1.5 → defstream-0.1.6}/DefStream.egg-info/top_level.txt +0 -0
  23. {defstream-0.1.5 → defstream-0.1.6}/LICENSE +0 -0
  24. {defstream-0.1.5 → defstream-0.1.6}/issues/feature/1/IMPLEMENTS.md +0 -0
  25. {defstream-0.1.5 → defstream-0.1.6}/issues/feature/1/README.md +0 -0
  26. {defstream-0.1.5 → defstream-0.1.6}/issues/feature/1/REQUIREMENTS.md +0 -0
  27. {defstream-0.1.5 → defstream-0.1.6}/issues/feature/1/TASK.md +0 -0
  28. {defstream-0.1.5 → defstream-0.1.6}/issues/feature/1/ci.refs.json +0 -0
  29. {defstream-0.1.5 → defstream-0.1.6}/sample/Broker/Sample.py +0 -0
  30. {defstream-0.1.5 → defstream-0.1.6}/sample/Worker/RabbitMQ/Client.py +0 -0
  31. {defstream-0.1.5 → defstream-0.1.6}/sample/Worker/RabbitMQ/Tasks.py +0 -0
  32. {defstream-0.1.5 → defstream-0.1.6}/sample/Worker/RabbitMQ/Worker.py +0 -0
  33. {defstream-0.1.5 → defstream-0.1.6}/sample/Worker/Redis/Sample.py +0 -0
  34. {defstream-0.1.5 → defstream-0.1.6}/sample/Worker/Sample.py +0 -0
  35. {defstream-0.1.5 → defstream-0.1.6}/setup.cfg +0 -0
  36. {defstream-0.1.5 → defstream-0.1.6}/setup.py +0 -0
  37. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Configuration.py +0 -0
  38. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Consumer.py +0 -0
  39. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Errors.py +0 -0
  40. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Event.py +0 -0
  41. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Implements/__init__.py +0 -0
  42. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Policies.py +0 -0
  43. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Priority.py +0 -0
  44. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/Protocol.py +0 -0
  45. {defstream-0.1.5 → defstream-0.1.6}/src/Broker/__init__.py +0 -0
  46. {defstream-0.1.5 → defstream-0.1.6}/src/Descriptor/DataObject.py +0 -0
  47. {defstream-0.1.5 → defstream-0.1.6}/src/Descriptor/Types.py +0 -0
  48. {defstream-0.1.5 → defstream-0.1.6}/src/Descriptor/Value.py +0 -0
  49. {defstream-0.1.5 → defstream-0.1.6}/src/Descriptor/__init__.py +0 -0
  50. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extension.py +0 -0
  51. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extensions/Console.html +0 -0
  52. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extensions/Console.py +0 -0
  53. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extensions/Dashboard.html +0 -0
  54. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extensions/Dashboard.py +0 -0
  55. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extensions/HealthCheck.py +0 -0
  56. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extensions/Metrics.py +0 -0
  57. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extensions/Specifications.py +0 -0
  58. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Extensions/__init__.py +0 -0
  59. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Inspector.py +0 -0
  60. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/Response.py +0 -0
  61. {defstream-0.1.5 → defstream-0.1.6}/src/Inspector/__init__.py +0 -0
  62. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Pattern.py +0 -0
  63. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/Array.py +0 -0
  64. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/Common.py +0 -0
  65. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/Compare.py +0 -0
  66. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/Condition.py +0 -0
  67. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/DataObject.py +0 -0
  68. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/DateTime.py +0 -0
  69. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/Number.py +0 -0
  70. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/Object.py +0 -0
  71. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/Size.py +0 -0
  72. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/String.py +0 -0
  73. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/Type.py +0 -0
  74. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Patterns/__init__.py +0 -0
  75. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Validate.py +0 -0
  76. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/Validator.py +0 -0
  77. {defstream-0.1.5 → defstream-0.1.6}/src/Validator/__init__.py +0 -0
  78. {defstream-0.1.5 → defstream-0.1.6}/src/Worker/Executor.py +0 -0
  79. {defstream-0.1.5 → defstream-0.1.6}/src/Worker/Message.py +0 -0
  80. {defstream-0.1.5 → defstream-0.1.6}/src/Worker/Pool.py +0 -0
  81. {defstream-0.1.5 → defstream-0.1.6}/src/Worker/Run.py +0 -0
  82. {defstream-0.1.5 → defstream-0.1.6}/src/Worker/Specification.py +0 -0
  83. {defstream-0.1.5 → defstream-0.1.6}/src/Worker/Startup.py +0 -0
  84. {defstream-0.1.5 → defstream-0.1.6}/src/__init__.py +0 -0
  85. {defstream-0.1.5 → defstream-0.1.6}/test/test_dashboard.py +0 -0
  86. {defstream-0.1.5 → defstream-0.1.6}/test/test_dashboard_ui.cjs +0 -0
  87. {defstream-0.1.5 → defstream-0.1.6}/test/test_inspector_console.py +0 -0
  88. {defstream-0.1.5 → defstream-0.1.6}/test/test_pool_callbacks.py +0 -0
  89. {defstream-0.1.5 → defstream-0.1.6}/test/test_process_signals.py +0 -0
  90. {defstream-0.1.5 → defstream-0.1.6}/test/test_release_workflow.py +0 -0
  91. {defstream-0.1.5 → defstream-0.1.6}/test/test_specifications.py +0 -0
  92. {defstream-0.1.5 → defstream-0.1.6}/test/test_task_messages.py +0 -0
  93. {defstream-0.1.5 → defstream-0.1.6}/test/test_task_priorities.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DefStream
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Synchronous message processing with pluggable broker adapters
5
5
  Home-page: https://github.com/yong5eon/DefStream
6
6
  Author: Heo Yongseon
@@ -124,6 +124,11 @@ class Add(
124
124
 
125
125
  ### 2. Worker 실행: `worker.py`
126
126
 
127
+ `Worker`는 `DefStream.Broker.Connection`을 직접 받습니다.
128
+ 연결 계약은 `connect()`, `close()`만 요구하며, 워커 실행 시 내부에서
129
+ `GetConsumer` 지원 여부를 검사합니다. 컨슈머를 생성할 수 없는 브로커는 `TypeError`를 발생시키며,
130
+ 워커 전용 연결 타입은 필요하지 않습니다.
131
+
127
132
  ```python
128
133
  from signal import SIGINT, signal
129
134
 
@@ -14,6 +14,9 @@ issues/feature/1/README.md
14
14
  issues/feature/1/REQUIREMENTS.md
15
15
  issues/feature/1/TASK.md
16
16
  issues/feature/1/ci.refs.json
17
+ issues/fix/3/IMPLEMENTS.md
18
+ issues/fix/3/README.md
19
+ issues/fix/3/REQUIREMENTS.md
17
20
  sample/Broker/Sample.py
18
21
  sample/Worker/Sample.py
19
22
  sample/Worker/RabbitMQ/Client.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DefStream
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: Synchronous message processing with pluggable broker adapters
5
5
  Home-page: https://github.com/yong5eon/DefStream
6
6
  Author: Heo Yongseon
@@ -124,6 +124,11 @@ class Add(
124
124
 
125
125
  ### 2. Worker 실행: `worker.py`
126
126
 
127
+ `Worker`는 `DefStream.Broker.Connection`을 직접 받습니다.
128
+ 연결 계약은 `connect()`, `close()`만 요구하며, 워커 실행 시 내부에서
129
+ `GetConsumer` 지원 여부를 검사합니다. 컨슈머를 생성할 수 없는 브로커는 `TypeError`를 발생시키며,
130
+ 워커 전용 연결 타입은 필요하지 않습니다.
131
+
127
132
  ```python
128
133
  from signal import SIGINT, signal
129
134
 
@@ -93,6 +93,11 @@ declares an error queue to receive rejected messages.
93
93
 
94
94
  ### 2. Run the Worker: `worker.py`
95
95
 
96
+ `Worker` accepts `DefStream.Broker.Connection` directly.
97
+ The connection contract only requires `connect()` and `close()`. The worker checks
98
+ for `GetConsumer` support internally when it runs. A broker that cannot create
99
+ a consumer raises `TypeError`; no worker-specific connection type is needed.
100
+
96
101
  ```python
97
102
  from signal import SIGINT, signal
98
103
 
@@ -91,6 +91,11 @@ class Add(
91
91
 
92
92
  ### 2. Worker 실행: `worker.py`
93
93
 
94
+ `Worker`는 `DefStream.Broker.Connection`을 직접 받습니다.
95
+ 연결 계약은 `connect()`, `close()`만 요구하며, 워커 실행 시 내부에서
96
+ `GetConsumer` 지원 여부를 검사합니다. 컨슈머를 생성할 수 없는 브로커는 `TypeError`를 발생시키며,
97
+ 워커 전용 연결 타입은 필요하지 않습니다.
98
+
94
99
  ```python
95
100
  from signal import SIGINT, signal
96
101
 
@@ -0,0 +1,133 @@
1
+ # 워커 브로커 연결 타입 단순화 구현 기록
2
+
3
+ ## 1. 구현 개요
4
+
5
+ [요구사항 명세서](./REQUIREMENTS.md)에 따라 워커 전용 합성 Protocol을 제거하고,
6
+ 공통 연결 타입과 내부 기능 검사를 사용하도록 변경했다.
7
+ 대상 브랜치는 `fix/3`이다.
8
+
9
+ ## 2. 구현 내용
10
+
11
+ ### 공통 브로커 계약
12
+
13
+ - [Connection.py](../../../src/Broker/Connection.py)의 `Connection`은 기존처럼
14
+ `connect()`와 `close()`만 요구한다.
15
+ - `GetConsumer`에 `@runtime_checkable`을 추가했다.
16
+ - 두 계약은 독립적으로 유지한다. 컨슈머가 없는 발행 전용 연결도 공통
17
+ `Connection`을 구현할 수 있다.
18
+
19
+ ### 워커와 공개 API
20
+
21
+ - [Worker.py](../../../src/Worker/Worker.py)의 생성자 입력을
22
+ `broker: Connection`으로 변경했다.
23
+ - `WorkerConnection` 클래스와 불필요한 `Protocol` import를 제거했다.
24
+ - [공개 export](../../../src/Worker/__init__.py)에서도 `WorkerConnection`을 제거했다.
25
+ - `run()`에서 기존 순서대로 브로커 연결 및 setup을 실행한 다음,
26
+ `consumer()` 호출 직전에 `isinstance(self.broker, GetConsumer)`를 검사한다.
27
+ - 미지원이면 `TypeError('Worker broker must support consumer creation')`를
28
+ 발생시킨다. 검사는 기존 `try/finally` 안에 있어 실패해도 종료 처리를 유지한다.
29
+ - 작업 실행, ACK/NACK, 실패 정책, RPC, Inspector와 실행 풀 로직은 변경하지 않았다.
30
+
31
+ ### 테스트와 사용자 문서
32
+
33
+ - [계약 테스트](../../../test/test_protocol_contracts.py)에서 생성자 타입,
34
+ 공개 export 제거, 구조적 연결 및 두 어댑터의 수용, 미지원 연결의 실패를 검증한다.
35
+ - 워커·수명 주기·시작 배너·Inspector·작업 훅 테스트의 브로커 Mock은
36
+ `consumer`를 생성 인자로 명시하도록 수정했다.
37
+ Python 3.12의 런타임 Protocol 검사에서 동적으로 만들어지는 Mock 속성에
38
+ 의존하지 않게 하기 위한 변경이며, 제품 코드에 Mock 전용 우회는 추가하지 않았다.
39
+ - [한국어 사용 설명서](../../../README.md)와 [영어 사용 설명서](../../../README.en.md)에
40
+ 공통 연결 입력과 내부 검사 동작을 동일하게 반영했다.
41
+
42
+ ## 3. Docker RabbitMQ 검증 환경
43
+
44
+ | 항목 | 값 |
45
+ |---|---|
46
+ | 검증일 | 2026-10-19 |
47
+ | 운영체제 | Windows |
48
+ | Python | 3.12.10, 작업 폴더의 `.venv` |
49
+ | 컨테이너 | `defstream-rabbitmq` |
50
+ | 로컬 이미지 | `rabbitmq:3.13.7-compose` |
51
+ | 실제 서버 버전 | `rabbitmq-diagnostics -q server_version`: `3.13.7` |
52
+ | AMQP 접속 | `127.0.0.1:5672`, 기본 테스트 사용자 `guest` |
53
+ | 포트 공개 범위 | 호스트 loopback으로 제한 |
54
+ | 연동 옵션 | `DEFSTREAM_RABBITMQ_TESTS=1` |
55
+ | 출력 인코딩 | `PYTHONIOENCODING=utf-8`, `PYTHONUTF8=1` |
56
+
57
+ 기존에 같은 이름의 컨테이너가 없어 테스트 전용 컨테이너를 생성했다.
58
+ 다른 애플리케이션의 RabbitMQ 컨테이너는 변경하지 않았다.
59
+
60
+ 작업 폴더 루트에서 실행한 Docker 명령은 다음과 같다.
61
+
62
+ ```powershell
63
+ docker run -d --name defstream-rabbitmq -p 127.0.0.1:5672:5672 rabbitmq:3.13.7-compose
64
+ docker exec defstream-rabbitmq rabbitmq-diagnostics -q ping
65
+ docker exec defstream-rabbitmq rabbitmq-diagnostics -q server_version
66
+ ```
67
+
68
+ `running true`, `Ping succeeded`, AMQP listener 시작을 확인한 뒤 테스트했다.
69
+
70
+ ## 4. 소스 선택과 실행 방식
71
+
72
+ 가상환경에는 별도로 설치된 패키지가 있어 단순 `unittest discover`만으로는
73
+ 현재 변경을 검증할 수 없다. 임시 검증 스크립트에서 `importlib.util`의
74
+ `spec_from_file_location`과 `submodule_search_locations`로 다음 패키지를 직접 로드했다.
75
+
76
+ | 패키지 | 작업 폴더 루트 기준 소스 |
77
+ |---|---|
78
+ | `DefStream` | `DefStream\src` |
79
+ | `DefStream.Broker.Implements.RabbitMQ` | `DefStream-RabbitMQ\src` |
80
+ | `DefStream.Broker.Implements.Redis` | `DefStream-Redis\src` |
81
+
82
+ 로드한 모듈은 `sys.modules`에 등록했다. 스크립트의 최상위에서 소스를 로드하여
83
+ Windows의 프로세스 풀 자식 프로세스도 같은 소스를 사용하게 했다.
84
+ 테스트 실행은 `if __name__ == '__main__'` 안에서 수행했다.
85
+ 실행 로그에 코어와 RabbitMQ 어댑터의 실제 로드 경로를 출력해 확인했다.
86
+
87
+ 각 패키지의 테스트는 별도 Python 프로세스에서
88
+ `unittest.TestLoader().discover(..., pattern='test_*.py')`와
89
+ `unittest.TextTestRunner(verbosity=2)`로 실행했다.
90
+ 코어 테스트 디렉터리는 `DefStream\test`, 어댑터 테스트 디렉터리는
91
+ `DefStream-RabbitMQ\test`이다. 테스트 전에 다음 환경 변수를 설정했다.
92
+
93
+ ```powershell
94
+ $env:PYTHONIOENCODING = 'utf-8'
95
+ $env:PYTHONUTF8 = '1'
96
+ $env:DEFSTREAM_RABBITMQ_TESTS = '1'
97
+ ```
98
+
99
+ ## 5. 검증 결과
100
+
101
+ | 검증 대상 | 전체 | 통과 | 건너뜀 | 실패·오류 | 소요 시간 |
102
+ |---|---:|---:|---:|---:|---:|
103
+ | 코어 전체 Python 테스트 | 160 | 156 | 4 | 0 | 42.870초 |
104
+ | RabbitMQ 어댑터 전체 Python 테스트 | 60 | 60 | 0 | 0 | 7.345초 |
105
+ | 합계 | 220 | 216 | 4 | 0 | 50.215초 |
106
+
107
+ 코어에서 건너뛴 4개는 Redis 전용 통합 테스트이다.
108
+ RabbitMQ 선택적 통합 테스트는 활성화되어 실제 서버에 접속했다.
109
+
110
+ 다음 경로의 성공을 확인했다.
111
+
112
+ - 워커 전용 타입 없이 구조적 연결과 RabbitMQ·Redis 연결 수용.
113
+ - 컨슈머 미지원 연결의 명시적 실패와 실행 풀 종료·완료 상태.
114
+ - 실제 RabbitMQ 메시지 소비, 재전달, 거부 및 워커 종료.
115
+ - 스레드 풀과 프로세스 풀을 통한 실제 작업 처리.
116
+ - RPC 성공, 빈 결과, 재전달, 원격 오류, 타임아웃과 라우팅 불가 처리.
117
+ - Inspector의 실제 RabbitMQ 프로세스 풀 상태·메트릭 및 Console 전송·RPC.
118
+ - 실제 RPC에서 요청 검증과 응답 변환.
119
+ - 우선순위 관련 어댑터 회귀 테스트.
120
+
121
+ 수정한 코어의 `Worker.py`, `Broker/Connection.py`는 Pylance 진단 오류가 없었다.
122
+ `git diff --check`도 통과했다. 전체 저장소의 정적 분석 통과를 의미하지는 않는다.
123
+
124
+ 검증 후 기본 vhost의 잔여 큐가 없음을 확인하고 테스트 컨테이너를
125
+ `docker stop defstream-rabbitmq`로 종료했다. 컨테이너는 재사용할 수 있도록 보존한다.
126
+ 다시 사용할 때는 `docker start defstream-rabbitmq` 후 준비 상태를 확인한다.
127
+
128
+ ## 6. 호환성 안내
129
+
130
+ 기존처럼 브로커 인스턴스를 `Worker`에 전달하면 된다.
131
+ `WorkerConnection`을 직접 import하거나 타입 힌트에 쓰던 코드는
132
+ `from DefStream.Broker import Connection`으로 변경해야 한다.
133
+ 컨슈머 없는 연결은 워커 생성 시점이 아니라 실행 중 컨슈머 생성 직전에 거부된다.
@@ -0,0 +1,47 @@
1
+ # 워커에 공통 브로커 연결 타입 사용
2
+
3
+ ## 달라진 점
4
+
5
+ 이전에는 워커에 전달할 연결의 타입을 표시하려면 워커 전용 `WorkerConnection`을
6
+ 사용해야 했습니다. 이제 `DefStream.Broker.Connection`을 사용하면 됩니다.
7
+ 워커를 위해 별도의 연결 타입을 가져오거나 정의할 필요가 없습니다.
8
+
9
+ RabbitMQ·Redis 연결 객체를 `Worker`에 전달하는 기존 코드는 그대로 사용할 수 있습니다.
10
+ 메시지 처리, ACK/NACK, 실패 정책, RPC와 스레드·프로세스 풀의 동작도 동일합니다.
11
+
12
+ ## 기존 코드에서 바꿀 부분
13
+
14
+ `WorkerConnection`을 직접 가져오거나 타입 힌트에 사용했다면 공통 `Connection`으로
15
+ 변경하세요. `WorkerConnection`은 더 이상 제공하지 않습니다.
16
+
17
+ 변경 전:
18
+
19
+ ```python
20
+ from DefStream.Worker import WorkerConnection
21
+ ```
22
+
23
+ 변경 후:
24
+
25
+ ```python
26
+ from DefStream.Broker import Connection
27
+ ```
28
+
29
+ 해당 타입을 사용한 매개변수·반환값·변수의 타입 힌트도 `Connection`으로 바꾸면 됩니다.
30
+ 브로커 객체만 전달하고 있었다면 수정할 필요가 없습니다.
31
+
32
+ ## 직접 만든 브로커를 사용하는 경우
33
+
34
+ 워커에서 사용할 연결에는 `connect()`, `close()` 외에 컨슈머를 만드는 `consumer()`가
35
+ 있어야 합니다. 호출자가 별도로 검사하거나 워커 전용 타입을 상속할 필요는 없습니다.
36
+
37
+ 워커가 연결과 setup을 마친 뒤 `consumer()` 지원 여부를 확인합니다.
38
+ 지원하지 않으면 `TypeError: Worker broker must support consumer creation`으로
39
+ 실패하고 종료 처리를 수행합니다. 메시지 발행만 가능한 연결은 워커에 사용할 수 없습니다.
40
+
41
+ 공통 `Connection` 자체에는 `consumer()` 구현을 강제하지 않으므로,
42
+ 워커에 사용하지 않는 발행 전용 연결은 변경할 필요가 없습니다.
43
+
44
+ ## 상세 기록
45
+
46
+ - [요구사항 명세서](./REQUIREMENTS.md)
47
+ - [구현 및 RabbitMQ 검증 기록](./IMPLEMENTS.md)
@@ -0,0 +1,52 @@
1
+ # 워커 브로커 연결 타입 단순화 요구사항 명세서
2
+
3
+ ## 1. 목적
4
+
5
+ 사용자가 워커 전용 연결 타입을 알아야 하는 부담을 없앤다.
6
+ `Worker`는 공통 브로커의 `Connection`을 받고, 컨슈머 생성 기능의 지원 여부는
7
+ 워커 내부에서 확인한다.
8
+
9
+ 대상 브랜치는 `fix/3`이며, 구현 내용과 검증 결과는
10
+ [구현 기록](./IMPLEMENTS.md), 변경 요약은 [변경 로그](./README.md)에 기록한다.
11
+
12
+ ## 2. 변경 전과 변경 후
13
+
14
+ | 구분 | 변경 전 | 변경 후 |
15
+ |---|---|---|
16
+ | 생성자 입력 타입 | 워커 전용 `WorkerConnection` | 공통 `DefStream.Broker.Connection` |
17
+ | 컨슈머 생성 계약 | `WorkerConnection`에서 두 Protocol 합성 | 독립적인 `GetConsumer` 유지 |
18
+ | 기능 지원 검사 | 워커의 명시적인 런타임 검사 없음 | 컨슈머 생성 직전 `isinstance` 검사 |
19
+ | 미지원 연결 | 컨슈머 호출 단계에서 기능 부재 오류 | 원인을 설명하는 `TypeError` |
20
+
21
+ ## 3. 기능 요구사항
22
+
23
+ | ID | 요구사항 |
24
+ |---|---|
25
+ | WCON-01 | `Worker.__init__`의 `broker` 타입은 공통 `Connection`이어야 한다. |
26
+ | WCON-02 | `WorkerConnection` 정의와 공개 export를 제거한다. 같은 목적의 워커 전용 합성 타입을 새로 추가하지 않는다. |
27
+ | WCON-03 | `Connection`은 `connect()`와 `close()`만 요구하며, `GetConsumer` 상속을 강제하지 않는다. |
28
+ | WCON-04 | `GetConsumer`를 런타임 검사 가능한 Protocol로 제공한다. 명시적 상속 없이 필요한 기능을 구현한 객체도 지원한다. |
29
+ | WCON-05 | 워커는 연결 및 setup 완료 후, 컨슈머 생성 직전에 `isinstance(self.broker, GetConsumer)`를 검사한다. 호출자에게 별도 검사나 타입 변환을 요구하지 않는다. |
30
+ | WCON-06 | 미지원 연결은 `TypeError('Worker broker must support consumer creation')`로 실패한다. 실패를 무시하거나 대체 컨슈머를 생성하지 않는다. |
31
+ | WCON-07 | 검사 실패도 기존 종료 경로를 거쳐 실행 풀 종료, cleanup, 연결 종료와 실행 완료 상태 처리를 수행한다. |
32
+ | WCON-08 | 기존 RabbitMQ·Redis 브로커, 작업 등록, 메시지 처리, ACK/NACK, 실패 정책, RPC와 실행 풀 동작을 유지한다. |
33
+
34
+ ## 4. 범위와 제한
35
+
36
+ - 변경 대상은 코어의 연결 계약, 워커, 직접 관련된 테스트와 문서이다.
37
+ - RabbitMQ·Redis 어댑터의 공개 API나 구현은 변경하지 않는다.
38
+ - 사용자 정의 연결에 `GetConsumer`의 명시적 상속을 강제하지 않는다.
39
+ - 런타임 Protocol 검사는 기능의 구조적 지원 여부를 확인한다.
40
+ 메서드 매개변수·반환 타입이나 실제 실행 성공까지 검증하는 기능은 아니다.
41
+ - 브랜치 생성, 커밋, 배포, 버전 번호 변경은 본 작업에 포함하지 않는다.
42
+
43
+ ## 5. 인수 기준
44
+
45
+ | ID | 검증 시나리오 |
46
+ |---|---|
47
+ | WCAT-01 | 생성자 타입 힌트가 공통 `Connection`이며 공개 export에 `WorkerConnection`이 없다. |
48
+ | WCAT-02 | 명시적 Protocol 상속이 없는 구조적 연결과 RabbitMQ·Redis 연결을 워커에 그대로 전달할 수 있다. |
49
+ | WCAT-03 | `connect()`·`close()`만 구현한 연결은 인스턴스화할 수 있지만, 작업을 등록하고 워커를 실행하면 명시적인 `TypeError`로 실패한다. 실행 풀은 종료되고 완료 상태가 설정된다. |
50
+ | WCAT-04 | 기존 워커 수명 주기, setup·cleanup, 스레드·프로세스 실행 테스트가 통과한다. |
51
+ | WCAT-05 | Docker의 `defstream-rabbitmq`를 사용하여 실제 소비·재전달·거부·종료, RPC, Inspector 연동을 검증한다. RabbitMQ 테스트를 건너뛴 결과를 실서버 검증으로 간주하지 않는다. |
52
+ | WCAT-06 | 설치된 과거 패키지가 아니라 현재 브랜치의 코어 소스와 로컬 RabbitMQ 어댑터를 로드해 검증한다. |
@@ -60,6 +60,7 @@ class GetStream(Protocol):
60
60
  ) -> Sendable: ...
61
61
 
62
62
 
63
+ @runtime_checkable
63
64
  class GetConsumer(Protocol):
64
65
  @abstractmethod
65
66
  def consumer(
@@ -14,7 +14,6 @@ from typing import (
14
14
  Type,
15
15
  Tuple,
16
16
  Union,
17
- Protocol,
18
17
  )
19
18
 
20
19
  from ..Broker import (
@@ -36,7 +35,6 @@ from .Startup import startup
36
35
 
37
36
  __all__ = (
38
37
  'Worker',
39
- 'WorkerConnection',
40
38
  'WorkerSetup',
41
39
  'WorkerCleanup',
42
40
  )
@@ -44,14 +42,6 @@ __all__ = (
44
42
  logger = getLogger(__name__)
45
43
 
46
44
 
47
- class WorkerConnection(
48
- Connection,
49
- GetConsumer,
50
- Protocol,
51
- ):
52
- pass
53
-
54
-
55
45
  class WorkerSetup(ABC):
56
46
  @abstractmethod
57
47
  def __call__(
@@ -85,7 +75,7 @@ def worker_hooks(
85
75
  class Worker:
86
76
  def __init__(
87
77
  self,
88
- broker: WorkerConnection,
78
+ broker: Connection,
89
79
  pool: Pool,
90
80
  setups: Optional[Union[WorkerSetup, Sequence[WorkerSetup]]] = None,
91
81
  cleanups: Optional[Union[WorkerCleanup, Sequence[WorkerCleanup]]] = None,
@@ -128,6 +118,8 @@ class Worker:
128
118
  self.broker.connect()
129
119
  for setup in self.setups:
130
120
  setup(self)
121
+ if not isinstance(self.broker, GetConsumer):
122
+ raise TypeError('Worker broker must support consumer creation')
131
123
  self.consumer = self.broker.consumer()
132
124
  for run in self.tasks:
133
125
  self.consumer.subs(
@@ -1,6 +1,5 @@
1
1
  from .Worker import (
2
2
  Worker,
3
- WorkerConnection,
4
3
  WorkerSetup,
5
4
  WorkerCleanup,
6
5
  )
@@ -31,7 +30,6 @@ __all__ = (
31
30
  'declare',
32
31
  'send',
33
32
  'request',
34
- 'WorkerConnection',
35
33
  'WorkerSetup',
36
34
  'WorkerCleanup',
37
35
  'Run',
@@ -231,8 +231,7 @@ class InspectorTests(TestCase):
231
231
  ),
232
232
  )
233
233
  consumer = Consumer()
234
- broker = Mock()
235
- broker.consumer.return_value = consumer
234
+ broker = Mock(consumer=Mock(return_value=consumer))
236
235
  worker = Worker(
237
236
  broker,
238
237
  ThreadPool(2),
@@ -360,7 +360,7 @@ class WorkerLifecycleTests(TestCase):
360
360
  def run(self):
361
361
  pass
362
362
 
363
- broker = Mock()
363
+ broker = Mock(consumer=Mock())
364
364
  pool = Mock()
365
365
  worker = Worker(
366
366
  broker,
@@ -1,6 +1,7 @@
1
1
  from inspect import isabstract
2
2
  from typing import (
3
3
  Optional,
4
+ get_type_hints,
4
5
  )
5
6
  from unittest import (
6
7
  TestCase,
@@ -45,13 +46,11 @@ from DefStream.Broker.Policies import (
45
46
  Reject,
46
47
  Requeue,
47
48
  )
48
- from DefStream.Worker import WorkerConnection
49
-
50
-
51
- def worker_connection(
52
- connection: WorkerConnection,
53
- ) -> WorkerConnection:
54
- return connection
49
+ from DefStream.Worker import (
50
+ Run,
51
+ Worker,
52
+ )
53
+ from DefStream.Worker import __all__ as worker_exports
55
54
 
56
55
 
57
56
  def queue_provider(
@@ -109,7 +108,7 @@ class ProtocolContractTests(TestCase):
109
108
  Handler()(event)
110
109
  event.ack.assert_called_once()
111
110
 
112
- def test_protocols_are_independent_and_abstract(self):
111
+ def test_protocols_are_abstract(self):
113
112
  self.assertNotIn(
114
113
  'Broker',
115
114
  connection_exports,
@@ -120,7 +119,6 @@ class ProtocolContractTests(TestCase):
120
119
  GetQueue,
121
120
  GetStream,
122
121
  GetConsumer,
123
- WorkerConnection,
124
122
  ):
125
123
  with self.subTest(protocol=protocol):
126
124
  self.assertTrue(protocol._is_protocol)
@@ -186,6 +184,14 @@ class ProtocolContractTests(TestCase):
186
184
  ))
187
185
 
188
186
  def test_worker_accepts_minimal_structural_and_concrete_connections(self):
187
+ self.assertIs(
188
+ get_type_hints(Worker.__init__)['broker'],
189
+ Connection,
190
+ )
191
+ self.assertNotIn(
192
+ 'WorkerConnection',
193
+ worker_exports,
194
+ )
189
195
  connections = (
190
196
  StructuralConnection(),
191
197
  RabbitConnection(RabbitConfiguration(
@@ -195,15 +201,59 @@ class ProtocolContractTests(TestCase):
195
201
  RedisConnection(RedisConfiguration()),
196
202
  )
197
203
  for connection in connections:
204
+ broker: Connection = connection
205
+ worker = Worker(
206
+ broker,
207
+ Mock(),
208
+ )
198
209
  self.assertIs(
199
- worker_connection(connection),
210
+ worker.broker,
200
211
  connection,
201
212
  )
213
+ self.assertIsInstance(
214
+ connection,
215
+ GetConsumer,
216
+ )
202
217
  self.assertIs(
203
218
  queue_provider(RedisConnection(RedisConfiguration())).__class__,
204
219
  RedisConnection,
205
220
  )
206
221
 
222
+ def test_worker_rejects_connection_without_consumer_creation(self):
223
+ class ProducerConnection(Connection):
224
+ def connect(self) -> None:
225
+ return
226
+
227
+ def close(self) -> None:
228
+ return
229
+
230
+ class Task(
231
+ Run,
232
+ parameters={'queue': 'protocol-test'},
233
+ ):
234
+ def run(self):
235
+ return
236
+
237
+ connection = ProducerConnection()
238
+ self.assertNotIsInstance(
239
+ connection,
240
+ GetConsumer,
241
+ )
242
+ pool = Mock()
243
+ worker = Worker(
244
+ connection,
245
+ pool,
246
+ )
247
+ worker.add(Task)
248
+ with self.assertRaisesRegex(
249
+ TypeError,
250
+ 'Worker broker must support consumer creation',
251
+ ):
252
+ worker.run()
253
+ pool.stop.assert_called_once_with()
254
+ self.assertTrue(worker.finished.is_set())
255
+ return
256
+
207
257
  def test_existing_event_codec_and_policy_protocols_are_explicit(self):
208
258
  for implementation, contract in (
209
259
  (
@@ -136,8 +136,7 @@ class HookTests(TestCase):
136
136
  with self.subTest(pool=pool_cls, failure=failure), TemporaryDirectory() as directory:
137
137
  path = str(Path(directory) / 'trace')
138
138
  consumer = FakeConsumer()
139
- broker = Mock()
140
- broker.consumer.return_value = consumer
139
+ broker = Mock(consumer=Mock(return_value=consumer))
141
140
  worker = Worker(broker, pool_cls(1))
142
141
  worker.add(HookRunner)
143
142
  delivery = Mock()
@@ -256,8 +256,7 @@ class PoolTests(TestCase):
256
256
  class WorkerTests(TestCase):
257
257
  def make_worker(self, runner, pool):
258
258
  consumer = FakeConsumer()
259
- broker = Mock()
260
- broker.consumer.return_value = consumer
259
+ broker = Mock(consumer=Mock(return_value=consumer))
261
260
  worker = Worker(broker, pool)
262
261
  worker.add(runner)
263
262
  return worker, consumer, broker
@@ -43,7 +43,7 @@ class StartupTests(TestCase):
43
43
  pool = Mock()
44
44
  pool.size.return_value = 4
45
45
  worker = Worker(
46
- Mock(),
46
+ Mock(consumer=Mock()),
47
47
  pool,
48
48
  )
49
49
  worker.add(Task)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes