Commit Graph

2148 Commits

Author SHA1 Message Date
Vickey 27bcf1383b fix: address code review — security, thread safety, performance, and null safety
Security (CRITICAL):
- KeyUtil: read JWT signing key from DC3_SECURITY_KEY env/property
- UserPasswordServiceImpl: read default password from DC3_SECURITY_DEFAULT_PASSWORD env
- KeyLoader: read OPC-UA keystore password from OPCUA_KEYSTORE_PASSWORD env
- WebFilterConfig: return 401 on malformed X-Auth-User header
- AlgorithmConstant: document deprecated hardcoded constants

Thread safety (HIGH):
- Fix TOCTOU races in 4 driver connectors (computeIfAbsent)
- Fix PlcS7 lock leak, OpcUa connect timeout (5s)
- CoapClientManager: synchronize setURI to prevent URI race
- PointValueJob, MqttScheduleJob: add @DisallowConcurrentExecution
- SystemHealthServiceImpl: preserve interrupt flag
- WindowSampleBuffer: local AtomicInteger → int

Performance (MEDIUM):
- EntityStateExpiryScanner: batch alarm saves (saveBatch)
- ImportDeviceServiceImpl: batch config saves
- ResourceRegistrySyncServiceImpl: batch-load nodes, eliminate N+1 COUNTs
- DriverSenderServiceImpl: debug-gate hot-path logging
- PointServiceImpl: stream().count() → size()

Null safety (LOW):
- RegexUtil: null guards on isName/isPhone/isMail/isPassword/isHost
- HostUtil: null-check getNetworkInterfaces() return
- TimeUtil: log parse failures instead of silent null

Infrastructure:
- Add spring-boot-starter-cache + @EnableCaching CacheConfig
- Replace embedded modbus4j/plc-s7 jars with external Maven dependencies
- Add dc3-driver-modbus-rtu module
2026-05-26 08:40:25 +08:00
Vickey 918b3d5e8e feat: update pom version to 2026.5.22 2026-05-26 00:30:11 +08:00
Vickey d5b6ea6f79 feat: format & style v2026.5.25 2026-05-25 21:53:50 +08:00
Vickey dedb03fe74 feat: add command/event attribute configuration system
Introduce CommandAttribute/EventAttribute with config variants:
- REST controllers, MyBatis mappers, services, and DAL managers
- gRPC client/server builders and proto message definitions
- DTOs, BOs, DOs, VOs for type-safe data transfer layer

fix: add explicit ::text casts in json_build_object parameters

PostgreSQL cannot infer parameter types inside json_build_object()
calls. Add ::text casts to #{stateExtType} and #{stateDescription}
to resolve "could not determine data type of parameter $10" errors.

refactor: restructure driver gRPC client and metadata layer

- Rename GrpcStubConfig to DriverClientStubConfig with dedicated DTOs
- Extract attribute builders from inline gRPC conversion logic
- Consolidate driver metadata, register service, and command handling
- Update device state heartbeat and command result event handling

docs: add multilingual architecture diagram and sync READMEs

- Translate architecture SVG into zh, ja, vi variants
- Update README architecture image references per language
- Add missing service-level shortcut sections to ja/vi READMEs
2026-05-25 00:00:00 +08:00
Vickey fd3af64634 feat: add layered architecture diagram and reposition project narrative
- Add IoT DC3 layered architecture diagram (SVG + HTML viewer)
  with Application / Management / Data / Driver layers,
  Security & Load and Platform & System sidebars,
  Data→Driver connection flows, and color-coded sections
- Reposition README descriptions across all languages (en, zh, ja, vi)
  and pom.xml to AI-ready narrative: "Connects devices, collects
  data organized for AI, and orchestrates the closed loop"
- Add README.ai.md with Chinese multi-paragraph AI narrative
  and closed-loop flowchart
- Optimize package.json metadata (name, license, scripts)
2026-05-24 13:35:57 +08:00
Vickey c40c3844bc docs: fix stale doc paths and version info in AI agent instructions 2026-05-24 10:30:26 +08:00
Vickey 8df45eac02 docs: remove AI commit identity section from AGENTS.md 2026-05-24 03:53:58 +08:00
Vickey 95a0f363e5 refactor: extract domain magic strings to constants
- Profile names: add 8 profile constants in EnvironmentConstant, update
  8 ActiveProfileConfig classes to reference them, and add missing
  dc3-common-constant dependency in 7 pom.xml files.
- Health checks: extract 6 health check map keys in SystemHealthServiceImpl,
  replace hardcoded "default" tenant with DefaultFlagEnum.
- Entity expiry: extract TICK_BODY constant, fix startup race by using
  @EventListener(ApplicationReadyEvent) instead of @PostConstruct.
- Proxy IP: extract PROXY_IP_HEADERS, UNKNOWN_IP, COMMA constants in
  RequestUtil to eliminate inline proxy header array and magic strings.
2026-05-24 03:53:51 +08:00
Vickey 0defdb5b61 refactor: extract driver protocol strings to config and constants
Replace hardcoded PROTOCOL constants in 8 driver CustomServiceImpl classes
with @Value("${dc3.driver.code}") for runtime configurability. Extract
transport-layer protocol strings (netty/tcp/udp) in 5 Netty handler/server
classes to PROTOCOL constants.
2026-05-24 03:53:18 +08:00
Vickey b5ab016cab chore: format MyBatis XML headers, SQL indentation and docs tables 2026-05-24 01:35:12 +08:00
Vickey 348267b14d fix: add missing tenantId ignore in MapStruct mapper builders 2026-05-24 01:35:07 +08:00
Vickey 4b9e3ad49e fix: resolve test compilation issues
- Fix ambiguous runExclusive mock by using typed ArgumentMatchers.<Supplier<String>>any()
- Fix setTimeoutUnit incorrect parameter in DeviceHealthScheduleJobTest
- Fix setProfileIds to setProfileId for 1:1 migration in DeviceToolTest
2026-05-24 01:35:03 +08:00
Vickey a88cb03aa1 style: format DTO builders, E2E teardown and Javadoc alignment 2026-05-24 01:34:57 +08:00
Vickey 4aed1dcee5 style: remove unused imports 2026-05-24 01:34:51 +08:00
Vickey 0db7bcd6cc feat: extend ModelConfig and ModelProvider BO/DO/VO with additional fields 2026-05-24 01:09:55 +08:00
pnoker 3b378e854b refactor: align data history models and constants 2026-05-24 01:07:02 +08:00
pnoker f0d0c7913c feat: add event alarm trigger, E2E contract tests, and docs
Add EVENT target type to AlarmTargetTypeFlagEnum, wire event report
into the alarm rule pipeline, add RabbitMQ contract E2E tests for
command call and event report, and create design documentation.
v2026.5.23
2026-05-23 19:16:01 +08:00
pnoker 5b24bc775b feat: add driver SDK custom command execution and event reporting
- Add DriverCommand interface with default execute() for custom commands
- Add CommandReceiver in driver SDK for receiving and dispatching commands
- Add commandResultSender and eventReportSender to DriverSenderService
- Add commandQueue and binding to DriverTopicConfig
- Add EventReportReceiver for async RabbitMQ event consumption
- Add report(EventReportDTO) overload to EventReportService
- Add dc3-common-facade-api dependency to driver module
- Fix sendDeviceStatus call missing 5th parameter
2026-05-23 18:46:51 +08:00
pnoker 150ba33244 feat: add custom command call API, event report API with RabbitMQ and gRPC
- Add dc3_command_record and dc3_event_record DTOs, DOs, mappers, managers
- Implement CommandRecordService: call flow with validation, RabbitMQ dispatch,
  result/dead receivers, full lifecycle (PENDING→SENT→SUCCESS/FAILED/TIMEOUT/DEAD)
- Implement EventReportService: report flow with validation and persistence
- Add REST controllers: /command_record (call/get/list), /event_report (report/get/list)
- Add RabbitMQ exchanges/queues/bindings for command dispatch, result, dead letter
- Add event exchange for driver→center event reporting
- Add gRPC CommandRecordApi and EventReportApi with proto definitions and servers
- Fix PointCommandValidator parameter type (String→PointExt)
2026-05-23 18:26:28 +08:00
Vickey aa5c4ddf8e feat: add Command and Event model with full CRUD stack and facade layer
Phase 2 — Command & Event 模型定义闭环:
- 5 new enums: CommandTypeFlag, CallTypeFlag, ParamDirectionFlag, EventTypeFlag, EventLevelFlag
- 4 Ext types: CommandExt, CommandParamExt, EventExt, EventParamExt
- 4 entity stacks (DO/BO/VO/Query/Builder): Command, CommandParam, Event, EventParam
- Mapper/XML with device JOIN support, Manager, Service with tenant validation
- 4 REST controllers, 2 gRPC servers, proto definitions (entity, query, page, service)

Phase 3 — Facade + Agentic 工具:
- Facade API: CommandFacade, EventFacade with tenant-scoped defaults
- gRPC Facade: CommandGrpcFacade, EventGrpcFacade with hand-rolled builders
- Local Facade: CommandLocalFacade, EventLocalFacade with MapStruct mappers
- GrpcStubConfig: register CommandApi and EventApi blocking stubs
- Agentic tools: CommandTool and EventTool (5 @Tool methods each)
2026-05-23 18:05:15 +08:00
Vickey e8b2eb908b docs: mark Phase 1 device-profile single-ownership tasks as done 2026-05-23 17:16:16 +08:00
Vickey 84a33daef4 docs: mark Phase 1 (Device↔Profile 1:1) as done 2026-05-23 15:12:52 +08:00
Vickey dd4f575bae refactor: convert Device↔Profile from M:N to 1:1 via device.profile_id
Replace dc3_profile_bind junction table with direct profile_id column on
dc3_device, simplifying the domain model. Device now owns a single
Profile instead of maintaining a many-to-many binding.

No data migration — seed DDL is updated separately.
2026-05-23 14:42:34 +08:00
Vickey cf2ffb0194 docs: update thing-model design with Phase 1 implementation plan 2026-05-23 08:20:48 +08:00
Vickey c6b70c1665 docs: update point-command.md status to reflect P0-P3 implementation
Correct stale [TODO] markers in §0, §1.3, §3, §4.1, §4.5, §4.7
to [DONE] matching the actual code delivered.
v2026.5.22
2026-05-22 23:49:10 +08:00
Vickey c556c6a0b8 feat: complete point command P0-P2 rebuild — DTO, lock, validation, API
P0: Replace content JSON-string with sealed PointCommandPayload + record DTOs.
P0: Accept write() boolean return, suppress echo on failure.
P1: Return commandId from POST endpoints, add RESTful GET /{commandId}.
P1: Add driver-online check via EntityStateMapper, PointCommandValidator.
P2: Add DeviceLockManager for per-device serialized command execution.
P3: Add POST /list API with pagination, commandId idempotency support.
2026-05-22 23:36:38 +08:00
Vickey 47760888bd feat: unify DeviceStatusEnum and DriverStatusEnum into EntityStatusEnum
- Replace two duplicate enums with single EntityStatusEnum (same index/code)
- Rename DB columns: state_flag→entity_state_flag, state_ext→entity_state_ext
- Add stateDescription field to DeviceStateDTO/DriverStateDTO for diagnostics
- Add description field to DeviceHealthState/DriverHealthState
- UPSERT SQL now writes stateDescription into entity_state_ext on heartbeat
- Remove EntityStateStatus interface (no longer needed with single enum)
- Update MQ layer, heartbeat services, status queries, gRPC, and all tests
2026-05-22 22:35:02 +08:00
pnoker 436b3b01bb feat: refine device and driver health state handling 2026-05-22 18:29:24 +08:00
pnoker 0471bd904e feat: add device health timeout state handling 2026-05-22 16:44:58 +08:00
pnoker 2e75061c70 style: clean up RabbitConstant, fix imports and update docs formatting 2026-05-22 09:16:27 +08:00
pnoker 6b4cefc74d feat(command): add DLX, result receipt, dedup cache, and validation
Phase 2-4 of the point command chain redesign (point-command.md):
- Add DLX (dead letter exchange/queue) for failed commands
- Add result exchange/queue and PointCommandResultReceiver
- Add PointCommandDeadReceiver for DLX message processing
- Add CommandDedupCache (Caffeine 5-min TTL) for driver-side idempotency
- Add PointCommandResultDTO for driver-to-center result receipts
- Persist commands (PENDING->SENT) with commandId/CorrelationData
- Enhance validateCommandScope: device/point enable flag, rwFlag for write
- Add GET /point_command/get_by_command_id query endpoint
- Update PointCommandDTO to carry commandId and tenantId
- Update PointCommandReceiver: dedup check, result sending, bounded retry
2026-05-22 08:56:59 +08:00
pnoker 10b09d2656 feat(data): add point command persistence model and status enums
Phase 2 of the point command chain redesign (point-command.md):
- Add PointCommandStatusEnum (PENDING/SENT/SUCCESS/FAILED/TIMEOUT/EXPIRED/DEAD)
- Add PointCommandSourceEnum (HTTP/GRPC/AGENTIC/SCHEDULED)
- Add PointCommandDO persistence object for dc3_point_command table
- Add PointCommandManager/Impl and PointCommandMapper (MyBatis-Plus)
2026-05-22 08:56:30 +08:00
pnoker 2617565e72 refactor(command): rename PointValueCommand/DeviceCommand to PointCommand
Phase 1 of the point command chain redesign (point-command.md):
- Delete dead code: DriverCommandDTO, DriverCommandTypeEnum
- Rename DeviceCommandDTO to PointCommandDTO (inner: DeviceRead->PointRead, DeviceWrite->PointWrite)
- Rename DeviceCommandTypeEnum to PointCommandTypeEnum
- Rename PointValueCommand* to PointCommand* (Controller, Service, ServiceImpl, Facade, VOs)
- Rename facade methods: dispatchRead->submitRead, dispatchWrite->submitWrite
- Rename HTTP path: /point_value_command -> /point_command
- Rename RabbitMQ exchange/queue/routing: dc3.e.command -> dc3.e.point_command
- Update all config, driver, agentic, and test files to new naming
- Update frontend API paths to /point_command/*
2026-05-22 08:55:51 +08:00
pnoker 8772149298 feat(data): implement RabbitMQ TTL+DLX timeout for driver and device state
Replace Caffeine LocalCacheService + @Scheduled scanner with
lease-based timeout using RabbitMQ TTL + DLX:
- Driver: each heartbeat publishes a 45s delayed check message;
  DriverTimeoutCheckReceiver performs secondary lease_version and
  expire_time verification before marking offline.
- Device: a self-sustaining 10s tick queue triggers batch scanning
  of expired device leases in EntityStateExpiryScanner.
- Remove OfflineExpiryListener and LocalCacheService from state chain.
- Heartbeat services now only write dc3_entity_state (source of truth).
2026-05-22 01:36:14 +08:00
pnoker 439330f3ff refactor(data): align entity state table with lease-based timeout design doc
Rename driver_id to parent_entity_id, ttl_seconds to timeout_seconds.
Add last_state_flag, last_heartbeat_time, last_alarm_id,
timeout_source_flag, and state_ext fields to EntityStateDO.
Add TimeoutSourceFlagEnum for timeout source classification.
2026-05-22 01:35:41 +08:00
pnoker 9007cbc22c style: apply consistent code formatting across all modules and docs 2026-05-22 00:24:22 +08:00
Vickey 0583fbee06 fix(data): harden entity state design and add test coverage
Design fixes:
- EntityStateExpiryScanner: use atomic lambdaUpdate with lease_version
  WHERE condition instead of read-then-write, preventing duplicate
  alarm writes across multiple Data Center instances.
- EntityStateExpiryScanner: add LIMIT 500 to scan query to bound
  memory usage after extended downtime.
- OfflineExpiryListener: atomically mark DB state as offline before
  writing alarm, preventing the scanner from writing a duplicate.
- EntityStateDO: fix Javadoc to reflect actual EntityTypeFlagEnum
  values (DRIVER=3, DEVICE=6).

Tests (32 total, all passing):
- EntityStateExpiryScannerTest: no expired rows, already-offline skip,
  online driver/device expiry with alarm, claim failure when another
  instance wins, multiple expired rows processed in order.
- DriverStateServiceImplTest: null guard, new DB row creation with
  correct fields, lease version increment, status flip alarm trigger.
- DeviceStateServiceImplTest: null guard, custom TTL, lease version
  increment, null driverId defaults to 0, status flip alarm.
- DriverStatusServiceImplTest: empty page, offline when DB missing,
  online from DB, offline when expired, device online/offline counts.
- DeviceStatusServiceImplTest: empty page, offline when DB missing,
  online from DB, offline when expired, profile query.
2026-05-22 00:11:11 +08:00
Vickey 04d0d5342f feat(data): add persistent state lease table for driver/device status
Replace local Caffeine cache as the source of truth for driver and
device online status with dc3_entity_state, a persistent state lease
table backed by PostgreSQL.

- Add EntityStateDO/Mapper/Manager persistence stack.
- Add EntityStateExpiryScanner that scans expire_time every 15 seconds
  with lease_version recheck to avoid stale alarm writes.
- Dual-write heartbeat path: DriverStateServiceImpl and
  DeviceStateServiceImpl now write dc3_entity_state first, then local
  cache.
- All status queries (controllers, gRPC server, local facade,
  SystemHealthServiceImpl) read from dc3_entity_state. Rows missing or
  past expire_time return offline.
- OfflineExpiryListener gains a DB dedup check to skip expiry events
  already handled by the scanner.
- Add @EnableScheduling to DataApplication.
- Add design doc docs/design/entity-state.md with full DDL.

This resolves restart state loss, multi-instance inconsistency, and
races between expiry checks and fresh heartbeats.
v2026.5.21
2026-05-21 23:57:45 +08:00
Vickey e85dd9ba7e feat(alarm): batch rule pipeline with processBatch dispatch 2026-05-21 23:26:44 +08:00
Vickey db14585cb8 refactor: replace manual constructors with @RequiredArgsConstructor
Replace hand-written all-assignment constructors with Lombok
@RequiredArgsConstructor across controllers, services, config classes,
event listeners, and tool classes. No behavioral change.
2026-05-21 23:18:08 +08:00
Vickey a0b392c3aa feat: rule alarm optimization 2026-05-21 21:39:13 +08:00
Vickey c7d927bec4 feat(alarm): hybrid window aggregator and evaluator
Wires the windowed evaluation path that the previous P4 commits set up.
With this commit AVG/MIN/MAX/SUM/COUNT/ALL/ANY rules actually run end-
to-end:

- ConditionEvaluator: utility extracted out of RuleEvaluatorImpl so
  both the LAST path and the windowed path apply identical operator
  semantics. evaluate(condition, value), recoveryConditionOf(...) and
  toBigDecimal(...) are the only public surfaces.

- WindowDataSource: small interface with aggregate() (scalar fold for
  AVG/MIN/MAX/SUM/COUNT) and samples() (raw rows for ALL/ANY).
  AggregateOutcome carries (value, sampleCount) so callers can enforce
  minSamples.

- LocalWindowDataSource: reads WindowSampleBuffer.snapshot(...) and
  folds in Java; AVG uses scale=6 / HALF_UP rounding to match
  PostgreSQL's AVG(numeric). Skips null numValue samples for numeric
  aggregates; COUNT counts every sample.

- RepositoryWindowDataSource: pushes the aggregate to the time-series
  store via RepositoryService.aggregateInWindow / samplesInWindow
  introduced in the previous commit. Only POINT facts are supported.

- HybridWindowDataSource (@Primary): routes by spec.duration —
  duration <= dc3.alarm.window.local-cutoff goes to local, larger
  goes to repository.

- WindowedRuleEvaluator: dispatch by mode. AVG/MIN/MAX/SUM/COUNT pull
  the scalar and feed it to ConditionEvaluator; ALL/ANY pull samples
  and apply the rule's per-sample condition. minSamples gates both
  paths. Recovery uses the synthesized condition (Recovery.operator
  + Recovery.threshold) over the same window, mirroring the LAST path.

- RuleEvaluatorImpl: rewritten to dispatch by parsed WindowSpec. LAST
  stays inline (no data-source plumbing needed); everything else
  delegates to the windowed evaluator. The previous warn-once gate
  for non-LAST modes is removed; invalid window specs (which should
  already be rejected by the save validator) are skipped with a
  single log line.

Tests:
- RuleEvaluatorImplTest is rewritten to pass a mock
  WindowedRuleEvaluator and verify dispatch.
- WindowedRuleEvaluatorTest covers AVG/COUNT/ALL/ANY/recovery paths
  + minSamples gate + LAST defensive guard.
- All 193 tests in dc3-common-data pass.
2026-05-21 21:30:07 +08:00
Vickey 3b01a16ad1 feat(repository): time-windowed point-value aggregation
Long-window alarm rules need a way to fold AVG/MIN/MAX/SUM/COUNT over
a time bracket of dc3_point_value, plus a raw-sample pull for ALL/ANY
where the rule condition has to run per row. The existing
RepositoryService had neither — listHistoryPointValue is count-bound
without a from/to, listPagePointValue only takes a lower bound.

Add two methods on RepositoryService:

- aggregateInWindow(WindowAggregateRequest) -> WindowAggregateResult
  pushes AVG/MIN/MAX/SUM/COUNT to PostgreSQL via a fixed-set <choose>
  in the new mapper XML so the function name can't be a SQL-injection
  vector. Numeric aggregates filter num_value IS NOT NULL to align
  with the partial index already in place; COUNT counts every row in
  the window.
- samplesInWindow(tenantId, deviceId, pointId, from, to)
  pulls (num_value, cal_value, create_time) ordered by create_time.
  Used by ALL/ANY long-window paths — slow by design, the doc warns
  against this combination.

Both live on PointValueMapper + PointValueMapper.xml so a future TSDB
adapter can swap the implementation without touching the alarm side.
PostgresRepositoryServiceImpl wires the calls through and guards
against null arguments. RepositoryStrategyFactoryTest's stub gets the
two new no-op methods so it still satisfies the interface.

Tests: PostgresRepositoryServiceImplWindowTest covers delegation +
null guards. The actual SQL is verified by inspection — adding a real
DAL slice (Testcontainers) for the alarm side is left for later.
2026-05-21 21:23:55 +08:00
Vickey c5fcc0b4fd feat(alarm): in-memory window sample buffer
Short windows (≤ dc3.alarm.window.local-cutoff, default PT5M) need a
side store the rule engine can read to fold AVG/MIN/MAX/SUM/COUNT/
ALL/ANY over recent samples. PointValueLocalCacheService is latest-
only and can't satisfy this, so introduce a dedicated buffer.

Shape:

- WindowSample (record): {numValue, calValue, timestamp}. numValue is
  null for non-numeric payloads — aggregators must filter null.
- WindowSampleKey (record): (tenantId, targetType, entityId). Per-
  entity rather than per-rule so multiple rules on the same entity
  share one time-ordered series.
- WindowSampleBuffer: Caffeine map of key -> ConcurrentLinkedDeque,
  trimmed inline on each append by both retention time
  (local-cutoff) and per-key sample count (max-samples-per-buffer,
  default 1000). Idle keys are evicted by Caffeine after
  buffer-idle-expiry (default 30min).
- AlarmWindowProperties (`@ConfigurationProperties("dc3.alarm.window")`):
  exposes localCutoff / maxBufferKeys / maxSamplesPerBuffer /
  bufferIdleExpiry.

AlarmRuleTriggerServiceImpl.processPointValue now appends to the
buffer before dispatching the fact, so when the engine eventually
reads a snapshot it sees the triggering sample inside the window.

Doesn't connect to the evaluator yet — that's P4-4. P4-3 lands the
long-window repository path next.
2026-05-21 21:10:59 +08:00
Vickey cd94180d2b feat(alarm): parse ISO-8601 window duration and accept all modes at save
Phase 4 lifts the LAST-only gate so users can save AVG/MIN/MAX/SUM/
COUNT/ALL/ANY rules ahead of the runtime evaluator landing. RuleExt's
window block already declares a mode + ISO-8601 duration + minSamples,
but nothing parsed those fields — save validation rejected anything
non-LAST and runtime ignored the duration entirely.

Add three pieces:

- WindowMode enum (dc3-common-model) — typed mode value with case-
  insensitive ofCode(), plus reducesToScalar() / requiresDuration()
  classifiers used by the future evaluator.
- WindowSpec record — parsed view {mode, duration, minSamples, valid,
  reason} with last() / ok() / invalid() factories.
- WindowSpecParser — turns RuleExt.Window into WindowSpec, applying
  the rules consistently (null window = LAST, blank mode = LAST,
  duration parse failure = invalid, non-positive duration on
  aggregation modes = invalid). Future runtime evaluator + this
  validator both call through the same parser.

RuleServiceImpl.validateWindowMode now routes through the parser:
unknown modes, malformed durations, and zero/negative durations on
aggregation modes get UnSupportException at save time. AVG with PT3M
is now accepted at save (the runtime path still no-ops for non-LAST
until the evaluator wiring lands).

Existing rejection tests flipped: AVG with PT3M is now accepted, the
rejection paths are exercised through unknown mode, zero duration,
and malformed duration. WindowSpecParserTest covers parser behavior
in isolation.
2026-05-21 21:06:27 +08:00
Vickey fa6243f011 feat(data): cache notify policy and message templates
RuleNotificationServiceImpl was issuing four DB lookups per alarm
fan-out: notify by id, message by id, enabled-bind list by tenant +
notify, and channel by id. Notify/message/channel/bind rows mutate at
configuration time and are read on every notification, so the read-amp
is significant under sustained alarm load.

Add NotifyConfigCache, a Caffeine-backed wrapper that owns:

- notify (NotifyBO by id)
- message (MessageBO by id)
- channel (NotifyChannelBO by id), with tenant scope enforced at the
  call site so cross-tenant lookups still return null
- bind list (List<NotifyChannelBindBO> by (tenantId, notifyId))

Invalidation:

- NotifyServiceImpl, MessageServiceImpl, NotifyChannelServiceImpl,
  NotifyChannelBindServiceImpl call the matching invalidate hook on
  add/update/delete.
- Bind updates invalidate both old and new (tenantId, notifyId) tuples
  in case the parent notify changes.

RuleNotificationServiceImpl is dramatically slimmed: the four
loadNotify/loadMessage/loadEnabledBinds/loadChannel helpers now route
through the cache, and the previously-injected manager/builder fields
that only existed for those calls are dropped.

Configurable via dc3.alarm.cache.notify.{max-size,ttl-seconds};
defaults hold 5000 entries for 60 seconds across each section.
2026-05-21 20:52:32 +08:00
pnoker 308df030c2 feat(data): batch alarm rule trigger entrypoint
PointValueServiceImpl.save(List) used to do
pointValueBOList.forEach(alarmRuleTriggerService::processPointValue),
forcing every batch caller to know that the trigger is per-fact. With
RuleRegistry now caching candidate rules, the per-fact dispatch is
already cheap, but the call site still leaks the loop.

Add AlarmRuleTriggerService.processPointValues(List<PointValueBO>) so
batch ingest hands off the whole list in one call. The default impl
fans out per-fact internally — rule semantics (firing, recovery, dedup)
are still defined sample-by-sample, so the engine itself stays unchanged.

Tests: PointValueServiceImplTest now verifies the bulk handoff
(times(1) processPointValues), and a new AlarmRuleTriggerServiceImplTest
covers the trigger's per-fact fan-out + invalid-entry filtering for
both the single and batch entrypoints.
2026-05-21 20:39:38 +08:00
pnoker 8404ad3623 feat(data): cache active rules in RuleRegistry
RuleEngineImpl previously hit dc3_rule on every fact via
RuleCandidateLookup + RuleBuilder.buildBOListByDOList. With per-second
point ingestion, that means database QPS = point upload rate.

Add RuleRegistry as a Caffeine-backed cache fronting both the lookup
and the BO conversion. Cache key is (tenantId, alarmTargetTypeFlag,
entityId), which is exactly the dimension RuleCandidateLookup already
filters on, so the cached list is reusable across repeated facts for
the same entity. Cached values are pre-built RuleBOs — no repeat
DO -> BO work either.

Invalidation:
- RuleServiceImpl.add/update/delete drops the per-tenant cache so the
  next evaluation reflects the change.
- A 60-second write TTL is a safety net for missed invalidations
  (e.g., a manual SQL update).

Configurable via dc3.alarm.cache.rule.{max-size,ttl-seconds}; defaults
hold 10000 keys for 60 seconds.
2026-05-21 20:14:40 +08:00
pnoker 2ea09124f7 feat(data): async notify dispatch via RabbitMQ worker
Outbound notification used to call adapter.send synchronously inside
the @Transactional rule pipeline, so a slow webhook or feishu adapter
would stretch the rule transaction and starve the rabbit consumers
that feed it. Move the dispatch onto its own queue:

- New dc3.q.notify.task queue bound to the alarm exchange with routing
  key dc3.r.notify.task.<channel-type>; 24h TTL on the queue caps any
  runaway backlog when an adapter is fully wedged.
- NotifyTaskDTO carries the rendered MessagePayload plus the PENDING
  notify_history row id so the worker can mutate it in place.
- NotifyTaskSender publishes tasks; RuleNotificationServiceImpl writes
  the PENDING row, advances last_notify_time for rate-limit debouncing,
  and hands the task to the sender — no blocking I/O remains in the
  notification entrypoint.
- NotifyWorker consumes the queue, looks up the channel/adapter, and
  stamps the history row with SUCCESS / FAILED / SKIPPED. Failures
  re-publish the task with retryCount++ until MAX_ATTEMPTS (3); the
  history row carries RETRYING in between. We deliberately don't use
  basicNack(requeue=true) — RabbitMQ would put the message back at the
  head of the queue and the worker would tight-loop on it.
- No DLQ wired; terminal FAILED rows are the operator audit trail.

Add tests for both the sender (routing-key shape, history-id required)
and the worker (success/failed/retrying/terminal/skipped paths). Tests
now total 138 in dc3-common-data.
2026-05-21 19:03:04 +08:00
pnoker f2f14558be feat(data): introduce notify task pending state in history writes
dc3_notify_history previously transitioned directly from non-existent
to its terminal status (SUCCESS / FAILED / SKIPPED) at the end of the
synchronous send path. The async dispatch refactor in commit 7 needs
the history row to exist *before* the channel send so the worker has
something to update.

Refactor the writer into three pieces with no external behavior change:

- buildHistory(...) factors out the column population shared by both
  paths.
- persistPendingHistory(...) writes a PENDING row before adapter.send
  so the row id is stable for the subsequent finalize update (and for
  the upcoming worker).
- finalizeHistory(...) updates the same row to SUCCESS / FAILED with
  response_ext / error_message / target.
- historySkipped(...) keeps writing SKIPPED rows directly — those rows
  describe a *decision* to not send, so they have no PENDING phase.

This change keeps existing tests green and isolates the storage shape
needed for the queue-based worker that lands next.
2026-05-21 18:46:13 +08:00