DefStream 0.1.3.7__tar.gz → 0.1.4.12__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 (89) hide show
  1. {defstream-0.1.3.7 → defstream-0.1.4.12}/.github/workflows/ci.yml +111 -5
  2. {defstream-0.1.3.7 → defstream-0.1.4.12}/DefStream.egg-info/PKG-INFO +66 -1
  3. {defstream-0.1.3.7 → defstream-0.1.4.12}/DefStream.egg-info/SOURCES.txt +7 -0
  4. {defstream-0.1.3.7 → defstream-0.1.4.12}/PKG-INFO +66 -1
  5. {defstream-0.1.3.7 → defstream-0.1.4.12}/README.en.md +67 -0
  6. {defstream-0.1.3.7 → defstream-0.1.4.12}/README.md +65 -0
  7. defstream-0.1.4.12/issues/feature/1/IMPLEMENTS.md +73 -0
  8. defstream-0.1.4.12/issues/feature/1/README.md +74 -0
  9. defstream-0.1.4.12/issues/feature/1/REQUIREMENTS.md +134 -0
  10. defstream-0.1.4.12/issues/feature/1/TASK.md +40 -0
  11. defstream-0.1.4.12/issues/feature/1/ci.refs.json +4 -0
  12. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/Connection.py +28 -0
  13. defstream-0.1.4.12/src/Broker/Priority.py +26 -0
  14. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/__init__.py +10 -0
  15. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Worker/Message.py +78 -2
  16. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Worker/Run.py +51 -0
  17. defstream-0.1.4.12/test/test_task_priorities.py +396 -0
  18. {defstream-0.1.3.7 → defstream-0.1.4.12}/.gitignore +0 -0
  19. {defstream-0.1.3.7 → defstream-0.1.4.12}/DefStream.egg-info/dependency_links.txt +0 -0
  20. {defstream-0.1.3.7 → defstream-0.1.4.12}/DefStream.egg-info/requires.txt +0 -0
  21. {defstream-0.1.3.7 → defstream-0.1.4.12}/DefStream.egg-info/top_level.txt +0 -0
  22. {defstream-0.1.3.7 → defstream-0.1.4.12}/LICENSE +0 -0
  23. {defstream-0.1.3.7 → defstream-0.1.4.12}/sample/Broker/Sample.py +0 -0
  24. {defstream-0.1.3.7 → defstream-0.1.4.12}/sample/Worker/RabbitMQ/Client.py +0 -0
  25. {defstream-0.1.3.7 → defstream-0.1.4.12}/sample/Worker/RabbitMQ/Tasks.py +0 -0
  26. {defstream-0.1.3.7 → defstream-0.1.4.12}/sample/Worker/RabbitMQ/Worker.py +0 -0
  27. {defstream-0.1.3.7 → defstream-0.1.4.12}/sample/Worker/Redis/Sample.py +0 -0
  28. {defstream-0.1.3.7 → defstream-0.1.4.12}/sample/Worker/Sample.py +0 -0
  29. {defstream-0.1.3.7 → defstream-0.1.4.12}/setup.cfg +0 -0
  30. {defstream-0.1.3.7 → defstream-0.1.4.12}/setup.py +0 -0
  31. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/Configuration.py +0 -0
  32. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/Consumer.py +0 -0
  33. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/Errors.py +0 -0
  34. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/Event.py +0 -0
  35. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/Implements/__init__.py +0 -0
  36. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/Policies.py +0 -0
  37. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Broker/Protocol.py +0 -0
  38. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Descriptor/DataObject.py +0 -0
  39. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Descriptor/Types.py +0 -0
  40. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Descriptor/Value.py +0 -0
  41. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Descriptor/__init__.py +0 -0
  42. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extension.py +0 -0
  43. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extensions/Console.html +0 -0
  44. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extensions/Console.py +0 -0
  45. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extensions/Dashboard.html +0 -0
  46. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extensions/Dashboard.py +0 -0
  47. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extensions/HealthCheck.py +0 -0
  48. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extensions/Metrics.py +0 -0
  49. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extensions/Specifications.py +0 -0
  50. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Extensions/__init__.py +0 -0
  51. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Inspector.py +0 -0
  52. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/Response.py +0 -0
  53. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Inspector/__init__.py +0 -0
  54. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Pattern.py +0 -0
  55. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/Array.py +0 -0
  56. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/Common.py +0 -0
  57. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/Compare.py +0 -0
  58. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/Condition.py +0 -0
  59. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/DataObject.py +0 -0
  60. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/DateTime.py +0 -0
  61. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/Number.py +0 -0
  62. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/Object.py +0 -0
  63. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/Size.py +0 -0
  64. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/String.py +0 -0
  65. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/Type.py +0 -0
  66. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Patterns/__init__.py +0 -0
  67. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Validate.py +0 -0
  68. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/Validator.py +0 -0
  69. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Validator/__init__.py +0 -0
  70. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Worker/Executor.py +0 -0
  71. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Worker/Pool.py +0 -0
  72. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Worker/Specification.py +0 -0
  73. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Worker/Startup.py +0 -0
  74. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Worker/Worker.py +0 -0
  75. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/Worker/__init__.py +0 -0
  76. {defstream-0.1.3.7 → defstream-0.1.4.12}/src/__init__.py +0 -0
  77. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_dashboard.py +0 -0
  78. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_dashboard_ui.cjs +0 -0
  79. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_inspector.py +0 -0
  80. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_inspector_console.py +0 -0
  81. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_lifecycle_hooks.py +0 -0
  82. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_pool_callbacks.py +0 -0
  83. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_process_signals.py +0 -0
  84. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_protocol_contracts.py +0 -0
  85. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_runner_hooks.py +0 -0
  86. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_specifications.py +0 -0
  87. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_task_messages.py +0 -0
  88. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_worker.py +0 -0
  89. {defstream-0.1.3.7 → defstream-0.1.4.12}/test/test_worker_startup.py +0 -0
@@ -19,11 +19,17 @@ concurrency:
19
19
  jobs:
20
20
  changes:
21
21
  name: Plan release version
22
+ permissions:
23
+ contents: read
24
+ pull-requests: read
22
25
  runs-on: ubuntu-latest
23
26
  timeout-minutes: 5
24
27
  outputs:
25
28
  release: ${{ steps.changes.outputs.release }}
26
29
  version: ${{ steps.changes.outputs.version }}
30
+ core_ref: ${{ steps.refs.outputs.core_ref }}
31
+ rabbitmq_ref: ${{ steps.refs.outputs.rabbitmq_ref }}
32
+ redis_ref: ${{ steps.refs.outputs.redis_ref }}
27
33
  steps:
28
34
  - uses: actions/checkout@v7
29
35
  with:
@@ -33,6 +39,96 @@ jobs:
33
39
  uses: actions/setup-python@v7
34
40
  with:
35
41
  python-version: '3.13'
42
+ - name: Validate PR branch changelog
43
+ id: changelog
44
+ env:
45
+ GH_TOKEN: ${{ github.token }}
46
+ run: |
47
+ python - <<'PY'
48
+ from json import loads
49
+ from os import environ
50
+ from pathlib import Path
51
+ from subprocess import check_output
52
+
53
+ head = environ['GITHUB_SHA']
54
+ if environ['GITHUB_EVENT_NAME'] == 'pull_request':
55
+ event = loads(Path(environ['GITHUB_EVENT_PATH']).read_text(encoding='utf-8'))
56
+ branch = event['pull_request']['head']['ref']
57
+ else:
58
+ repository = environ['GITHUB_REPOSITORY']
59
+ pages = loads(check_output([
60
+ 'gh', 'api', '--paginate', '--slurp',
61
+ f'repos/{repository}/commits/{head}/pulls?per_page=100',
62
+ ]))
63
+ pulls = [
64
+ pull for page in pages for pull in page
65
+ if pull['merged_at'] is not None
66
+ and pull['merge_commit_sha'] == head
67
+ and pull['base']['ref'] == 'master'
68
+ and pull['base']['repo']['full_name'] == repository
69
+ ]
70
+ if len(pulls) != 1:
71
+ raise ValueError('Expected exactly one merged PR for the release commit')
72
+ branch = pulls[0]['head']['ref']
73
+ path = f'issues/{branch}/README.md'
74
+ entry = check_output(['git', 'ls-tree', head, '--', path]).decode().split()
75
+ if not entry or entry[0] not in ('100644', '100755'):
76
+ raise ValueError(f'Changelog must exist as a regular file: {path}')
77
+ body = check_output(['git', 'show', f'{head}:{path}'])
78
+ if not body.decode('utf-8').strip():
79
+ raise ValueError(f'Customer changelog is empty: {path}')
80
+ notes = Path(environ['RUNNER_TEMP'], 'release-notes', 'README.md')
81
+ notes.parent.mkdir(parents=True, exist_ok=True)
82
+ notes.write_bytes(body)
83
+ print(f'Customer changelog: {path}')
84
+ with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
85
+ output.write(f'branch={branch}\n')
86
+ PY
87
+ - name: Resolve dependency refs
88
+ id: refs
89
+ env:
90
+ BRANCH: ${{ steps.changelog.outputs.branch }}
91
+ run: |
92
+ python - <<'PY'
93
+ from json import loads
94
+ from os import environ
95
+ from pathlib import Path
96
+ from subprocess import check_output, run
97
+
98
+ head = environ['GITHUB_SHA']
99
+ path = f'issues/{environ["BRANCH"]}/ci.refs.json'
100
+ entry = check_output(['git', 'ls-tree', head, '--', path]).decode().split()
101
+ refs = {}
102
+ if entry:
103
+ if entry[0] not in ('100644', '100755'):
104
+ raise ValueError(f'Dependency refs must be a regular file: {path}')
105
+ refs = loads(check_output(['git', 'show', f'{head}:{path}']).decode('utf-8'))
106
+ else:
107
+ print(f'{path} not found: dependency refs default to master')
108
+ projects = {
109
+ 'DefStream': 'core_ref',
110
+ 'DefStream-RabbitMQ': 'rabbitmq_ref',
111
+ 'DefStream-Redis': 'redis_ref',
112
+ }
113
+ if not isinstance(refs, dict) or refs.keys() - projects.keys():
114
+ raise ValueError(f'Expected an object with only supported project names: {path}')
115
+ with Path(environ['GITHUB_OUTPUT']).open('a', encoding='utf-8') as output:
116
+ for project, key in projects.items():
117
+ ref = refs.get(project, 'master')
118
+ if not isinstance(ref, str) or not ref or ref.startswith('-'):
119
+ raise ValueError(f'Invalid dependency ref for {project}: {ref!r}')
120
+ run(['git', 'check-ref-format', '--allow-onelevel', ref], check=True)
121
+ output.write(f'{key}={ref}\n')
122
+ print(f'{project} dependency ref: {ref}')
123
+ PY
124
+ - name: Preserve customer release notes
125
+ uses: actions/upload-artifact@v7
126
+ with:
127
+ name: release-notes
128
+ path: ${{ runner.temp }}/release-notes/README.md
129
+ if-no-files-found: error
130
+ retention-days: 14
131
+ overwrite: true
36
132
  - name: Check complete push range and plan version
37
133
  id: changes
38
134
  env:
@@ -132,7 +228,7 @@ jobs:
132
228
  uses: actions/checkout@v7
133
229
  with:
134
230
  repository: yong5eon/DefStream
135
- ref: ${{ github.event.repository.name == 'DefStream' && github.sha || 'master' }}
231
+ ref: ${{ github.event.repository.name == 'DefStream' && github.sha || needs.changes.outputs.core_ref }}
136
232
  path: packages/DefStream
137
233
  fetch-depth: 0
138
234
  persist-credentials: false
@@ -141,7 +237,7 @@ jobs:
141
237
  uses: actions/checkout@v7
142
238
  with:
143
239
  repository: yong5eon/DefStream-RabbitMQ
144
- ref: ${{ github.event.repository.name == 'DefStream-RabbitMQ' && github.sha || 'master' }}
240
+ ref: ${{ github.event.repository.name == 'DefStream-RabbitMQ' && github.sha || needs.changes.outputs.rabbitmq_ref }}
145
241
  path: packages/DefStream-RabbitMQ
146
242
  fetch-depth: 0
147
243
  persist-credentials: false
@@ -150,7 +246,7 @@ jobs:
150
246
  uses: actions/checkout@v7
151
247
  with:
152
248
  repository: yong5eon/DefStream-Redis
153
- ref: ${{ github.event.repository.name == 'DefStream-Redis' && github.sha || 'master' }}
249
+ ref: ${{ github.event.repository.name == 'DefStream-Redis' && github.sha || needs.changes.outputs.redis_ref }}
154
250
  path: packages/DefStream-Redis
155
251
  fetch-depth: 0
156
252
  persist-credentials: false
@@ -267,6 +363,12 @@ jobs:
267
363
  name: distributions
268
364
  path: dist
269
365
 
366
+ - name: Download customer release notes
367
+ uses: actions/download-artifact@v8
368
+ with:
369
+ name: release-notes
370
+ path: release-notes
371
+
270
372
  - name: Set up Python
271
373
  uses: actions/setup-python@v7
272
374
  with:
@@ -371,6 +473,9 @@ jobs:
371
473
 
372
474
  repository = environ['GITHUB_REPOSITORY']
373
475
  version = environ['VERSION']
476
+ notes = Path('release-notes/README.md')
477
+ if not notes.read_text(encoding='utf-8').strip():
478
+ raise ValueError('Customer release notes are empty')
374
479
  request = Request(
375
480
  f'{environ["GITHUB_API_URL"]}/repos/{repository}/releases/tags/{quote(version, safe="")}',
376
481
  headers={
@@ -387,7 +492,7 @@ jobs:
387
492
  raise
388
493
  run([
389
494
  'gh', 'release', 'create', version, '--repo', repository,
390
- '--verify-tag', '--title', version, '--generate-notes', '--draft',
495
+ '--verify-tag', '--title', version, '--notes-file', str(notes), '--draft',
391
496
  ], check=True)
392
497
  else:
393
498
  if release['tag_name'] != version:
@@ -398,6 +503,7 @@ jobs:
398
503
  '--clobber', *artifacts,
399
504
  ], check=True)
400
505
  run([
401
- 'gh', 'release', 'edit', version, '--repo', repository, '--draft=false',
506
+ 'gh', 'release', 'edit', version, '--repo', repository,
507
+ '--notes-file', str(notes), '--draft=false',
402
508
  ], check=True)
403
509
  PY
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DefStream
3
- Version: 0.1.3.7
3
+ Version: 0.1.4.12
4
4
  Summary: Synchronous message processing with pluggable broker adapters
5
5
  Home-page: https://github.com/yong5eon/DefStream
6
6
  Author: Heo Yongseon
@@ -232,6 +232,71 @@ python client.py
232
232
  하나의 Worker에서는 같은 큐에 여러 작업을 중복 등록할 수 없습니다.
233
233
  메시지 유형을 보고 여러 `Run` 중 하나를 고르는 방식이 아니라 큐별로 작업을 선택합니다.
234
234
 
235
+ ### 작업 우선순위
236
+
237
+ RabbitMQ classic 큐에서는 작업 `parameters`의 RabbitMQ `Subscription`에
238
+ `priority`로 기본 메시지 우선순위, `max_priority`로 큐 최대값을 함께 정의합니다.
239
+ `Run`에 별도의 `priority` 설정을 두지 않습니다.
240
+ `declare()`가 이 설정을 읽어 우선순위 큐를 준비합니다.
241
+
242
+ ```python
243
+ from DefStream.Broker.Implements.RabbitMQ import Subscription
244
+ from DefStream.Worker import (
245
+ Run,
246
+ declare,
247
+ request,
248
+ send,
249
+ )
250
+
251
+
252
+ class Add(
253
+ Run,
254
+ parameters=Subscription(
255
+ queue='defstream.example.priority.add',
256
+ max_priority=5,
257
+ priority=2,
258
+ ),
259
+ ):
260
+ def run(
261
+ self,
262
+ a: int,
263
+ b: int,
264
+ ):
265
+ return a + b
266
+
267
+
268
+ # broker는 호출자가 연결한 RabbitMQ Connection입니다.
269
+ declare(
270
+ task=Add,
271
+ broker=broker,
272
+ )
273
+ send(
274
+ {'a': 10, 'b': 20},
275
+ task=Add,
276
+ broker=broker,
277
+ )
278
+ # RPC에는 별도로 실행 중인 Worker가 필요합니다.
279
+ result = request(
280
+ {'a': 10, 'b': 20},
281
+ task=Add,
282
+ broker=broker,
283
+ priority=5,
284
+ )
285
+ ```
286
+
287
+ - 호출의 `priority`가 생략되거나 `None`이면 작업 기본값을 사용합니다.
288
+ 명시적 `0`은 가장 낮은 우선순위이며 기본값을 덮어씁니다.
289
+ - 작업 기본값만 지정하면 큐 최대값은 RabbitMQ 상한 `255`입니다.
290
+ 최대값만 지정하면 기본값은 `None`이며, 호출값도 없으면 메시지 속성을 생략합니다.
291
+ - 작업 최대값을 초과하는 값, 음수, 정수가 아닌 값과 `bool`은 거부합니다.
292
+ - 작업 설정 없이 호출에서만 지정하면 브로커 범위 `0..255`만 검증합니다.
293
+ 우선순위 큐의 사전 선언과 실제 큐 최대값 준수는 호출자 책임입니다.
294
+ - 자식 작업은 설정을 상속하고 유효한 값으로 재정의할 수 있지만 해제할 수는 없습니다.
295
+ - Redis에 우선순위를 요청하면 `NotImplementedError`가 발생합니다.
296
+ 우선순위 미지정 시 기존 Redis 동작은 유지합니다.
297
+ - 우선순위는 같은 큐의 대기 메시지 전달에 적용됩니다. 이미 전달·실행 중인
298
+ 작업을 선점하거나 여러 작업의 실행·완료 순서를 보장하지 않습니다.
299
+
235
300
  ### 성공, 실패와 재시도
236
301
 
237
302
  - 작업 실행과 완료 훅이 성공한 뒤 ACK합니다. RabbitMQ RPC는 응답 발행도 ACK보다 먼저 수행합니다.
@@ -9,6 +9,11 @@ DefStream.egg-info/SOURCES.txt
9
9
  DefStream.egg-info/dependency_links.txt
10
10
  DefStream.egg-info/requires.txt
11
11
  DefStream.egg-info/top_level.txt
12
+ issues/feature/1/IMPLEMENTS.md
13
+ issues/feature/1/README.md
14
+ issues/feature/1/REQUIREMENTS.md
15
+ issues/feature/1/TASK.md
16
+ issues/feature/1/ci.refs.json
12
17
  sample/Broker/Sample.py
13
18
  sample/Worker/Sample.py
14
19
  sample/Worker/RabbitMQ/Client.py
@@ -22,6 +27,7 @@ src/Broker/Consumer.py
22
27
  src/Broker/Errors.py
23
28
  src/Broker/Event.py
24
29
  src/Broker/Policies.py
30
+ src/Broker/Priority.py
25
31
  src/Broker/Protocol.py
26
32
  src/Broker/__init__.py
27
33
  src/Broker/Implements/__init__.py
@@ -76,5 +82,6 @@ test/test_protocol_contracts.py
76
82
  test/test_runner_hooks.py
77
83
  test/test_specifications.py
78
84
  test/test_task_messages.py
85
+ test/test_task_priorities.py
79
86
  test/test_worker.py
80
87
  test/test_worker_startup.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DefStream
3
- Version: 0.1.3.7
3
+ Version: 0.1.4.12
4
4
  Summary: Synchronous message processing with pluggable broker adapters
5
5
  Home-page: https://github.com/yong5eon/DefStream
6
6
  Author: Heo Yongseon
@@ -232,6 +232,71 @@ python client.py
232
232
  하나의 Worker에서는 같은 큐에 여러 작업을 중복 등록할 수 없습니다.
233
233
  메시지 유형을 보고 여러 `Run` 중 하나를 고르는 방식이 아니라 큐별로 작업을 선택합니다.
234
234
 
235
+ ### 작업 우선순위
236
+
237
+ RabbitMQ classic 큐에서는 작업 `parameters`의 RabbitMQ `Subscription`에
238
+ `priority`로 기본 메시지 우선순위, `max_priority`로 큐 최대값을 함께 정의합니다.
239
+ `Run`에 별도의 `priority` 설정을 두지 않습니다.
240
+ `declare()`가 이 설정을 읽어 우선순위 큐를 준비합니다.
241
+
242
+ ```python
243
+ from DefStream.Broker.Implements.RabbitMQ import Subscription
244
+ from DefStream.Worker import (
245
+ Run,
246
+ declare,
247
+ request,
248
+ send,
249
+ )
250
+
251
+
252
+ class Add(
253
+ Run,
254
+ parameters=Subscription(
255
+ queue='defstream.example.priority.add',
256
+ max_priority=5,
257
+ priority=2,
258
+ ),
259
+ ):
260
+ def run(
261
+ self,
262
+ a: int,
263
+ b: int,
264
+ ):
265
+ return a + b
266
+
267
+
268
+ # broker는 호출자가 연결한 RabbitMQ Connection입니다.
269
+ declare(
270
+ task=Add,
271
+ broker=broker,
272
+ )
273
+ send(
274
+ {'a': 10, 'b': 20},
275
+ task=Add,
276
+ broker=broker,
277
+ )
278
+ # RPC에는 별도로 실행 중인 Worker가 필요합니다.
279
+ result = request(
280
+ {'a': 10, 'b': 20},
281
+ task=Add,
282
+ broker=broker,
283
+ priority=5,
284
+ )
285
+ ```
286
+
287
+ - 호출의 `priority`가 생략되거나 `None`이면 작업 기본값을 사용합니다.
288
+ 명시적 `0`은 가장 낮은 우선순위이며 기본값을 덮어씁니다.
289
+ - 작업 기본값만 지정하면 큐 최대값은 RabbitMQ 상한 `255`입니다.
290
+ 최대값만 지정하면 기본값은 `None`이며, 호출값도 없으면 메시지 속성을 생략합니다.
291
+ - 작업 최대값을 초과하는 값, 음수, 정수가 아닌 값과 `bool`은 거부합니다.
292
+ - 작업 설정 없이 호출에서만 지정하면 브로커 범위 `0..255`만 검증합니다.
293
+ 우선순위 큐의 사전 선언과 실제 큐 최대값 준수는 호출자 책임입니다.
294
+ - 자식 작업은 설정을 상속하고 유효한 값으로 재정의할 수 있지만 해제할 수는 없습니다.
295
+ - Redis에 우선순위를 요청하면 `NotImplementedError`가 발생합니다.
296
+ 우선순위 미지정 시 기존 Redis 동작은 유지합니다.
297
+ - 우선순위는 같은 큐의 대기 메시지 전달에 적용됩니다. 이미 전달·실행 중인
298
+ 작업을 선점하거나 여러 작업의 실행·완료 순서를 보장하지 않습니다.
299
+
235
300
  ### 성공, 실패와 재시도
236
301
 
237
302
  - 작업 실행과 완료 훅이 성공한 뒤 ACK합니다. RabbitMQ RPC는 응답 발행도 ACK보다 먼저 수행합니다.
@@ -202,6 +202,73 @@ Task bodies must be **mappings with string keys**. Return values must be
202
202
  serializable by the adapter. A Worker cannot register multiple tasks for the
203
203
  same queue: routing selects tasks by queue, not by a type field in the message.
204
204
 
205
+ ### Task priorities
206
+
207
+ For RabbitMQ classic queues, define both the default message `priority` and queue
208
+ `max_priority` in the RabbitMQ `Subscription` supplied as the task's `parameters`.
209
+ There is no separate `priority` setting on `Run`.
210
+ `declare()` reads these settings to provision the priority queue.
211
+
212
+ ```python
213
+ from DefStream.Broker.Implements.RabbitMQ import Subscription
214
+ from DefStream.Worker import (
215
+ Run,
216
+ declare,
217
+ request,
218
+ send,
219
+ )
220
+
221
+
222
+ class Add(
223
+ Run,
224
+ parameters=Subscription(
225
+ queue='defstream.example.priority.add',
226
+ max_priority=5,
227
+ priority=2,
228
+ ),
229
+ ):
230
+ def run(
231
+ self,
232
+ a: int,
233
+ b: int,
234
+ ):
235
+ return a + b
236
+
237
+
238
+ # broker is a RabbitMQ Connection connected by the caller.
239
+ declare(
240
+ task=Add,
241
+ broker=broker,
242
+ )
243
+ send(
244
+ {'a': 10, 'b': 20},
245
+ task=Add,
246
+ broker=broker,
247
+ )
248
+ # RPC requires a separately running Worker.
249
+ result = request(
250
+ {'a': 10, 'b': 20},
251
+ task=Add,
252
+ broker=broker,
253
+ priority=5,
254
+ )
255
+ ```
256
+
257
+ - Omitting the call's `priority` or passing `None` uses the task default.
258
+ An explicit `0` overrides the default with the lowest priority.
259
+ - Setting only the task default sets the queue maximum to RabbitMQ's limit of
260
+ `255`. Setting only the maximum leaves the default as `None`; without a
261
+ per-call value either, the message priority property is omitted.
262
+ - Values above the task maximum, negative values, non-integers, and `bool` are rejected.
263
+ - Specifying priority only at call time validates only the broker range `0..255`.
264
+ The caller must provision a priority queue and respect its actual maximum.
265
+ - Child tasks inherit settings and may override them with valid values, but cannot
266
+ clear them.
267
+ - Requesting priorities with Redis raises `NotImplementedError`.
268
+ Calls without priorities retain existing Redis behavior.
269
+ - Priority affects delivery of waiting messages within the same queue. It does not
270
+ preempt delivered or running tasks or guarantee execution and completion order.
271
+
205
272
  ### Success, failures, and retries
206
273
 
207
274
  - ACK follows successful execution and completion hooks. RabbitMQ RPC also publishes the reply before ACK.
@@ -199,6 +199,71 @@ python client.py
199
199
  하나의 Worker에서는 같은 큐에 여러 작업을 중복 등록할 수 없습니다.
200
200
  메시지 유형을 보고 여러 `Run` 중 하나를 고르는 방식이 아니라 큐별로 작업을 선택합니다.
201
201
 
202
+ ### 작업 우선순위
203
+
204
+ RabbitMQ classic 큐에서는 작업 `parameters`의 RabbitMQ `Subscription`에
205
+ `priority`로 기본 메시지 우선순위, `max_priority`로 큐 최대값을 함께 정의합니다.
206
+ `Run`에 별도의 `priority` 설정을 두지 않습니다.
207
+ `declare()`가 이 설정을 읽어 우선순위 큐를 준비합니다.
208
+
209
+ ```python
210
+ from DefStream.Broker.Implements.RabbitMQ import Subscription
211
+ from DefStream.Worker import (
212
+ Run,
213
+ declare,
214
+ request,
215
+ send,
216
+ )
217
+
218
+
219
+ class Add(
220
+ Run,
221
+ parameters=Subscription(
222
+ queue='defstream.example.priority.add',
223
+ max_priority=5,
224
+ priority=2,
225
+ ),
226
+ ):
227
+ def run(
228
+ self,
229
+ a: int,
230
+ b: int,
231
+ ):
232
+ return a + b
233
+
234
+
235
+ # broker는 호출자가 연결한 RabbitMQ Connection입니다.
236
+ declare(
237
+ task=Add,
238
+ broker=broker,
239
+ )
240
+ send(
241
+ {'a': 10, 'b': 20},
242
+ task=Add,
243
+ broker=broker,
244
+ )
245
+ # RPC에는 별도로 실행 중인 Worker가 필요합니다.
246
+ result = request(
247
+ {'a': 10, 'b': 20},
248
+ task=Add,
249
+ broker=broker,
250
+ priority=5,
251
+ )
252
+ ```
253
+
254
+ - 호출의 `priority`가 생략되거나 `None`이면 작업 기본값을 사용합니다.
255
+ 명시적 `0`은 가장 낮은 우선순위이며 기본값을 덮어씁니다.
256
+ - 작업 기본값만 지정하면 큐 최대값은 RabbitMQ 상한 `255`입니다.
257
+ 최대값만 지정하면 기본값은 `None`이며, 호출값도 없으면 메시지 속성을 생략합니다.
258
+ - 작업 최대값을 초과하는 값, 음수, 정수가 아닌 값과 `bool`은 거부합니다.
259
+ - 작업 설정 없이 호출에서만 지정하면 브로커 범위 `0..255`만 검증합니다.
260
+ 우선순위 큐의 사전 선언과 실제 큐 최대값 준수는 호출자 책임입니다.
261
+ - 자식 작업은 설정을 상속하고 유효한 값으로 재정의할 수 있지만 해제할 수는 없습니다.
262
+ - Redis에 우선순위를 요청하면 `NotImplementedError`가 발생합니다.
263
+ 우선순위 미지정 시 기존 Redis 동작은 유지합니다.
264
+ - 우선순위는 같은 큐의 대기 메시지 전달에 적용됩니다. 이미 전달·실행 중인
265
+ 작업을 선점하거나 여러 작업의 실행·완료 순서를 보장하지 않습니다.
266
+
202
267
  ### 성공, 실패와 재시도
203
268
 
204
269
  - 작업 실행과 완료 훅이 성공한 뒤 ACK합니다. RabbitMQ RPC는 응답 발행도 ACK보다 먼저 수행합니다.
@@ -0,0 +1,73 @@
1
+ # 워커 작업 우선순위 구현 기록
2
+
3
+ ## 1. 구현 범위
4
+
5
+ [요구사항](./REQUIREMENTS.md)에 따라 작업 정의, 선언, 전송, RPC 요청에 우선순위를 연결했다.
6
+ Redis 구현, 실행 풀 스케줄링, 작업 선점은 변경하지 않았다.
7
+
8
+ ## 2. 주요 변경
9
+
10
+ - [Run](../../../src/Worker/Run.py)은 기존 `parameters`의 구독 설정에서
11
+ `priority`와 `max_priority`를 읽는다. 별도의 `Run.priority` 속성이나
12
+ 클래스 선언 키워드는 제공하지 않는다. 구독 객체와 매핑을
13
+ 모두 지원하며, 기본값과 최대값의 타입·하한 및 상호 범위를 검증한다.
14
+ - 자식 작업은 부모 구독 설정을 상속한다. 구독 설정을 재정의할 때 상속된 기본값이나
15
+ 최대값을 제거하면 오류로 거부한다. 변경하려면 유지할 값도 새 구독 설정에 명시한다.
16
+ - [메시지 API](../../../src/Worker/Message.py)의 `send`, `request`에 키워드 전용
17
+ `priority`를 추가했다. 호출값이 `None`이면 작업 기본값을 사용하며 `0`을 보존한다.
18
+ - [PrioritySupport](../../../src/Broker/Connection.py) 계약으로 지원 브로커의
19
+ 구독 정규화와 우선순위 발행을 구분한다. 기존 브로커 계약은 유지한다.
20
+ - 우선순위를 전혀 사용하지 않는 경로는 기존 인자 형태로 호출한다.
21
+ 미지원 브로커에 우선순위를 요청하면 선언·발행 전에 `NotImplementedError`를 발생시킨다.
22
+ - [공유 검증 함수](../../../src/Broker/Priority.py)를 작업 및 RabbitMQ 경로에서
23
+ 재사용한다. 정수가 아닌 값과 `bool`은 `TypeError`, 범위 오류는 `ValueError`로 처리한다.
24
+
25
+ ## 3. 브로커 연계
26
+
27
+ - RabbitMQ `Subscription` 생성·파싱 시 기본값만 있으면 최대값을 `255`로
28
+ 정규화한다. 원본 매핑은 변경하지 않으며 기존 구독 객체는 그대로 재사용한다.
29
+ - 작업 최대값이 있으면 이를 우선 사용한다. 호출에서만 지정하면 브로커 허용 범위만
30
+ 검증하고 큐 선언·변경·조회는 수행하지 않는다.
31
+ - 작업 본문, 스키마, ACK 및 실행 풀의 처리 흐름은 변경하지 않았다.
32
+
33
+ ## 4. 검증 결과
34
+
35
+ - 가상 환경의 Python과 기존 `unittest` 러너로 전체 코어 테스트 137개를 실행했다.
36
+ 129개 통과, 선택적 통합 테스트 등 8개 건너뜀.
37
+ - 새 우선순위 테스트 7개로 기본값, 최대값, `0`, 호출 재정의, RPC 전달,
38
+ Redis 거부·기존 전송, 상속 및 잘못된 설정을 검증했다.
39
+ - Windows 기본 CP949 출력에서는 기존 시작 배너가 인코딩 오류를 발생시켜,
40
+ `PYTHONIOENCODING=utf-8`, `PYTHONUTF8=1`로 실행하여 통과했다.
41
+ 관련 없는 시작 배너 코드는 수정하지 않았다.
42
+ - 변경한 코어 소스의 Pylance 진단에 오류가 없음을 확인했다.
43
+ - Redis 회귀 테스트도 14개 중 10개 통과, 선택적 통합 테스트 4개 건너뜀으로 확인했다.
44
+ - 테스트 도구에서 테스트를 발견하지 못하여 저장소의 `unittest discover`로 검증했다.
45
+ - 후속 Docker RabbitMQ 3.13.7 검증에서 어댑터 전체 35개 테스트가 모두 통과했다.
46
+ 코어 메시지·우선순위 테스트도 실제 RabbitMQ 옵션을 활성화하여 18개 중
47
+ 16개 통과, Redis 전용 통합 테스트 2개 건너뜀을 확인했다.
48
+ - 기본값을 `Subscription.priority`로 통합한 뒤 전체 코어 테스트를 재실행했다.
49
+ RabbitMQ 실서버 옵션을 활성화하여 137개 중 133개 통과, 4개 건너뜀을 확인했다.
50
+ Redis 회귀 테스트는 14개 중 10개 통과, 4개 건너뜀이다.
51
+ - 코어의 수정된 소스는 Pylance 오류가 없었다. RabbitMQ 원본 경로는 기존 상대
52
+ import 해석 문제 등이 남아 있어 전체 정적 분석 통과로 간주하지 않는다.
53
+
54
+ 실행 명령은 작업 폴더 루트 기준이다.
55
+
56
+ ```powershell
57
+ $env:PYTHONIOENCODING = 'utf-8'
58
+ $env:PYTHONUTF8 = '1'
59
+ .\.venv\Scripts\python.exe -m unittest discover -s .\DefStream\test
60
+ ```
61
+
62
+ ## 5. 공통 브로커 참조 경로 통일
63
+
64
+ - 구현체가 공통 인터페이스와 요소를 `from DefStream.Broker import ...`로 참조하도록 공개 경로를 통일했다.
65
+ - 공통 패키지에서 `Requeue`, `Reject`, `validate_priority`를 추가로 공개했다.
66
+ - RabbitMQ와 Redis 구현체의 상위 상대 import를 제거했다. 구현체 내부 import는 유지했다.
67
+ - RabbitMQ 실서버 테스트 38개 통과, Redis 테스트 10개 통과·4개 건너뜀,
68
+ 코어 메시지 테스트 16개 통과·2개 건너뜀을 확인했다.
69
+
70
+ ## 6. 관련 문서
71
+
72
+ - [사용 설명서](../../../README.md)
73
+ - [RabbitMQ 구현 기록](../../../../DefStream-RabbitMQ/issues/feature/1/IMPLEMENTS.md)
@@ -0,0 +1,74 @@
1
+ # 워커 작업 우선순위 지원
2
+
3
+ ## 변경 전 (AS-IS)
4
+
5
+ 작업을 대상으로 하는 `declare`, `send`, `request`에서는 큐의 우선순위 설정과
6
+ 메시지 우선순위를 지정할 수 없었습니다.
7
+
8
+ ## 변경 후 (TO-BE)
9
+
10
+ 지원 브로커의 작업 구독 설정에 기본·최대 우선순위를 정의하고, `send`와 `request`에서
11
+ 메시지별 우선순위를 재정의할 수 있습니다. 현재 지원 브로커는 RabbitMQ입니다.
12
+
13
+ ## 사용 방법
14
+
15
+ RabbitMQ 작업의 `parameters`에 `Subscription(priority=2, max_priority=5, ...)`를
16
+ 지정하세요. 기본값은 `Run`의 별도 속성이 아니라 구독 설정에 정의합니다.
17
+ 호출자가 연결한 `broker`와 구체적인 작업 클래스 `Add`를 사용해 다음처럼 호출합니다.
18
+
19
+ ```python
20
+ from DefStream.Worker import (
21
+ declare,
22
+ request,
23
+ send,
24
+ )
25
+
26
+
27
+ # 작업 구독 설정을 읽어 큐를 준비합니다.
28
+ declare(
29
+ task=Add,
30
+ broker=broker,
31
+ )
32
+
33
+ # 호출값을 생략하면 작업 구독의 기본 우선순위를 사용합니다.
34
+ send(
35
+ {'a': 1, 'b': 2},
36
+ task=Add,
37
+ broker=broker,
38
+ )
39
+
40
+ # RPC 요청도 호출별 우선순위를 지정할 수 있습니다.
41
+ result = request(
42
+ {'a': 1, 'b': 2},
43
+ task=Add,
44
+ broker=broker,
45
+ priority=5,
46
+ )
47
+ ```
48
+
49
+ RPC 결과를 받으려면 해당 작업을 처리하는 Worker가 실행 중이어야 합니다.
50
+ 전체 작업 정의와 큐 전환 방법은
51
+ [RabbitMQ 우선순위 사용 안내](https://github.com/yong5eon/DefStream-RabbitMQ/blob/master/issues/feature/1/README.md)를 참고하세요.
52
+
53
+ ## 기존 코드에 미치는 영향
54
+
55
+ - 우선순위를 지정하지 않은 기존 호출은 그대로 사용할 수 있습니다.
56
+ - 호출값 생략 또는 `None`은 작업 기본값을 사용하며, `0`은 명시적인 재정의입니다.
57
+ - 기본값만 지정하면 RabbitMQ 큐 최대값은 `255`입니다. 최대값만 지정하고
58
+ 호출값도 없으면 메시지 우선순위 속성을 생략합니다.
59
+ - 잘못된 타입이나 범위는 선언·발행 전에 오류로 처리합니다.
60
+ 작업 설정 없이 호출에서만 지정하는 경우에는 브로커 허용 범위만 검증하므로,
61
+ 실제 우선순위 큐의 사전 선언은 호출자가 보장해야 합니다.
62
+ - 자식 작업에서 구독 설정을 교체한다면 부모의 기본·최대 우선순위도 새 설정에
63
+ 명시해야 합니다. 유효한 값으로 재정의할 수 있지만 해제할 수는 없습니다.
64
+ - Redis는 우선순위를 지원하지 않습니다. Redis에 우선순위를 요청하면
65
+ `NotImplementedError`가 발생하며, 미지정 시에는 기존 동작을 유지합니다.
66
+ - 우선순위는 같은 큐의 대기 메시지 전달에만 적용되며, 실행 중 작업의 선점이나
67
+ 전역 실행·완료 순서를 보장하지 않습니다.
68
+
69
+ ## 브로커 확장 개발자를 위한 공개 요소
70
+
71
+ 공통 패키지에서 기존 인터페이스와 함께 `PrioritySupport`, `Requeue`, `Reject`,
72
+ `validate_priority`를 가져올 수 있습니다. 브로커 구현 시 공통 요소는
73
+ `from DefStream.Broker import ...`로 참조하세요.
74
+ 기존 우선순위 미지원 브로커에 새 인자 구현을 강제하지 않습니다.