Address review feedback on the batched RPC persistence PR:
- JpaRpcDao: replace field @Autowired with @RequiredArgsConstructor + final
fields, matching the sibling batch DAOs (JpaBaseEventDao, JpaBaseEdgeEventDao).
- TbRpcService: route the update path's post-persist notification through
notifyRuleEngine() so create and update share identical try/catch + logging.
The initial RPC create is now persisted synchronously before sending
(persist-before-send); only status updates remain on the async batched
queue. The batched-insert plumbing that was retained for reversibility is
now dead code and is removed:
- remove createAsync from RpcService/RpcDao/BaseRpcService/JpaRpcDao
- delete RpcQueueEntry (insert/update discriminator no longer needed)
- rename RpcInsertRepository -> RpcUpdateRepository; the queue now carries
RpcEntity directly and batches UPDATEs only
- re-aim JpaRpcDaoTest / TbRpcServiceTest off the removed insert API
No behavior change: update batching, uuid partitioning/sort, and the
response COALESCE preservation are all unchanged.
The deprecation cleanup set @EqualsAndHashCode(callSuper = true) on AlarmComment,
so equals/hashCode now include the BaseData id and createdTime. The delete tests
built the expected comment via a fresh builder (no id), which no longer matches the
server-generated deletion comment (which reuses the original comment id).
Mutate the original alarm comment (already carrying the correct id/createdTime) into
the expected SYSTEM deletion comment instead of building a fresh one.
The previous version stubbed both writes with immediateFuture and submitted them sequentially, so the QUEUED-before-DELIVERED order held by construction and would have passed even if per-rpcId striping regressed to a shared multi-threaded pool. Make the QUEUED notification block (signal start via latch, then sleep) and submit DELIVERED only once QUEUED is in flight, so the assertion now depends on the single-thread stripe serializing same-rpcId callbacks.
On the update paths buildRpc rebuilt the Rpc with createdTime=now, which the post-persist rule-engine notification then serialized, so RPC_DELIVERED/etc. carried the update moment instead of the row's real creation time (the UPDATE never touches created_time). Capture createdTime once at create and thread it through ToDeviceRpcRequestMetadata so update-path notifications report the original value, restoring pre-async behavior.
Also add a striped-executor ordering test (same rpcId -> RPC_QUEUED before RPC_DELIVERED) and document that sql.rpc.callback_threads is independent of batch_threads.
- build saveOrUpdate result inside the transaction so updateCounts is a
non-null local taken straight from batchUpdate, avoiding an NPE on the
@Nullable transactionTemplate.execute() result
- guard HashPartitioner against a non-positive partition count
- fail fast in TbRpcService when sql.rpc.callback_threads < 1
- add a deterministic saveOrUpdate coalesced-batch test and rename the
timing-dependent async test to reflect what it actually verifies
- Rename TbRpcService.save -> update so the create/update pairing is
self-documenting (matches createAsync/updateAsync); update all actor
call sites and unit tests.
- Extract the (hash & 0x7FFFFFFF) % n striping into a shared
HashPartitioner helper used by both TbSqlBlockingQueueWrapper and
TbRpcService, so the end-to-end submission-order invariant has a single
source of truth.
- Collapse the insert/update split in RpcInsertRepository.saveOrUpdate
into a single partitioningBy pass.
- Remove the unused TbRpcService.findRpcById (and its now-orphaned import).
- Restore the prime-thread-count guidance on the RPC batch_threads /
callback_threads yml comments.
Async RPC persistence now returns a per-write Boolean: an INSERT-on-conflict always persists (true), while an UPDATE-by-id reports false when its WHERE id = ? matched no row (RPC deleted via TTL/manual delete). TbRpcService notifies the rule engine only when the write actually persisted, restoring the findById-null skip the async refactor had dropped.
Also: drop the unused tenantId param from createAsync/updateAsync; make TbRpcService's callback-thread count constructor-injectable (removes test reflection) with a comment on why callback striping exists alongside the queue partitioning; correct the RPC batch_threads yml comment; assert the Boolean contract in JpaRpcDaoTest and add a notification-suppression unit test.
Split the batched RPC persistence into a create path (INSERT ... ON CONFLICT)
and an update path (UPDATE ... WHERE id = ?). A status update for a row deleted
in the meantime (TTL cleanup / manual delete) is no longer resurrected,
restoring the old findById-null skip. Both paths COALESCE the response so a
status update that carries none no longer clobbers a previously stored one.
The create-vs-update intent is carried by a dedicated RpcQueueEntry record
(keeping RpcEntity pure data) and exposed via explicit createAsync/updateAsync
DAO methods instead of a boolean flag; insert/update batch scaffolding is
collapsed into a shared helper.
Adds DAO regression tests (re-queue keeps existing row, null-response
preservation, no resurrection of a deleted row) and TbRpcService create/update
wiring tests.
* feat: send pseudonymized install report (tenant/user hash, tb version) to IoT Hub
* refactor: source install-report tb version/edition from ProjectInfo
Use the existing ProjectInfo component (getProjectVersion/getProductType)
instead of an ad-hoc optional BuildProperties field and a hard-coded "CE"
edition, so PE reports its own product type and the version is the canonical
cleaned value. Constructor-injected via @RequiredArgsConstructor.
If iotHubInstalledItemService.save throws after the local entity was already created, the entity was orphaned — visible in the tenant but invisible to IoT Hub update/delete. Extracted the per-type entity-deletion dispatch from deleteInstalledItem into a deleteEntityForDescriptor helper and call it from doInstallVersion and registerDeviceInstall on tracking-save failure. registerDeviceInstall now also wraps reportVersionInstalled best-effort to match doInstallVersion.
The post-install Thread.sleep was driven by installTimeoutMs from the uploaded solution.json with no upper bound — a malicious or misconfigured template could pin an HTTP worker thread indefinitely. Cap it at a configurable max (default 60s) exposed as iot-hub.max-install-timeout-ms with an IOT_HUB_MAX_INSTALL_TIMEOUT_MS env override.
Stream entries through a fixed buffer and abort with IOException when the archive crosses any of three configurable thresholds: total uncompressed bytes, per-entry uncompressed bytes, or entry count. Defaults — 200 MiB total, 50 MiB per entry, 10 000 entries — and env-var overrides IOT_HUB_MAX_UNCOMPRESSED_ARCHIVE_BYTES / IOT_HUB_MAX_UNCOMPRESSED_ENTRY_BYTES / IOT_HUB_MAX_ARCHIVE_ENTRY_COUNT are exposed under iot-hub in thingsboard.yml.
Root entries other than WIDGET or SOLUTION_TEMPLATE should always be installed even when a prior version is already installed — only widgets and solution templates are deduplicated by item id at install-plan resolution time.
Route all parse-failure throws through a single parseFailure(action, itemType[, section], cause) helper so the user-facing wording lives in one place and update paths share the template with install paths.
- IotHubRestClient.getVersionFileData now streams the response and
enforces a configurable size cap (iot-hub.max-file-data-size-bytes,
env IOT_HUB_MAX_FILE_DATA_SIZE_BYTES, default 100 MiB). Oversized
payloads are rejected up front via Content-Length and mid-stream
for chunked transfers, preventing OOMs from unknown bodies.
- DefaultIotHubService guards every iotHubRestClient.getVersionInfo
call against a null response, and rejects update attempts where
the installed item's type does not match the new version's type
("Installed item type does not match the new version's item type.").
Solution-template descriptor is also populated with
tenantTelemetryKeys / tenantAttributeKeys captured during install.
Drops the empty updateDeviceProfile stub.
- Add a tenant-scoped findByTenantIdAndId + deleteByTenantIdAndId to
IotHubInstalledItemDao (and the bulk deleteByTenantIdAndIdIn JPA
query). IotHubInstalledItemServiceImpl now uses these so reads
and deletes always honour the tenant boundary.
- Database: add lts schema_update.sql + schema-entities-idx.sql
indexes on iot_hub_installed_item(tenant_id),
(tenant_id, item_type), and (tenant_id, item_id) for the new
tenant-scoped lookups.
- DefaultSolutionService: minor cleanup touched alongside.
- .gitignore trailing-newline fix; remove stale superpowers
plan/spec docs that have been folded into the implementation.