feat(mid-section-defect): harden with distributed lock, rate limit, filter separation, abort, SQL classification and tests
Address 6 code review findings (P0-P3): add Redis distributed lock to prevent duplicate Oracle pipeline on cold cache, apply rate limiting to 3 high-cost routes, separate UI filter state from committed query state, add AbortController for request cancellation, push workcenter group classification into Oracle SQL CASE WHEN, and add 18 route+service tests. Also add workcenter group selection to job-query equipment selector and rename button to "查詢". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,3 +31,75 @@ Boolean query parsing in routes SHALL use shared helper behavior.
|
||||
- **WHEN** routes parse common boolean query parameters
|
||||
- **THEN** parsing behavior MUST be consistent across routes via shared utility
|
||||
|
||||
### Requirement: Mid-section defect analysis endpoints SHALL apply distributed lock to prevent duplicate pipeline execution
|
||||
The `/api/mid-section-defect/analysis` pipeline SHALL use a Redis distributed lock to prevent concurrent identical queries from executing the full Oracle pipeline in parallel.
|
||||
|
||||
#### Scenario: Two parallel requests with cold cache
|
||||
- **WHEN** two requests with identical parameters arrive simultaneously and no cache exists
|
||||
- **THEN** the first request SHALL acquire the lock and execute the full pipeline
|
||||
- **THEN** the second request SHALL wait by polling the cache until the first request completes
|
||||
- **THEN** only ONE full Oracle pipeline execution SHALL occur
|
||||
|
||||
#### Scenario: Lock wait timeout
|
||||
- **WHEN** a waiting request does not see a cache result within 90 seconds
|
||||
- **THEN** the request SHALL proceed with its own pipeline execution (fail-open)
|
||||
|
||||
#### Scenario: Redis unavailable
|
||||
- **WHEN** Redis is unavailable during lock acquisition
|
||||
- **THEN** the lock function SHALL return acquired=true (fail-open)
|
||||
- **THEN** the request SHALL proceed normally without blocking
|
||||
|
||||
#### Scenario: Pipeline exception with lock held
|
||||
- **WHEN** the pipeline throws an exception while the lock is held
|
||||
- **THEN** the lock SHALL be released in a finally block
|
||||
- **THEN** subsequent requests SHALL NOT be blocked by a stale lock
|
||||
|
||||
### Requirement: Mid-section defect routes SHALL apply rate limiting
|
||||
The `/analysis`, `/analysis/detail`, and `/export` endpoints SHALL apply per-client rate limiting using the existing `configured_rate_limit` mechanism.
|
||||
|
||||
#### Scenario: Analysis endpoint rate limit exceeded
|
||||
- **WHEN** a client sends more than 6 requests to `/api/mid-section-defect/analysis` within 60 seconds
|
||||
- **THEN** the endpoint SHALL return HTTP 429 with a `Retry-After` header
|
||||
- **THEN** the service function SHALL NOT be called
|
||||
|
||||
#### Scenario: Detail endpoint rate limit exceeded
|
||||
- **WHEN** a client sends more than 15 requests to `/api/mid-section-defect/analysis/detail` within 60 seconds
|
||||
- **THEN** the endpoint SHALL return HTTP 429 with a `Retry-After` header
|
||||
|
||||
#### Scenario: Export endpoint rate limit exceeded
|
||||
- **WHEN** a client sends more than 3 requests to `/api/mid-section-defect/export` within 60 seconds
|
||||
- **THEN** the endpoint SHALL return HTTP 429 with a `Retry-After` header
|
||||
|
||||
#### Scenario: Loss reasons endpoint not rate limited
|
||||
- **WHEN** a client sends requests to `/api/mid-section-defect/loss-reasons`
|
||||
- **THEN** no rate limiting SHALL be applied (endpoint is lightweight with 24h cache)
|
||||
|
||||
### Requirement: Mid-section defect upstream history SHALL classify workcenters in SQL
|
||||
The upstream history SQL query SHALL classify `WORKCENTERNAME` into workcenter groups using Oracle `CASE WHEN` expressions, returning the full production line history without excluding any stations.
|
||||
|
||||
#### Scenario: Workcenter group classification in SQL
|
||||
- **WHEN** the upstream history query executes
|
||||
- **THEN** each row SHALL include a `WORKCENTER_GROUP` column derived from `CASE WHEN` pattern matching
|
||||
- **THEN** the classification SHALL match the patterns defined in `workcenter_groups.py`
|
||||
|
||||
#### Scenario: Unknown workcenter name
|
||||
- **WHEN** a `WORKCENTERNAME` does not match any known pattern
|
||||
- **THEN** `WORKCENTER_GROUP` SHALL be NULL
|
||||
- **THEN** the row SHALL still be included in the result (not filtered out)
|
||||
|
||||
#### Scenario: Full production line retention
|
||||
- **WHEN** the upstream history is fetched for ancestor CIDs
|
||||
- **THEN** ALL stations SHALL be included (cutting, welding, mid-section, testing)
|
||||
- **THEN** no order-based filtering SHALL be applied
|
||||
|
||||
### Requirement: Mid-section defect routes and service SHALL have test coverage
|
||||
Route and service test files SHALL exist and cover core behaviors.
|
||||
|
||||
#### Scenario: Route tests exist
|
||||
- **WHEN** pytest discovers tests
|
||||
- **THEN** `tests/test_mid_section_defect_routes.py` SHALL contain tests for success, parameter validation (400), service failure (500), and rate limiting (429)
|
||||
|
||||
#### Scenario: Service tests exist
|
||||
- **WHEN** pytest discovers tests
|
||||
- **THEN** `tests/test_mid_section_defect_service.py` SHALL contain tests for date validation, pagination logic, and loss reasons caching
|
||||
|
||||
|
||||
@@ -87,3 +87,39 @@ Pages that require server-side parameter validation before serving SHALL validat
|
||||
- **WHEN** the pure Vite hold-detail page loads
|
||||
- **THEN** the page SHALL read `reason` from URL parameters
|
||||
- **THEN** if `reason` is empty or missing, the page SHALL redirect to `/wip-overview`
|
||||
|
||||
### Requirement: Mid-section defect page SHALL separate filter state from query state
|
||||
The mid-section defect page SHALL maintain separate reactive state for UI input (`filters`) and committed query parameters (`committedFilters`).
|
||||
|
||||
#### Scenario: User changes date without clicking query
|
||||
- **WHEN** user modifies the date range in the filter bar but does not click "查詢"
|
||||
- **THEN** auto-refresh, pagination, and CSV export SHALL continue using the previously committed filter values
|
||||
- **THEN** the new date range SHALL NOT affect any API calls until "查詢" is clicked
|
||||
|
||||
#### Scenario: User clicks query button
|
||||
- **WHEN** user clicks "查詢"
|
||||
- **THEN** the current `filters` state SHALL be snapshotted into `committedFilters`
|
||||
- **THEN** all subsequent API calls SHALL use the committed values
|
||||
|
||||
#### Scenario: CSV export uses committed filters
|
||||
- **WHEN** user clicks "匯出 CSV" after modifying filters without re-querying
|
||||
- **THEN** the export SHALL use the committed filter values from the last query
|
||||
- **THEN** the export SHALL NOT use the current UI filter values
|
||||
|
||||
### Requirement: Mid-section defect page SHALL cancel in-flight requests on new query
|
||||
The mid-section defect page SHALL use `AbortController` to cancel in-flight API requests when a new query is initiated.
|
||||
|
||||
#### Scenario: New query cancels previous query
|
||||
- **WHEN** user clicks "查詢" while a previous query is still in-flight
|
||||
- **THEN** the previous query's summary and detail requests SHALL be aborted
|
||||
- **THEN** the AbortError SHALL be handled silently (no error banner shown)
|
||||
|
||||
#### Scenario: Page navigation cancels previous detail request
|
||||
- **WHEN** user clicks next page while a previous page request is still in-flight
|
||||
- **THEN** the previous page request SHALL be aborted
|
||||
- **THEN** the new page request SHALL proceed independently
|
||||
|
||||
#### Scenario: Query and pagination use independent abort keys
|
||||
- **WHEN** a query is in-flight and user triggers pagination
|
||||
- **THEN** the query SHALL NOT be cancelled by the pagination request
|
||||
- **THEN** the pagination SHALL use a separate abort key from the query
|
||||
|
||||
Reference in New Issue
Block a user