mirror of
https://github.com/emqx/neuron.git
synced 2026-10-30 16:09:02 +08:00
Compare commits
9
Commits
v2.9-daily
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da276bebe1 | ||
|
|
3ef9684f70 | ||
|
|
04d4f5114e | ||
|
|
16e5e4bfcc | ||
|
|
29155c4741 | ||
|
|
e6e4823f94 | ||
|
|
de1eec7cd9 | ||
|
|
10f3b194bb | ||
|
|
73f0f6d653 |
@@ -0,0 +1,50 @@
|
||||
# Neuron Engineering and Review Guide
|
||||
|
||||
## Repository context
|
||||
|
||||
Neuron is the open-source C gateway/core and built-in plugin repository. `plugins/restful/` contains management handlers; `plugins/mqtt/` handles broker interactions; `src/utils/`, `src/adapter/`, `src/core/`, and `src/persist/` contain shared decoding, runtime, plugin, and storage logic. This guide covers code maintained in this repository.
|
||||
|
||||
Read [the threat model](docs/threat-model.md) and [security policy](SECURITY.md). Preserve more specific directory instructions and generated-file workflows. Match existing naming, ownership conventions, and error handling. Do not manually edit generated parsers when their generation workflow is authoritative.
|
||||
|
||||
## Code Review Rules
|
||||
|
||||
### Establish the actual security boundary
|
||||
|
||||
- Establish the effective management binding, authentication configuration, and access path. The checked-in loopback/auth defaults are configurable. When a reverse proxy is present, inspect forwarding and authorization without assuming it preserves the gateway's security boundary. Trace the reachable transport before claiming or excluding remote exposure.
|
||||
- For each finding, identify attacker-controlled input, privileges, enabling configuration, changed lines, sink, and concrete effect. Review introduced regressions and impacted callers, not merely suspicious API names. Separate verified behavior from assumptions about releases or packaging.
|
||||
- An authenticated caller is not automatically authorized for user management, credential changes, plugin operations, or device writes. Check each operation's actual identity policy and alternate route/method. Derive roles and authority from Neuron's own supported identity model.
|
||||
|
||||
### Fail closed at authentication and trust boundaries
|
||||
|
||||
- Require appropriate JWT signature, algorithm, issuer/key association, audience, time, and identity validation. External tokens and missing claims must not gain authority through an undocumented fallback. Handle all validation errors, not one named mismatch case.
|
||||
- Account deletion, password change, and token revocation are separate mechanisms. Test the intended behavior without assuming signed tokens instantly disappear.
|
||||
- Treat configured credentials and key material as secrets. Validate TLS peer identity and certificate trust; never admit a shared production private key merely because it ships with the application.
|
||||
|
||||
### Validate memory and lifetime at the consuming boundary
|
||||
|
||||
- Check actual source bytes and destination capacity separately. Validate counts/types, arithmetic, signedness, narrowing, termination, and allocation/iteration agreement before copying or dereferencing. Include singular/batch, malformed, empty, partial, and oversized inputs.
|
||||
- Trace ownership across message queues, callbacks, reconnection, configuration replacement, and shutdown. Retain/copy borrowed data as needed; stop callbacks before releasing state. Include allocation and partial-failure cleanup.
|
||||
- Configuration, imports, restored databases and protocol files remain parsing inputs. Constrain paths and symlinks, preserve valid state on failure, and avoid secrets in errors/logs/exports. UI/schema validation is not sufficient runtime validation.
|
||||
|
||||
### Review intended capabilities accurately
|
||||
|
||||
- Metrics intentionally permits unauthenticated Prometheus scraping. Report added sensitive disclosure, control behavior, or resource abuse—not simply the documented lack of JWT enforcement.
|
||||
- Native plugins share service-process privileges. Authorized extension loading is not itself host escape. Lower-trust access to loading or memory corruption still warrants review. Prove actual privileges and distinguish service execution, container-root execution, and host compromise.
|
||||
- Broker ACLs and device-write rights are independent from REST authentication. Follow publish/subscribe directions and configured write targets; do not infer arbitrary downstream takeover from a scoped action.
|
||||
- Supervision is deployment-dependent. Recovery may shorten a crash but does not eliminate repeated DoS, persistent corruption, or RCE. Do not infer RCE from an unweaponized crash or assume historical binary hardening still applies.
|
||||
|
||||
### Findings and regression evidence
|
||||
|
||||
State prerequisites and unaffected cases. Cite the narrow code location and reachable call chain; avoid generic severity boilerplate. A safe fix should cover shared helpers and sibling paths, not just the reported route. Add targeted boundary/negative tests, including failure branches and ownership during teardown. Keep private reports, unpublished exploit details, customer data, credentials and reward discussions out of public review comments and repository documentation.
|
||||
|
||||
## Build and validation
|
||||
|
||||
Use the documented supported build environment and dependencies. From the repository root, a normal CMake build can use:
|
||||
|
||||
```sh
|
||||
cmake -S . -B build
|
||||
cmake --build build -j4
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
Check `CMakeLists.txt` and CI for the target toolchain and options. Current Debug builds enable ASan unless disabled; do not assume Release does. Functional pytest suites under `tests/ft/` require gateway/broker/simulator dependencies and change runtime state. Run only targeted authorized lab tests; never point them at production OT equipment. Report commands actually run and any missing prerequisites rather than claiming validation from source inspection alone.
|
||||
@@ -89,6 +89,7 @@ if(NOT ENABLE_DATALAYERS)
|
||||
list(FILTER NEURON_SRC_PARSE EXCLUDE REGEX "neu_json_datalayers\\.c$")
|
||||
endif()
|
||||
set(NEURON_BASE_SOURCES
|
||||
src/base/property.c
|
||||
src/base/tag.c
|
||||
src/base/neu_plugin_common.c
|
||||
src/base/tag_sort.c
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Security Policy
|
||||
|
||||
## Private reporting
|
||||
|
||||
The canonical [Neuron security policy](.github/SECURITY.md) governs vulnerability reporting, supported release tracks, and coordinated disclosure. This page adds reporting context for the open-source Neuron code maintained in this repository; it does not establish a separate support or reward policy.
|
||||
|
||||
**Do not disclose vulnerabilities through public issues, discussions, or pull requests.** Open the [Neuron Security tab](https://github.com/emqx/neuron/security) and select **Report a vulnerability** to report privately.
|
||||
|
||||
## Neuron report context
|
||||
|
||||
Include the affected version/commit or image digest, component, safe reproduction, observed impact, and affected/unaffected conditions. In particular, specify:
|
||||
|
||||
- Neuron deployment/package and whether access is direct or through an operator-managed reverse proxy.
|
||||
- Effective management binding, authentication/token configuration, and direct or proxied access path.
|
||||
- Attacker identity/permissions and controlled input: REST request, device response, broker message, import, file, or other source.
|
||||
- Relevant plugin, transport/TLS mode, broker topic/direction ACLs, and device-write prerequisites.
|
||||
- Whether the demonstrated effect is disclosure, denied/unauthorized action, crash, persistent availability loss, memory corruption, or execution. Distinguish observed effects from downstream hypotheses.
|
||||
|
||||
Use synthetic credentials and isolated services/simulators. Do not test customer systems or real equipment without authorization, and transmit sensitive evidence only in the private report.
|
||||
|
||||
The [threat model](docs/threat-model.md) explains deployment boundaries and intended capabilities. Loopback/private networking and authenticated peers are assessment context, not blanket exclusions. Anonymous metrics and authorized native extensions have intended uses; report a concrete confidentiality, integrity, availability, or authorization violation rather than the capability alone.
|
||||
|
||||
Maintainers and automated reviewers should also follow [AGENTS.md](AGENTS.md). Do not place private vulnerability details in public review comments.
|
||||
@@ -299,6 +299,123 @@ Neuron 将为 IIoT 平台提供一系列 API 服务,用于查询基本信息
|
||||
}
|
||||
```
|
||||
|
||||
## Node 自定义属性
|
||||
|
||||
自定义属性是节点的 key/value 元数据,南向驱动节点和北向应用节点均支持。四种操作统一使用 **/api/v2/node/properties**,且节点必须已存在。
|
||||
|
||||
属性的 JSON 值类型在查询和重启后保持不变。节点重命名后,属性随节点迁移到新名称;删除节点时,其属性一并删除。重新创建同名节点不会恢复已删除的属性。
|
||||
|
||||
### 请求头部
|
||||
|
||||
**Authorization** Bearer \<token\>(登录接口返回的 JWT,鉴权规则与其他节点接口一致)
|
||||
|
||||
**Content-Type** application/json(POST、PUT、DELETE)
|
||||
|
||||
### 键和值
|
||||
|
||||
* `node`、`key` 为必填非空字符串,不允许包含 NUL(`\u0000`)。同一节点内的属性 key 唯一。
|
||||
* `value` 必填,`false`、`0`、`""`、`[]` 均为有效值,不能因其为空或为零而省略。请直接传入 JSON 值,不要将其再次编码成 JSON 字符串。
|
||||
* 支持的标量值:字符串、布尔值、整数、浮点数。整数范围为有符号 64 位;浮点数必须是可用双精度表示的有限数值。
|
||||
* 支持的一维数组:元素全部为字符串、全部为布尔值或全部为数值。数值数组允许整数和浮点数混用,例如 `[1, 2.5]`。允许空数组。
|
||||
* 不接受 `null`、对象、嵌套数组及不同类别混合的数组,例如 `[true, 1]`、`[1, "2"]`。字符串值允许包含转义 NUL(`\u0000`)及其他转义字符。
|
||||
* POST 的 `properties` 和 DELETE 的 `keys` 必须为非空数组,批次内不允许重复 key,每项都必须合法。批次操作是原子的:全部成功或完全不修改。
|
||||
|
||||
### 获取属性
|
||||
|
||||
*GET* **/api/v2/node/properties?node=modbus-tcp-node**
|
||||
|
||||
查询参数 `node` 必填。节点名称包含特殊字符时需进行 URL 编码。
|
||||
|
||||
HTTP 200 响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": [
|
||||
{"key": "location", "value": "Building A"},
|
||||
{"key": "enabled", "value": false},
|
||||
{"key": "thresholds", "value": [0, 12.5]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
节点没有属性时返回 `{"properties": []}`。响应值保持原有 JSON 类型;客户端应按 key 识别属性,不依赖数组中的位置。
|
||||
|
||||
### 新增属性
|
||||
|
||||
*POST* **/api/v2/node/properties**
|
||||
|
||||
```json
|
||||
{
|
||||
"node": "modbus-tcp-node",
|
||||
"properties": [
|
||||
{"key": "location", "value": "Building A"},
|
||||
{"key": "enabled", "value": false},
|
||||
{"key": "thresholds", "value": [0, 12.5]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
所有 key 都必须是新属性。任一 key 已存在时,拒绝整个批次,不会新增或覆盖任何属性。
|
||||
|
||||
HTTP 200 响应:
|
||||
|
||||
```json
|
||||
{"error": 0}
|
||||
```
|
||||
|
||||
### 更新单个属性
|
||||
|
||||
*PUT* **/api/v2/node/properties**
|
||||
|
||||
```json
|
||||
{
|
||||
"node": "modbus-tcp-node",
|
||||
"key": "enabled",
|
||||
"value": true
|
||||
}
|
||||
```
|
||||
|
||||
替换一个已有属性的值,新值可以变为其他支持的类型。此操作不会创建不存在的属性,也不能重命名 key。
|
||||
|
||||
HTTP 200 响应:
|
||||
|
||||
```json
|
||||
{"error": 0}
|
||||
```
|
||||
|
||||
### 删除属性
|
||||
|
||||
*DELETE* **/api/v2/node/properties**
|
||||
|
||||
```json
|
||||
{
|
||||
"node": "modbus-tcp-node",
|
||||
"keys": ["location", "enabled"]
|
||||
}
|
||||
```
|
||||
|
||||
原子地删除指定 key。不存在的 key 会被忽略,即使所有指定 key 都不存在也返回成功。空 `keys` 数组不表示删除全部属性,而是非法请求。
|
||||
|
||||
HTTP 200 响应:
|
||||
|
||||
```json
|
||||
{"error": 0}
|
||||
```
|
||||
|
||||
### 响应状态
|
||||
|
||||
| HTTP 状态 | 错误码 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| 200 | 0 | POST、PUT、DELETE 成功;GET 返回上述 properties 对象 |
|
||||
| 400 | 1002 | JSON 请求体、必填字段、key、value 或批次不合法 |
|
||||
| 400 | 1003 | GET 查询参数缺失或不合法 |
|
||||
| 401 / 403 | 现有 JWT 错误码 | 令牌缺失、格式错误、过期或无效,遵循现有鉴权规则 |
|
||||
| 404 | 2003 | 节点不存在,适用于全部四种操作 |
|
||||
| 404 | 2020 | 属性不存在,仅适用于 PUT |
|
||||
| 409 | 2019 | 属性已存在,仅适用于 POST;整个批次保持不变 |
|
||||
|
||||
错误响应格式为 `{"error": <错误码>}`。
|
||||
|
||||
## 获取 Node
|
||||
|
||||
*GET* /api/v2/node
|
||||
|
||||
@@ -291,6 +291,123 @@ Neuron provide a series of API services for IIoT platform, to query the basic in
|
||||
}
|
||||
```
|
||||
|
||||
## Node Properties
|
||||
|
||||
Node properties are custom key/value metadata for both southbound driver nodes and northbound application nodes. All four operations use **/api/v2/node/properties** and require an existing node.
|
||||
|
||||
Properties retain their JSON value types across reads and restarts. Renaming a node moves its properties to the new name; deleting a node also deletes its properties. Recreating a node with the same name starts with no properties.
|
||||
|
||||
### Request Headers
|
||||
|
||||
**Authorization** Bearer \<token\> (JWT returned by the login API, using the same authentication rules as other node APIs)
|
||||
|
||||
**Content-Type** application/json (POST, PUT and DELETE)
|
||||
|
||||
### Keys and Values
|
||||
|
||||
* `node` and `key` are required, non-empty strings and must not contain NUL (`\u0000`). Keys are unique within a node.
|
||||
* `value` is required, including when it is `false`, `0`, `""` or `[]`. It is a JSON value, not a JSON-encoded string.
|
||||
* Supported scalar values: string, boolean, integer and floating-point number. Integers use the signed 64-bit range; floating-point numbers must be finite and representable as double precision.
|
||||
* Supported arrays: one-dimensional arrays containing only strings, only booleans, or only numbers. Integers and floating-point numbers may be mixed in a numeric array, for example `[1, 2.5]`. Empty arrays are allowed.
|
||||
* `null`, objects, nested arrays and mixed-category arrays such as `[true, 1]` or `[1, "2"]` are rejected. JSON string values may contain escaped NUL (`\u0000`) and other escaped characters.
|
||||
* POST `properties` and DELETE `keys` must be non-empty arrays, without duplicate keys. Every item must be valid. A batch either succeeds in full or makes no changes.
|
||||
|
||||
### Get Properties
|
||||
|
||||
*GET* **/api/v2/node/properties?node=modbus-tcp-node**
|
||||
|
||||
The `node` query parameter is required. URL-encode the node name when needed.
|
||||
|
||||
HTTP 200 response:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": [
|
||||
{"key": "location", "value": "Building A"},
|
||||
{"key": "enabled", "value": false},
|
||||
{"key": "thresholds", "value": [0, 12.5]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A node without properties returns `{"properties": []}`. The response contains JSON values with their original types; clients should identify properties by key rather than array position.
|
||||
|
||||
### Add Properties
|
||||
|
||||
*POST* **/api/v2/node/properties**
|
||||
|
||||
```json
|
||||
{
|
||||
"node": "modbus-tcp-node",
|
||||
"properties": [
|
||||
{"key": "location", "value": "Building A"},
|
||||
{"key": "enabled", "value": false},
|
||||
{"key": "thresholds", "value": [0, 12.5]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All keys must be new. If any key already exists, the entire batch is rejected without adding or replacing any properties.
|
||||
|
||||
HTTP 200 response:
|
||||
|
||||
```json
|
||||
{"error": 0}
|
||||
```
|
||||
|
||||
### Update a Property
|
||||
|
||||
*PUT* **/api/v2/node/properties**
|
||||
|
||||
```json
|
||||
{
|
||||
"node": "modbus-tcp-node",
|
||||
"key": "enabled",
|
||||
"value": true
|
||||
}
|
||||
```
|
||||
|
||||
Replaces the value of one existing property. The new value may have a different supported type. This operation does not create a missing property or rename its key.
|
||||
|
||||
HTTP 200 response:
|
||||
|
||||
```json
|
||||
{"error": 0}
|
||||
```
|
||||
|
||||
### Delete Properties
|
||||
|
||||
*DELETE* **/api/v2/node/properties**
|
||||
|
||||
```json
|
||||
{
|
||||
"node": "modbus-tcp-node",
|
||||
"keys": ["location", "enabled"]
|
||||
}
|
||||
```
|
||||
|
||||
Deletes the listed keys atomically. Missing keys are ignored and do not cause an error, including when all listed keys are missing. An empty `keys` array is not a request to delete all properties and is rejected.
|
||||
|
||||
HTTP 200 response:
|
||||
|
||||
```json
|
||||
{"error": 0}
|
||||
```
|
||||
|
||||
### Response Status
|
||||
|
||||
| HTTP status | Error | Meaning |
|
||||
| --- | --- | --- |
|
||||
| 200 | 0 | POST, PUT or DELETE succeeded; GET returns the properties object shown above |
|
||||
| 400 | 1002 | Invalid JSON body, missing required fields or invalid keys/values/batch |
|
||||
| 400 | 1003 | Missing or invalid GET query parameter |
|
||||
| 401 / 403 | Existing JWT error codes | Missing, malformed, expired or invalid token, following the existing authentication rules |
|
||||
| 404 | 2003 | Node does not exist (all four operations) |
|
||||
| 404 | 2020 | Property does not exist (PUT only) |
|
||||
| 409 | 2019 | A property already exists (POST only); the entire batch is unchanged |
|
||||
|
||||
Error responses use `{"error": <error code>}`.
|
||||
|
||||
## Get Node
|
||||
|
||||
*GET* /api/v2/node
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Neuron Threat Model
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
This model guides security review of the Neuron core, built-in plugins, and their integration boundaries. It describes intended security requirements and deployment assumptions, not a certification that every path implements them. Check the actual changed code, configuration, packaging, and release when assessing a finding.
|
||||
|
||||
This model covers code maintained in this open-source repository. Describe external inputs and deployment dependencies in terms of the interfaces they expose; derive security requirements from Neuron's own behavior and supported identity model.
|
||||
|
||||
## Deployment profiles
|
||||
|
||||
| Profile | Entry and authentication boundary | Review implications |
|
||||
| --- | --- | --- |
|
||||
| Direct management access | Management HTTP API, normally port 7000; checked-in configuration uses `127.0.0.1` and enables authentication | Binding, authentication, keys, process identity, and TLS termination are configurable. State the actual profile, not just defaults. |
|
||||
| Management through an operator-managed reverse proxy | Client requests reach Neuron through an intermediary | Verify exposed routes, forwarding, TLS termination, and effective gateway/proxy authentication and authorization. Do not assume a proxy preserves the intended boundary. |
|
||||
| Device and broker integrations | Protocol sockets, broker subscriptions, and configured remote endpoints | These paths do not inherit REST authentication. Trace their own peer identity, topic permissions, message handling, and device-write controls. |
|
||||
|
||||
Loopback reduces direct remote exposure but does not exclude access through a proxy, SSRF, a local process, shared network namespaces, or modified packaging. Conversely, a source-level handler defect does not by itself establish external reachability in a particular deployment.
|
||||
|
||||
Production deployments commonly segment management and OT networks. Segmentation affects exploit prerequisites; it is not a parser safety guarantee or a reason to exclude a compromised device or authorized malicious publisher.
|
||||
|
||||
## Assets and actors
|
||||
|
||||
Protect management identities and JWT signing material; device/broker credentials; configuration and persisted state; plugin libraries and schemas; process memory; collected data; device-write capability; and acquisition/forwarding availability.
|
||||
|
||||
Relevant actors include unauthenticated management peers, ordinary authenticated users, administrators, holders of tokens from configured issuers, malicious or compromised devices/brokers/publishers, and local processes. Administrators and installed native code possess substantial intended authority, but that does not authorize lower-trust actors to acquire it.
|
||||
|
||||
Use synthetic secrets and simulators for validation. Never direct a security test at customer equipment without explicit authorization.
|
||||
|
||||
## Data flows and trust boundaries
|
||||
|
||||
| Flow | Boundary | Required checks |
|
||||
| --- | --- | --- |
|
||||
| Client → REST handler | Network caller → management authority | Explicit authentication and operation-specific identity/authorization checks; complete error handling |
|
||||
| JWT → identity | Externally supplied claims → trusted principal | Signature, permitted algorithm, configured issuer/key association, intended audience, applicable time constraints, and explicit identity semantics |
|
||||
| Broker → MQTT write request | Authorized publisher → gateway/device action | Broker identity and direction-specific topic ACLs, message validation, configured target and write permissions |
|
||||
| Device reply → parser/cache | Remote bytes → process memory and tag values | Actual frame lengths, destination bounds, counts/types, resource limits, and object lifetimes |
|
||||
| Configuration/import/database → runtime | Stored or supplied data → allocations, paths, plugins, and connections | Revalidation, bounded decoding, path containment, safe persistence, and secret protection |
|
||||
| Plugin library → gateway process | Native extension → service privileges | Authorized loading and trusted artifact admission; no assumed plugin sandbox |
|
||||
| Driver write → field device | Management/integration command → operational state | Caller authority, configured destination, writable tag attributes, and value validation |
|
||||
|
||||
## Security requirements
|
||||
|
||||
### Management identity and JWT
|
||||
|
||||
A valid signature is necessary but not sufficient for accepting a token. Bind verification to the expected audience and configured issuer/key relationship; reject missing or invalid claims when required by that token profile. Do not treat tokens minted for other services as Neuron credentials merely because a signing key is shared.
|
||||
|
||||
Claims must not implicitly create administrative authority without an explicit supported identity policy. Distinguish legacy local-token conventions from admission of externally issued tokens. User creation, deletion, credential changes, and other sensitive actions require their own authorization decisions; a valid JWT alone does not establish that every operation is permitted.
|
||||
|
||||
Trace credential removal and token lifetime separately. Deleting an account need not invalidate an already signed stateless token unless a revocation mechanism exists; do not claim immediate revocation without evidence.
|
||||
|
||||
### Parsing, memory, and concurrency
|
||||
|
||||
Validate source bytes remaining and destination capacity independently. Check arithmetic before allocation, copy, pointer movement, or narrowing conversion. Reject inconsistent frame lengths, invalid types, excessive counts, oversized strings, and incomplete fragments. Keep allocation sizes consistent with iteration bounds.
|
||||
|
||||
For asynchronous messages, establish whether each buffer is borrowed, owned, copied, or transferred. Retain data until the last callback finishes. Cancellation, reconnection, configuration changes, and shutdown must not leave callbacks accessing freed state. Include failure and partial-initialization cleanup paths.
|
||||
|
||||
Treat persisted/imported data as input. Schema or UI checks are not substitutes for runtime validation, including when old configuration is restored after an upgrade.
|
||||
|
||||
### Cryptography and secrets
|
||||
|
||||
Validate peer identity as well as chain trust when the protocol requires it. Keep TLS server verification separate from possession of a client credential. Public certificates are not private keys; do not confuse their sensitivity. Shipped production credentials and reusable trusted CA private keys must not be public image material. Protect key storage, rotation, permissions, logs, backups, and exports.
|
||||
|
||||
### Metrics and intended capabilities
|
||||
|
||||
Prometheus metrics intentionally supports unauthenticated scraping. Absence of JWT verification on the documented metrics endpoint is not alone an authorization defect. Still review added labels/data for secrets, unexpected sensitive values, unbounded cardinality, expensive requests, and newly introduced control actions. Operators must place the endpoint appropriately.
|
||||
|
||||
Native plugins execute within the gateway process with its service-account authority. Authorized loading of native code is an intended capability, not evidence of container escape. A lower-trust path reaching plugin loading or corrupting plugin memory is a separate security question. Process-user execution, container-root execution, and host compromise require different evidence.
|
||||
|
||||
## Availability and impact limits
|
||||
|
||||
Identify the actual supervisor and restart policy. Recovery is deployment-dependent, not universal. Automatic recovery can reduce a single crash's duration, while repeated inputs may sustain a crash loop. It does not undo credential exposure, state corruption, unsafe device writes, or arbitrary execution.
|
||||
|
||||
Do not infer code execution from every invalid memory access. Conversely, do not reduce established memory corruption to harmless failure just because the process restarts. Account for build architecture, hardening, actual privileges, and demonstrated primitives. Do not extrapolate historical non-PIE or sanitizer assumptions to current binaries.
|
||||
|
||||
## Evidence and maintenance
|
||||
|
||||
Useful anchors are `neuron.json`, `src/argparse.c`, `src/utils/neu_jwt.c`, `include/neuron/utils/http_handler.h`, `plugins/restful/`, `plugins/mqtt/`, `src/core/plugin_manager.c`, `src/persist/`, and `CMakeLists.txt`.
|
||||
|
||||
A review should identify the changed boundary, attacker-controlled input, necessary configuration, actual effects, and a safe regression test. Separate implementation evidence from packaging assumptions and unverified downstream consequences.
|
||||
|
||||
Update this model when changing binding/authentication defaults, token profiles, metrics contents, user policies, plugin admission, write routing, persistence formats, process privileges, or supervision. Follow the [security policy](../SECURITY.md) for private disclosure and the [review rules](../AGENTS.md) for engineering guidance.
|
||||
@@ -72,6 +72,8 @@ typedef enum {
|
||||
NEU_ERR_NODE_TAGS_TOO_LONG = 2016,
|
||||
NEU_ERR_NODE_TAGS_TOO_MANY = 2017,
|
||||
NEU_ERR_NODE_TAGS_INVALID = 2018,
|
||||
NEU_ERR_NODE_PROPERTY_EXIST = 2019,
|
||||
NEU_ERR_NODE_PROPERTY_NOT_EXIST = 2020,
|
||||
|
||||
NEU_ERR_GROUP_ALREADY_SUBSCRIBED = 2101,
|
||||
NEU_ERR_GROUP_NOT_SUBSCRIBE = 2102,
|
||||
|
||||
@@ -31,6 +31,7 @@ extern "C" {
|
||||
|
||||
#include "define.h"
|
||||
#include "errcodes.h"
|
||||
#include "property.h"
|
||||
#include "tag.h"
|
||||
#include "type.h"
|
||||
#include <math.h>
|
||||
@@ -206,6 +207,12 @@ typedef enum neu_reqresp_type {
|
||||
NEU_REQ_SERVER_AUTH_USER_UPDATE_PWD,
|
||||
NEU_REQ_SERVER_AUTH_USER_INFO,
|
||||
NEU_RESP_SERVER_AUTH_USER_INFO,
|
||||
|
||||
NEU_REQ_ADD_NODE_PROPERTIES,
|
||||
NEU_REQ_DELETE_NODE_PROPERTIES,
|
||||
NEU_REQ_UPDATE_NODE_PROPERTY,
|
||||
NEU_REQ_GET_NODE_PROPERTIES,
|
||||
NEU_RESP_GET_NODE_PROPERTIES,
|
||||
} neu_reqresp_type_e;
|
||||
|
||||
static const char *neu_reqresp_type_string_t[] = {
|
||||
@@ -374,6 +381,12 @@ static const char *neu_reqresp_type_string_t[] = {
|
||||
"NEU_REQ_SERVER_AUTH_USER_UPDATE_PWD",
|
||||
[NEU_REQ_SERVER_AUTH_USER_INFO] = "NEU_REQ_SERVER_AUTH_USER_INFO",
|
||||
[NEU_RESP_SERVER_AUTH_USER_INFO] = "NEU_RESP_SERVER_AUTH_USER_INFO",
|
||||
|
||||
[NEU_REQ_ADD_NODE_PROPERTIES] = "NEU_REQ_ADD_NODE_PROPERTIES",
|
||||
[NEU_REQ_DELETE_NODE_PROPERTIES] = "NEU_REQ_DELETE_NODE_PROPERTIES",
|
||||
[NEU_REQ_UPDATE_NODE_PROPERTY] = "NEU_REQ_UPDATE_NODE_PROPERTY",
|
||||
[NEU_REQ_GET_NODE_PROPERTIES] = "NEU_REQ_GET_NODE_PROPERTIES",
|
||||
[NEU_RESP_GET_NODE_PROPERTIES] = "NEU_RESP_GET_NODE_PROPERTIES",
|
||||
};
|
||||
|
||||
inline static const char *neu_reqresp_type_string(neu_reqresp_type_e type)
|
||||
@@ -554,6 +567,65 @@ typedef struct neu_req_del_node {
|
||||
char node[NEU_NODE_NAME_LEN];
|
||||
} neu_req_del_node_t;
|
||||
|
||||
typedef struct neu_req_add_node_properties {
|
||||
char node[NEU_NODE_NAME_LEN];
|
||||
size_t n_property;
|
||||
neu_node_property_t *properties;
|
||||
} neu_req_add_node_properties_t;
|
||||
|
||||
static inline void
|
||||
neu_req_add_node_properties_fini(neu_req_add_node_properties_t *req)
|
||||
{
|
||||
neu_node_properties_free(req->properties, req->n_property);
|
||||
req->properties = NULL;
|
||||
req->n_property = 0;
|
||||
}
|
||||
|
||||
typedef struct neu_req_delete_node_properties {
|
||||
char node[NEU_NODE_NAME_LEN];
|
||||
size_t n_key;
|
||||
char **keys;
|
||||
} neu_req_delete_node_properties_t;
|
||||
|
||||
static inline void
|
||||
neu_req_delete_node_properties_fini(neu_req_delete_node_properties_t *req)
|
||||
{
|
||||
for (size_t i = 0; i < req->n_key; ++i) {
|
||||
free(req->keys[i]);
|
||||
}
|
||||
free(req->keys);
|
||||
req->keys = NULL;
|
||||
req->n_key = 0;
|
||||
}
|
||||
|
||||
typedef struct neu_req_update_node_property {
|
||||
char node[NEU_NODE_NAME_LEN];
|
||||
neu_node_property_t property;
|
||||
} neu_req_update_node_property_t;
|
||||
|
||||
static inline void
|
||||
neu_req_update_node_property_fini(neu_req_update_node_property_t *req)
|
||||
{
|
||||
neu_node_property_fini(&req->property);
|
||||
}
|
||||
|
||||
typedef struct neu_req_get_node_properties {
|
||||
char node[NEU_NODE_NAME_LEN];
|
||||
} neu_req_get_node_properties_t;
|
||||
|
||||
typedef struct neu_resp_get_node_properties {
|
||||
size_t n_property;
|
||||
neu_node_property_t *properties;
|
||||
} neu_resp_get_node_properties_t;
|
||||
|
||||
static inline void
|
||||
neu_resp_get_node_properties_fini(neu_resp_get_node_properties_t *resp)
|
||||
{
|
||||
neu_node_properties_free(resp->properties, resp->n_property);
|
||||
resp->properties = NULL;
|
||||
resp->n_property = 0;
|
||||
}
|
||||
|
||||
typedef struct neu_req_get_node {
|
||||
neu_node_type_e type;
|
||||
int n_plugin;
|
||||
|
||||
@@ -28,6 +28,7 @@ extern "C" {
|
||||
|
||||
#include "adapter_info.h"
|
||||
#include "persist/json/persist_json_plugin.h"
|
||||
#include "property.h"
|
||||
|
||||
typedef neu_json_plugin_req_plugin_t neu_persist_plugin_info_t;
|
||||
|
||||
@@ -264,6 +265,17 @@ int neu_persister_update_node_tags(const char *node_name, const char *tags);
|
||||
*/
|
||||
int neu_persister_update_node_state(const char *node_name, int state);
|
||||
|
||||
int neu_persister_add_node_properties(const char * node,
|
||||
const neu_node_property_t *properties,
|
||||
size_t n_property);
|
||||
int neu_persister_delete_node_properties(const char * node,
|
||||
const char *const *keys, size_t n_key);
|
||||
int neu_persister_update_node_property(const char * node,
|
||||
const neu_node_property_t *property);
|
||||
int neu_persister_load_node_properties(const char * node,
|
||||
neu_node_property_t **properties,
|
||||
size_t * n_property);
|
||||
|
||||
/**
|
||||
* Persist plugins.
|
||||
* @param plugin_infos vector of neu_persist_plugin_info_t.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef NEURON_PROPERTY_H
|
||||
#define NEURON_PROPERTY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <jansson.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
char *neu_node_property_json_dump(const json_t *value);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char *key;
|
||||
char *value_json;
|
||||
} neu_node_property_t;
|
||||
|
||||
static inline void neu_node_property_fini(neu_node_property_t *property)
|
||||
{
|
||||
free(property->key);
|
||||
free(property->value_json);
|
||||
property->key = NULL;
|
||||
property->value_json = NULL;
|
||||
}
|
||||
|
||||
static inline void neu_node_properties_free(neu_node_property_t *properties,
|
||||
size_t n_property)
|
||||
{
|
||||
for (size_t i = 0; i < n_property; ++i) {
|
||||
neu_node_property_fini(&properties[i]);
|
||||
}
|
||||
free(properties);
|
||||
}
|
||||
|
||||
static inline int neu_node_property_value_kind(const json_t *value)
|
||||
{
|
||||
if (json_is_string(value)) {
|
||||
return 1;
|
||||
}
|
||||
if (json_is_boolean(value)) {
|
||||
return 2;
|
||||
}
|
||||
if (json_is_number(value)) {
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline bool neu_node_property_value_valid(const json_t *value)
|
||||
{
|
||||
if (neu_node_property_value_kind(value) != 0) {
|
||||
return true;
|
||||
}
|
||||
if (!json_is_array(value)) {
|
||||
return false;
|
||||
}
|
||||
int kind = 0;
|
||||
for (size_t i = 0; i < json_array_size(value); ++i) {
|
||||
int item_kind = neu_node_property_value_kind(json_array_get(value, i));
|
||||
if (item_kind == 0 || (kind != 0 && kind != item_kind)) {
|
||||
return false;
|
||||
}
|
||||
kind = item_kind;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
+12
-3
@@ -340,9 +340,17 @@ static inline char *neu_value_str(neu_type_e type, neu_value_u value)
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
neu_type_e type;
|
||||
neu_value_u value;
|
||||
uint8_t precision;
|
||||
int64_t timestamp;
|
||||
int32_t quality;
|
||||
bool has_timestamp;
|
||||
bool has_quality;
|
||||
} neu_tag_sample_attrs_t;
|
||||
|
||||
typedef struct {
|
||||
neu_type_e type;
|
||||
neu_value_u value;
|
||||
uint8_t precision;
|
||||
neu_tag_sample_attrs_t attrs;
|
||||
} neu_dvalue_t;
|
||||
|
||||
static inline bool neu_dvalue_is_array(const neu_dvalue_t *dvalue)
|
||||
@@ -508,6 +516,7 @@ static inline void neu_copy_dvalue(const neu_dvalue_t *src, neu_dvalue_t *dst)
|
||||
dst->type = src->type;
|
||||
dst->precision = src->precision;
|
||||
dst->value = src->value;
|
||||
dst->attrs = src->attrs;
|
||||
|
||||
// value need deep copy
|
||||
switch (src->type) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE node_properties (
|
||||
node_name TEXT NOT NULL,
|
||||
key TEXT NOT NULL CHECK(length(key) > 0),
|
||||
value_json TEXT NOT NULL,
|
||||
PRIMARY KEY (node_name, key),
|
||||
FOREIGN KEY (node_name) REFERENCES nodes(name)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
@@ -713,6 +713,122 @@ void handle_put_drivers(nng_aio *aio)
|
||||
})
|
||||
}
|
||||
|
||||
void handle_add_node_properties(nng_aio *aio)
|
||||
{
|
||||
NEU_PROCESS_HTTP_REQUEST_VALIDATE_JWT(
|
||||
aio, neu_json_add_node_properties_req_t,
|
||||
neu_json_decode_add_node_properties_req, {
|
||||
neu_reqresp_head_t header = { 0 };
|
||||
neu_req_add_node_properties_t cmd = { 0 };
|
||||
header.ctx = aio;
|
||||
header.type = NEU_REQ_ADD_NODE_PROPERTIES;
|
||||
header.otel_trace_type = NEU_OTEL_TRACE_TYPE_REST_COMM;
|
||||
strcpy(cmd.node, req->node);
|
||||
cmd.properties = req->properties;
|
||||
cmd.n_property = req->n_property;
|
||||
req->properties = NULL;
|
||||
req->n_property = 0;
|
||||
if (neu_plugin_op(neu_rest_get_plugin(), header, &cmd) != 0) {
|
||||
neu_req_add_node_properties_fini(&cmd);
|
||||
NEU_JSON_RESPONSE_ERROR(NEU_ERR_IS_BUSY, {
|
||||
neu_http_response(aio, error_code.error, result_error);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
void handle_delete_node_properties(nng_aio *aio)
|
||||
{
|
||||
NEU_PROCESS_HTTP_REQUEST_VALIDATE_JWT(
|
||||
aio, neu_json_delete_node_properties_req_t,
|
||||
neu_json_decode_delete_node_properties_req, {
|
||||
neu_reqresp_head_t header = { 0 };
|
||||
neu_req_delete_node_properties_t cmd = { 0 };
|
||||
header.ctx = aio;
|
||||
header.type = NEU_REQ_DELETE_NODE_PROPERTIES;
|
||||
header.otel_trace_type = NEU_OTEL_TRACE_TYPE_REST_COMM;
|
||||
strcpy(cmd.node, req->node);
|
||||
cmd.keys = req->keys;
|
||||
cmd.n_key = req->n_key;
|
||||
req->keys = NULL;
|
||||
req->n_key = 0;
|
||||
if (neu_plugin_op(neu_rest_get_plugin(), header, &cmd) != 0) {
|
||||
neu_req_delete_node_properties_fini(&cmd);
|
||||
NEU_JSON_RESPONSE_ERROR(NEU_ERR_IS_BUSY, {
|
||||
neu_http_response(aio, error_code.error, result_error);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
void handle_update_node_property(nng_aio *aio)
|
||||
{
|
||||
NEU_PROCESS_HTTP_REQUEST_VALIDATE_JWT(
|
||||
aio, neu_json_update_node_property_req_t,
|
||||
neu_json_decode_update_node_property_req, {
|
||||
neu_reqresp_head_t header = { 0 };
|
||||
neu_req_update_node_property_t cmd = { 0 };
|
||||
header.ctx = aio;
|
||||
header.type = NEU_REQ_UPDATE_NODE_PROPERTY;
|
||||
header.otel_trace_type = NEU_OTEL_TRACE_TYPE_REST_COMM;
|
||||
strcpy(cmd.node, req->node);
|
||||
cmd.property = req->property;
|
||||
req->property.key = NULL;
|
||||
req->property.value_json = NULL;
|
||||
if (neu_plugin_op(neu_rest_get_plugin(), header, &cmd) != 0) {
|
||||
neu_req_update_node_property_fini(&cmd);
|
||||
NEU_JSON_RESPONSE_ERROR(NEU_ERR_IS_BUSY, {
|
||||
neu_http_response(aio, error_code.error, result_error);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
void handle_get_node_properties(nng_aio *aio)
|
||||
{
|
||||
NEU_VALIDATE_JWT(aio);
|
||||
|
||||
neu_req_get_node_properties_t cmd = { 0 };
|
||||
ssize_t length =
|
||||
neu_http_get_param_str(aio, "node", cmd.node, sizeof(cmd.node));
|
||||
if (length <= 0 || length >= (ssize_t) sizeof(cmd.node) ||
|
||||
strlen(cmd.node) != (size_t) length) {
|
||||
NEU_JSON_RESPONSE_ERROR(NEU_ERR_PARAM_IS_WRONG, {
|
||||
neu_http_response(aio, error_code.error, result_error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
neu_reqresp_head_t header = {
|
||||
.ctx = aio,
|
||||
.type = NEU_REQ_GET_NODE_PROPERTIES,
|
||||
.otel_trace_type = NEU_OTEL_TRACE_TYPE_REST_COMM,
|
||||
};
|
||||
if (neu_plugin_op(neu_rest_get_plugin(), header, &cmd) != 0) {
|
||||
NEU_JSON_RESPONSE_ERROR(NEU_ERR_IS_BUSY, {
|
||||
neu_http_response(aio, error_code.error, result_error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void handle_get_node_properties_resp(nng_aio * aio,
|
||||
neu_resp_get_node_properties_t *properties)
|
||||
{
|
||||
neu_json_get_node_properties_resp_t resp = {
|
||||
.n_property = properties->n_property,
|
||||
.properties = properties->properties,
|
||||
};
|
||||
char *result = NULL;
|
||||
if (neu_json_encode_get_node_properties_resp(&resp, &result) != 0) {
|
||||
NEU_JSON_RESPONSE_ERROR(NEU_ERR_EINTERNAL, {
|
||||
neu_http_response(aio, error_code.error, result_error);
|
||||
});
|
||||
} else {
|
||||
neu_http_ok(aio, result);
|
||||
}
|
||||
free(result);
|
||||
neu_resp_get_node_properties_fini(properties);
|
||||
}
|
||||
|
||||
void handle_put_node_tag(nng_aio *aio)
|
||||
{
|
||||
neu_plugin_t *plugin = neu_rest_get_plugin();
|
||||
|
||||
@@ -43,4 +43,11 @@ void handle_get_nodes_state_resp(nng_aio * aio,
|
||||
void handle_put_drivers(nng_aio *aio);
|
||||
void handle_put_node_tag(nng_aio *aio);
|
||||
|
||||
void handle_add_node_properties(nng_aio *aio);
|
||||
void handle_delete_node_properties(nng_aio *aio);
|
||||
void handle_update_node_property(nng_aio *aio);
|
||||
void handle_get_node_properties(nng_aio *aio);
|
||||
void handle_get_node_properties_resp(
|
||||
nng_aio *aio, neu_resp_get_node_properties_t *properties);
|
||||
|
||||
#endif
|
||||
@@ -109,6 +109,9 @@ static struct neu_http_handler cors_handler[] = {
|
||||
{
|
||||
.url = "/api/v2/node/setting",
|
||||
},
|
||||
{
|
||||
.url = "/api/v2/node/properties",
|
||||
},
|
||||
{
|
||||
.url = "/api/v2/node/ctl",
|
||||
},
|
||||
@@ -763,6 +766,30 @@ static struct neu_http_handler rest_handlers[] = {
|
||||
.url = "/api/v2/simulator/export",
|
||||
.value.handler = handle_simulator_export,
|
||||
},
|
||||
{
|
||||
.method = NEU_HTTP_METHOD_POST,
|
||||
.type = NEU_HTTP_HANDLER_FUNCTION,
|
||||
.url = "/api/v2/node/properties",
|
||||
.value.handler = handle_add_node_properties,
|
||||
},
|
||||
{
|
||||
.method = NEU_HTTP_METHOD_PUT,
|
||||
.type = NEU_HTTP_HANDLER_FUNCTION,
|
||||
.url = "/api/v2/node/properties",
|
||||
.value.handler = handle_update_node_property,
|
||||
},
|
||||
{
|
||||
.method = NEU_HTTP_METHOD_DELETE,
|
||||
.type = NEU_HTTP_HANDLER_FUNCTION,
|
||||
.url = "/api/v2/node/properties",
|
||||
.value.handler = handle_delete_node_properties,
|
||||
},
|
||||
{
|
||||
.method = NEU_HTTP_METHOD_GET,
|
||||
.type = NEU_HTTP_HANDLER_FUNCTION,
|
||||
.url = "/api/v2/node/properties",
|
||||
.value.handler = handle_get_node_properties,
|
||||
},
|
||||
{
|
||||
.method = NEU_HTTP_METHOD_PUT,
|
||||
.type = NEU_HTTP_HANDLER_FUNCTION,
|
||||
|
||||
@@ -332,6 +332,13 @@ static int dashb_plugin_request(neu_plugin_t * plugin,
|
||||
neu_otel_scope_set_status_code2(scope, NEU_OTEL_STATUS_OK, 0);
|
||||
}
|
||||
break;
|
||||
case NEU_RESP_GET_NODE_PROPERTIES:
|
||||
handle_get_node_properties_resp(
|
||||
header->ctx, (neu_resp_get_node_properties_t *) data);
|
||||
if (neu_otel_control_is_started() && trace) {
|
||||
neu_otel_scope_set_status_code2(scope, NEU_OTEL_STATUS_OK, 0);
|
||||
}
|
||||
break;
|
||||
case NEU_RESP_GET_NODE_SETTING:
|
||||
handle_get_node_setting_resp(header->ctx,
|
||||
(neu_resp_get_node_setting_t *) data);
|
||||
|
||||
@@ -620,6 +620,10 @@ static int adapter_command(neu_adapter_t *adapter, neu_reqresp_head_t header,
|
||||
snprintf(pheader->receiver, sizeof(pheader->receiver), "%s", cmd->node);
|
||||
break;
|
||||
}
|
||||
case NEU_REQ_ADD_NODE_PROPERTIES:
|
||||
case NEU_REQ_DELETE_NODE_PROPERTIES:
|
||||
case NEU_REQ_UPDATE_NODE_PROPERTY:
|
||||
case NEU_REQ_GET_NODE_PROPERTIES:
|
||||
case NEU_REQ_CHECK_SCHEMA:
|
||||
case NEU_REQ_GET_DRIVER_GROUP:
|
||||
case NEU_REQ_GET_SUB_DRIVER_TAGS: {
|
||||
@@ -768,6 +772,7 @@ static int adapter_command(neu_adapter_t *adapter, neu_reqresp_head_t header,
|
||||
break;
|
||||
}
|
||||
case NEU_RESP_ERROR:
|
||||
case NEU_RESP_GET_NODE_PROPERTIES:
|
||||
case NEU_RESP_SERVER_AUTH_USER_INFO:
|
||||
case NEU_RESP_SERVER_AUTH_SWITCH_STATUS:
|
||||
case NEU_RESP_SERVER_SECURITY_POLICY_STATUS:
|
||||
@@ -951,6 +956,7 @@ static int adapter_loop(enum neu_event_io_type type, int fd, void *usr_data)
|
||||
case NEU_RESP_GET_NODE_STATE:
|
||||
case NEU_RESP_GET_NODES_STATE:
|
||||
case NEU_RESP_GET_NODE_SETTING:
|
||||
case NEU_RESP_GET_NODE_PROPERTIES:
|
||||
case NEU_REQ_UPDATE_GROUP:
|
||||
case NEU_RESP_GET_SUBSCRIBE_GROUP:
|
||||
case NEU_RESP_GET_DRIVER_SUBSCRIBE_GROUP:
|
||||
|
||||
+31
-10
@@ -169,6 +169,7 @@ void neu_driver_cache_add(neu_driver_cache_t *cache, const char *group,
|
||||
elem->timestamp = 0;
|
||||
elem->changed = false;
|
||||
elem->value = value;
|
||||
memset(&elem->value_old.attrs, 0, sizeof(elem->value_old.attrs));
|
||||
|
||||
pthread_mutex_unlock(&cache->mtx);
|
||||
}
|
||||
@@ -229,6 +230,13 @@ bool neu_driver_cache_update_change(neu_driver_cache_t *cache,
|
||||
tkey_t key = to_key(group, tag);
|
||||
bool tag_changed = false;
|
||||
|
||||
if (!value.attrs.has_timestamp) {
|
||||
value.attrs.timestamp = 0;
|
||||
}
|
||||
if (!value.attrs.has_quality) {
|
||||
value.attrs.quality = 0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&cache->mtx);
|
||||
HASH_FIND(hh, cache->table, &key, sizeof(tkey_t), elem);
|
||||
if (elem != NULL) {
|
||||
@@ -483,9 +491,8 @@ bool neu_driver_cache_update_change(neu_driver_cache_t *cache,
|
||||
}
|
||||
case NEU_TYPE_FLOAT:
|
||||
if (elem->value_old.precision == 0) {
|
||||
elem->changed =
|
||||
elem->value_old.value.f32 != value.value.f32;
|
||||
tag_changed = elem->changed;
|
||||
tag_changed = elem->value_old.value.f32 != value.value.f32;
|
||||
elem->changed |= tag_changed;
|
||||
} else {
|
||||
if (fabs(elem->value_old.value.f32 - value.value.f32) >
|
||||
pow(0.1, elem->value_old.precision)) {
|
||||
@@ -496,9 +503,8 @@ bool neu_driver_cache_update_change(neu_driver_cache_t *cache,
|
||||
break;
|
||||
case NEU_TYPE_DOUBLE:
|
||||
if (elem->value_old.precision == 0) {
|
||||
elem->changed =
|
||||
elem->value_old.value.d64 != value.value.d64;
|
||||
tag_changed = elem->changed;
|
||||
tag_changed = elem->value_old.value.d64 != value.value.d64;
|
||||
elem->changed |= tag_changed;
|
||||
} else {
|
||||
if (fabs(elem->value_old.value.d64 - value.value.d64) >
|
||||
pow(0.1, elem->value_old.precision)) {
|
||||
@@ -728,8 +734,8 @@ bool neu_driver_cache_update_change(neu_driver_cache_t *cache,
|
||||
}
|
||||
case NEU_TYPE_FLOAT:
|
||||
if (elem->value.precision == 0) {
|
||||
elem->changed = elem->value.value.f32 != value.value.f32;
|
||||
tag_changed = elem->changed;
|
||||
tag_changed = elem->value.value.f32 != value.value.f32;
|
||||
elem->changed |= tag_changed;
|
||||
} else {
|
||||
if (fabs(elem->value.value.f32 - value.value.f32) >
|
||||
pow(0.1, elem->value.precision)) {
|
||||
@@ -740,8 +746,8 @@ bool neu_driver_cache_update_change(neu_driver_cache_t *cache,
|
||||
break;
|
||||
case NEU_TYPE_DOUBLE:
|
||||
if (elem->value.precision == 0) {
|
||||
elem->changed = elem->value.value.d64 != value.value.d64;
|
||||
tag_changed = elem->changed;
|
||||
tag_changed = elem->value.value.d64 != value.value.d64;
|
||||
elem->changed |= tag_changed;
|
||||
} else {
|
||||
if (fabs(elem->value.value.d64 - value.value.d64) >
|
||||
pow(0.1, elem->value.precision)) {
|
||||
@@ -758,13 +764,26 @@ bool neu_driver_cache_update_change(neu_driver_cache_t *cache,
|
||||
}
|
||||
}
|
||||
|
||||
const neu_tag_sample_attrs_t *previous_attrs =
|
||||
sub_filter_err && elem->value.type == NEU_TYPE_ERROR
|
||||
? &elem->value_old.attrs
|
||||
: &elem->value.attrs;
|
||||
if (previous_attrs->has_quality != value.attrs.has_quality ||
|
||||
(value.attrs.has_quality &&
|
||||
previous_attrs->quality != value.attrs.quality)) {
|
||||
elem->changed = true;
|
||||
tag_changed = true;
|
||||
}
|
||||
|
||||
if (sub_filter_err && value.type != NEU_TYPE_ERROR) {
|
||||
elem->value_old.type = value.type;
|
||||
elem->value_old.value = value.value;
|
||||
elem->value_old.precision = value.precision;
|
||||
elem->value_old.attrs = value.attrs;
|
||||
}
|
||||
|
||||
error_not_report:
|
||||
elem->value.attrs = value.attrs;
|
||||
|
||||
if (change) {
|
||||
elem->changed = true;
|
||||
@@ -887,6 +906,7 @@ int neu_driver_cache_meta_get(neu_driver_cache_t *cache, const char *group,
|
||||
|
||||
if (elem != NULL) {
|
||||
value->timestamp = elem->timestamp;
|
||||
value->value.attrs = elem->value.attrs;
|
||||
value->value.type = elem->value.type;
|
||||
value->value.precision = elem->value.precision;
|
||||
|
||||
@@ -1061,6 +1081,7 @@ int neu_driver_cache_meta_get_changed(neu_driver_cache_t *cache,
|
||||
|
||||
if (elem != NULL && elem->changed) {
|
||||
value->timestamp = elem->timestamp;
|
||||
value->value.attrs = elem->value.attrs;
|
||||
value->value.type = elem->value.type;
|
||||
value->value.precision = elem->value.precision;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "type.h"
|
||||
#include "tag.h"
|
||||
|
||||
typedef struct neu_driver_cache neu_driver_cache_t;
|
||||
|
||||
|
||||
@@ -2851,6 +2851,8 @@ static void read_report_group(int64_t timestamp, int64_t timeout,
|
||||
|
||||
tag_value.datatag.bias = tag->bias;
|
||||
|
||||
tag_value.value.attrs = value.value.attrs;
|
||||
|
||||
if (value.value.type == NEU_TYPE_ERROR) {
|
||||
tag_value.value = value.value;
|
||||
|
||||
@@ -3041,6 +3043,8 @@ static void read_group(int64_t timestamp, int64_t timeout,
|
||||
continue;
|
||||
}
|
||||
|
||||
tag_value.value.attrs = value.value.attrs;
|
||||
|
||||
if (value.value.type == NEU_TYPE_ERROR) {
|
||||
tag_value.value = value.value;
|
||||
utarray_push_back(tag_values, &tag_value);
|
||||
@@ -3236,6 +3240,8 @@ static void read_group_paginate(int64_t timestamp, int64_t timeout,
|
||||
continue;
|
||||
}
|
||||
|
||||
tag_value.value.attrs = value.value.attrs;
|
||||
|
||||
if (value.value.type == NEU_TYPE_ERROR) {
|
||||
tag_value.value = value.value;
|
||||
utarray_push_back(tag_values, &tag_value);
|
||||
|
||||
+12
-1
@@ -174,7 +174,12 @@ extern "C" {
|
||||
XX(NEU_REQ_SERVER_AUTH_USER_INFO, neu_req_server_auth_users_info_t) \
|
||||
XX(NEU_RESP_SERVER_AUTH_USER_INFO, neu_resp_server_auth_users_info_t) \
|
||||
XX(NEU_REQ_RENAME_TAG, neu_req_rename_tag_t) \
|
||||
XX(NEU_RESP_RENAME_TAG, neu_resp_rename_tag_t)
|
||||
XX(NEU_RESP_RENAME_TAG, neu_resp_rename_tag_t) \
|
||||
XX(NEU_REQ_ADD_NODE_PROPERTIES, neu_req_add_node_properties_t) \
|
||||
XX(NEU_REQ_DELETE_NODE_PROPERTIES, neu_req_delete_node_properties_t) \
|
||||
XX(NEU_REQ_UPDATE_NODE_PROPERTY, neu_req_update_node_property_t) \
|
||||
XX(NEU_REQ_GET_NODE_PROPERTIES, neu_req_get_node_properties_t) \
|
||||
XX(NEU_RESP_GET_NODE_PROPERTIES, neu_resp_get_node_properties_t)
|
||||
|
||||
static inline size_t neu_reqresp_size(neu_reqresp_type_e t)
|
||||
{
|
||||
@@ -235,6 +240,12 @@ static inline neu_msg_t *neu_msg_new(neu_reqresp_type_e t, void *ctx,
|
||||
case NEU_REQ_GET_NODE_SETTING:
|
||||
body_size = neu_reqresp_size(NEU_RESP_GET_NODE_SETTING);
|
||||
break;
|
||||
case NEU_REQ_GET_NODE_PROPERTIES:
|
||||
body_size = neu_reqresp_size(NEU_RESP_GET_NODE_PROPERTIES);
|
||||
if (body_size < data_size) {
|
||||
body_size = data_size;
|
||||
}
|
||||
break;
|
||||
case NEU_REQ_GET_NODES_STATE:
|
||||
body_size = neu_reqresp_size(NEU_RESP_GET_NODES_STATE);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#include <locale.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "property.h"
|
||||
|
||||
typedef struct {
|
||||
char * data;
|
||||
size_t length;
|
||||
size_t capacity;
|
||||
} property_json_buffer_t;
|
||||
|
||||
static int append(property_json_buffer_t *buffer, const char *text)
|
||||
{
|
||||
size_t length = strlen(text);
|
||||
if (length >= SIZE_MAX - buffer->length) {
|
||||
return -1;
|
||||
}
|
||||
size_t needed = buffer->length + length + 1;
|
||||
if (needed > buffer->capacity) {
|
||||
size_t capacity = buffer->capacity ? buffer->capacity : 64;
|
||||
while (capacity < needed) {
|
||||
if (capacity > SIZE_MAX / 2) {
|
||||
capacity = needed;
|
||||
break;
|
||||
}
|
||||
capacity *= 2;
|
||||
}
|
||||
char *data = realloc(buffer->data, capacity);
|
||||
if (data == NULL) {
|
||||
return -1;
|
||||
}
|
||||
buffer->data = data;
|
||||
buffer->capacity = capacity;
|
||||
}
|
||||
memcpy(buffer->data + buffer->length, text, length + 1);
|
||||
buffer->length += length;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dump_value(property_json_buffer_t *buffer, const json_t *value)
|
||||
{
|
||||
if (json_is_real(value)) {
|
||||
char text[64];
|
||||
int length =
|
||||
snprintf(text, sizeof(text), "%.17g", json_real_value(value));
|
||||
if (length < 0 || (size_t) length >= sizeof(text) - 2) {
|
||||
return -1;
|
||||
}
|
||||
const char *point = localeconv()->decimal_point;
|
||||
char * decimal = *point ? strstr(text, point) : NULL;
|
||||
if (decimal != NULL && strcmp(point, ".") != 0) {
|
||||
memmove(decimal + 1, decimal + strlen(point),
|
||||
strlen(decimal + strlen(point)) + 1);
|
||||
*decimal = '.';
|
||||
}
|
||||
if (strpbrk(text, ".eE") == NULL) {
|
||||
strcat(text, ".0");
|
||||
}
|
||||
return append(buffer, text);
|
||||
}
|
||||
if (json_is_array(value)) {
|
||||
if (append(buffer, "[") != 0) {
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < json_array_size(value); ++i) {
|
||||
if ((i != 0 && append(buffer, ",") != 0) ||
|
||||
dump_value(buffer, json_array_get(value, i)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return append(buffer, "]");
|
||||
}
|
||||
if (json_is_object(value)) {
|
||||
if (append(buffer, "{") != 0) {
|
||||
return -1;
|
||||
}
|
||||
const char *key;
|
||||
json_t * item;
|
||||
bool first = true;
|
||||
json_object_foreach((json_t *) value, key, item)
|
||||
{
|
||||
json_t *name = json_string(key);
|
||||
if (name == NULL) {
|
||||
return -1;
|
||||
}
|
||||
int ret = (!first && append(buffer, ",") != 0) ||
|
||||
dump_value(buffer, name) != 0 || append(buffer, ":") != 0 ||
|
||||
dump_value(buffer, item) != 0;
|
||||
json_decref(name);
|
||||
if (ret != 0) {
|
||||
return -1;
|
||||
}
|
||||
first = false;
|
||||
(void) first;
|
||||
}
|
||||
return append(buffer, "}");
|
||||
}
|
||||
char *text = json_dumps(value, JSON_COMPACT | JSON_ENCODE_ANY);
|
||||
if (text == NULL) {
|
||||
return -1;
|
||||
}
|
||||
int ret = append(buffer, text);
|
||||
free(text);
|
||||
return ret;
|
||||
}
|
||||
|
||||
char *neu_node_property_json_dump(const json_t *value)
|
||||
{
|
||||
property_json_buffer_t buffer = { 0 };
|
||||
if (dump_value(&buffer, value) != 0) {
|
||||
free(buffer.data);
|
||||
return NULL;
|
||||
}
|
||||
return buffer.data;
|
||||
}
|
||||
@@ -277,6 +277,72 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data)
|
||||
neu_msg_free(msg);
|
||||
break;
|
||||
}
|
||||
case NEU_REQ_ADD_NODE_PROPERTIES: {
|
||||
neu_req_add_node_properties_t *cmd =
|
||||
(neu_req_add_node_properties_t *) &header[1];
|
||||
neu_resp_error_t e = { .error = NEU_ERR_NODE_NOT_EXIST };
|
||||
|
||||
if (neu_node_manager_find(manager->node_manager, cmd->node) != NULL) {
|
||||
e.error = neu_persister_add_node_properties(
|
||||
cmd->node, cmd->properties, cmd->n_property);
|
||||
}
|
||||
neu_req_add_node_properties_fini(cmd);
|
||||
header->type = NEU_RESP_ERROR;
|
||||
neu_msg_exchange(header);
|
||||
reply(manager, header, &e);
|
||||
break;
|
||||
}
|
||||
case NEU_REQ_DELETE_NODE_PROPERTIES: {
|
||||
neu_req_delete_node_properties_t *cmd =
|
||||
(neu_req_delete_node_properties_t *) &header[1];
|
||||
neu_resp_error_t e = { .error = NEU_ERR_NODE_NOT_EXIST };
|
||||
|
||||
if (neu_node_manager_find(manager->node_manager, cmd->node) != NULL) {
|
||||
e.error = neu_persister_delete_node_properties(
|
||||
cmd->node, (const char *const *) cmd->keys, cmd->n_key);
|
||||
}
|
||||
neu_req_delete_node_properties_fini(cmd);
|
||||
header->type = NEU_RESP_ERROR;
|
||||
neu_msg_exchange(header);
|
||||
reply(manager, header, &e);
|
||||
break;
|
||||
}
|
||||
case NEU_REQ_UPDATE_NODE_PROPERTY: {
|
||||
neu_req_update_node_property_t *cmd =
|
||||
(neu_req_update_node_property_t *) &header[1];
|
||||
neu_resp_error_t e = { .error = NEU_ERR_NODE_NOT_EXIST };
|
||||
|
||||
if (neu_node_manager_find(manager->node_manager, cmd->node) != NULL) {
|
||||
e.error =
|
||||
neu_persister_update_node_property(cmd->node, &cmd->property);
|
||||
}
|
||||
neu_req_update_node_property_fini(cmd);
|
||||
header->type = NEU_RESP_ERROR;
|
||||
neu_msg_exchange(header);
|
||||
reply(manager, header, &e);
|
||||
break;
|
||||
}
|
||||
case NEU_REQ_GET_NODE_PROPERTIES: {
|
||||
neu_req_get_node_properties_t *cmd =
|
||||
(neu_req_get_node_properties_t *) &header[1];
|
||||
neu_resp_get_node_properties_t resp = { 0 };
|
||||
neu_resp_error_t e = { .error = NEU_ERR_NODE_NOT_EXIST };
|
||||
|
||||
if (neu_node_manager_find(manager->node_manager, cmd->node) != NULL) {
|
||||
e.error = neu_persister_load_node_properties(
|
||||
cmd->node, &resp.properties, &resp.n_property);
|
||||
}
|
||||
neu_msg_exchange(header);
|
||||
if (e.error == NEU_ERR_SUCCESS) {
|
||||
header->type = NEU_RESP_GET_NODE_PROPERTIES;
|
||||
reply(manager, header, &resp);
|
||||
} else {
|
||||
neu_resp_get_node_properties_fini(&resp);
|
||||
header->type = NEU_RESP_ERROR;
|
||||
reply(manager, header, &e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NEU_REQ_UPDATE_NODE_TAG: {
|
||||
neu_req_update_node_tag_t *cmd =
|
||||
(neu_req_update_node_tag_t *) &header[1];
|
||||
@@ -1645,6 +1711,12 @@ static int manager_loop(enum neu_event_io_type type, int fd, void *usr_data)
|
||||
break;
|
||||
}
|
||||
|
||||
case NEU_RESP_GET_NODE_PROPERTIES: {
|
||||
neu_resp_get_node_properties_t resp =
|
||||
*(neu_resp_get_node_properties_t *) &header[1];
|
||||
reply(manager, header, &resp);
|
||||
break;
|
||||
}
|
||||
case NEU_RESP_ADD_TAG:
|
||||
case NEU_RESP_ADD_GTAG:
|
||||
case NEU_RESP_IMPORT_TAGS:
|
||||
@@ -1923,6 +1995,10 @@ inline static void reply(neu_manager_t *manager, neu_reqresp_head_t *header,
|
||||
} else {
|
||||
nlog_warn("reply %s to %s, error: %d", neu_reqresp_type_string(t),
|
||||
receiver, ret);
|
||||
if (t == NEU_RESP_GET_NODE_PROPERTIES) {
|
||||
neu_resp_get_node_properties_fini(
|
||||
(neu_resp_get_node_properties_t *) neu_msg_get_body(msg));
|
||||
}
|
||||
neu_msg_free(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "json/json.h"
|
||||
|
||||
#include "neu_json_node.h"
|
||||
#include "utils/set.h"
|
||||
|
||||
int neu_json_encode_add_node_req(void *json_object, void *param)
|
||||
{
|
||||
@@ -748,4 +749,212 @@ void neu_json_decode_update_node_tag_req_free(
|
||||
free(req->tags);
|
||||
free(req);
|
||||
}
|
||||
}
|
||||
|
||||
static char *property_string(json_t *value, size_t max_length)
|
||||
{
|
||||
if (!json_is_string(value)) {
|
||||
return NULL;
|
||||
}
|
||||
size_t length = json_string_length(value);
|
||||
const char *str = json_string_value(value);
|
||||
if (length == 0 || length >= max_length || strlen(str) != length) {
|
||||
return NULL;
|
||||
}
|
||||
return strdup(str);
|
||||
}
|
||||
|
||||
static int decode_property(json_t *object, neu_node_property_t *property)
|
||||
{
|
||||
property->key = property_string(json_object_get(object, "key"), SIZE_MAX);
|
||||
json_t *value = json_object_get(object, "value");
|
||||
if (property->key == NULL || !neu_node_property_value_valid(value)) {
|
||||
return -1;
|
||||
}
|
||||
property->value_json = neu_node_property_json_dump(value);
|
||||
return property->value_json == NULL ? -1 : 0;
|
||||
}
|
||||
|
||||
int neu_json_decode_add_node_properties_req(
|
||||
char *buf, neu_json_add_node_properties_req_t **result)
|
||||
{
|
||||
*result = NULL;
|
||||
json_t *object =
|
||||
json_loads(buf, JSON_REJECT_DUPLICATES | JSON_ALLOW_NUL, NULL);
|
||||
neu_json_add_node_properties_req_t *req = calloc(1, sizeof(*req));
|
||||
neu_strset_t seen = NULL;
|
||||
int ret = -1;
|
||||
if (req == NULL || !json_is_object(object)) {
|
||||
goto end;
|
||||
}
|
||||
req->node =
|
||||
property_string(json_object_get(object, "node"), NEU_NODE_NAME_LEN);
|
||||
json_t *array = json_object_get(object, "properties");
|
||||
if (req->node == NULL || !json_is_array(array) ||
|
||||
json_array_size(array) == 0) {
|
||||
goto end;
|
||||
}
|
||||
size_t count = json_array_size(array);
|
||||
req->properties = calloc(count, sizeof(*req->properties));
|
||||
if (req->properties == NULL) {
|
||||
goto end;
|
||||
}
|
||||
req->n_property = count;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (decode_property(json_array_get(array, i), &req->properties[i]) !=
|
||||
0 ||
|
||||
neu_strset_add(&seen, req->properties[i].key) != 1) {
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
*result = req;
|
||||
ret = 0;
|
||||
end:
|
||||
neu_strset_free(&seen);
|
||||
json_decref(object);
|
||||
if (ret != 0) {
|
||||
neu_json_decode_add_node_properties_req_free(req);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void neu_json_decode_add_node_properties_req_free(
|
||||
neu_json_add_node_properties_req_t *req)
|
||||
{
|
||||
if (req != NULL) {
|
||||
free(req->node);
|
||||
neu_node_properties_free(req->properties, req->n_property);
|
||||
free(req);
|
||||
}
|
||||
}
|
||||
|
||||
int neu_json_decode_delete_node_properties_req(
|
||||
char *buf, neu_json_delete_node_properties_req_t **result)
|
||||
{
|
||||
*result = NULL;
|
||||
json_t *object =
|
||||
json_loads(buf, JSON_REJECT_DUPLICATES | JSON_ALLOW_NUL, NULL);
|
||||
neu_json_delete_node_properties_req_t *req = calloc(1, sizeof(*req));
|
||||
neu_strset_t seen = NULL;
|
||||
int ret = -1;
|
||||
if (req == NULL || !json_is_object(object)) {
|
||||
goto end;
|
||||
}
|
||||
req->node =
|
||||
property_string(json_object_get(object, "node"), NEU_NODE_NAME_LEN);
|
||||
json_t *array = json_object_get(object, "keys");
|
||||
if (req->node == NULL || !json_is_array(array) ||
|
||||
json_array_size(array) == 0) {
|
||||
goto end;
|
||||
}
|
||||
size_t count = json_array_size(array);
|
||||
req->keys = calloc(count, sizeof(*req->keys));
|
||||
if (req->keys == NULL) {
|
||||
goto end;
|
||||
}
|
||||
req->n_key = count;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
req->keys[i] = property_string(json_array_get(array, i), SIZE_MAX);
|
||||
if (req->keys[i] == NULL || neu_strset_add(&seen, req->keys[i]) != 1) {
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
*result = req;
|
||||
ret = 0;
|
||||
end:
|
||||
neu_strset_free(&seen);
|
||||
json_decref(object);
|
||||
if (ret != 0) {
|
||||
neu_json_decode_delete_node_properties_req_free(req);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void neu_json_decode_delete_node_properties_req_free(
|
||||
neu_json_delete_node_properties_req_t *req)
|
||||
{
|
||||
if (req != NULL) {
|
||||
free(req->node);
|
||||
for (size_t i = 0; i < req->n_key; ++i) {
|
||||
free(req->keys[i]);
|
||||
}
|
||||
free(req->keys);
|
||||
free(req);
|
||||
}
|
||||
}
|
||||
|
||||
int neu_json_decode_update_node_property_req(
|
||||
char *buf, neu_json_update_node_property_req_t **result)
|
||||
{
|
||||
*result = NULL;
|
||||
json_t *object =
|
||||
json_loads(buf, JSON_REJECT_DUPLICATES | JSON_ALLOW_NUL, NULL);
|
||||
neu_json_update_node_property_req_t *req = calloc(1, sizeof(*req));
|
||||
if (req == NULL || !json_is_object(object)) {
|
||||
goto error;
|
||||
}
|
||||
req->node =
|
||||
property_string(json_object_get(object, "node"), NEU_NODE_NAME_LEN);
|
||||
if (req->node == NULL || decode_property(object, &req->property) != 0) {
|
||||
goto error;
|
||||
}
|
||||
json_decref(object);
|
||||
*result = req;
|
||||
return 0;
|
||||
error:
|
||||
json_decref(object);
|
||||
neu_json_decode_update_node_property_req_free(req);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void neu_json_decode_update_node_property_req_free(
|
||||
neu_json_update_node_property_req_t *req)
|
||||
{
|
||||
if (req != NULL) {
|
||||
free(req->node);
|
||||
neu_node_property_fini(&req->property);
|
||||
free(req);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
encode_node_properties_resp(json_t * json_object,
|
||||
neu_json_get_node_properties_resp_t *resp)
|
||||
{
|
||||
json_t *array = json_array();
|
||||
if (array == NULL) {
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < resp->n_property; ++i) {
|
||||
neu_node_property_t *property = &resp->properties[i];
|
||||
json_t * value = json_loads(property->value_json,
|
||||
JSON_DECODE_ANY | JSON_ALLOW_NUL, NULL);
|
||||
if (!neu_node_property_value_valid(value)) {
|
||||
json_decref(value);
|
||||
json_decref(array);
|
||||
return -1;
|
||||
}
|
||||
json_t *item =
|
||||
json_pack("{s:s,s:o}", "key", property->key, "value", value);
|
||||
if (item == NULL || json_array_append_new(array, item) != 0) {
|
||||
json_decref(array);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return json_object_set_new(json_object, "properties", array);
|
||||
}
|
||||
|
||||
int neu_json_encode_get_node_properties_resp(
|
||||
neu_json_get_node_properties_resp_t *resp, char **result)
|
||||
{
|
||||
*result = NULL;
|
||||
json_t *object = json_object();
|
||||
if (object == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (encode_node_properties_resp(object, resp) == 0) {
|
||||
*result = neu_node_property_json_dump(object);
|
||||
}
|
||||
json_decref(object);
|
||||
return *result == NULL ? -1 : 0;
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
#define _NEU_JSON_API_NEU_JSON_NODE_H_
|
||||
|
||||
#include "define.h"
|
||||
#include "property.h"
|
||||
#include "json/json.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -149,6 +150,43 @@ typedef struct {
|
||||
|
||||
int neu_json_encode_get_node_setting_resp(void *json_object, void *param);
|
||||
|
||||
typedef struct {
|
||||
char * node;
|
||||
size_t n_property;
|
||||
neu_node_property_t *properties;
|
||||
} neu_json_add_node_properties_req_t;
|
||||
|
||||
typedef struct {
|
||||
char * node;
|
||||
size_t n_key;
|
||||
char **keys;
|
||||
} neu_json_delete_node_properties_req_t;
|
||||
|
||||
typedef struct {
|
||||
char * node;
|
||||
neu_node_property_t property;
|
||||
} neu_json_update_node_property_req_t;
|
||||
|
||||
typedef struct {
|
||||
size_t n_property;
|
||||
neu_node_property_t *properties;
|
||||
} neu_json_get_node_properties_resp_t;
|
||||
|
||||
int neu_json_decode_add_node_properties_req(
|
||||
char *buf, neu_json_add_node_properties_req_t **result);
|
||||
void neu_json_decode_add_node_properties_req_free(
|
||||
neu_json_add_node_properties_req_t *req);
|
||||
int neu_json_decode_delete_node_properties_req(
|
||||
char *buf, neu_json_delete_node_properties_req_t **result);
|
||||
void neu_json_decode_delete_node_properties_req_free(
|
||||
neu_json_delete_node_properties_req_t *req);
|
||||
int neu_json_decode_update_node_property_req(
|
||||
char *buf, neu_json_update_node_property_req_t **result);
|
||||
void neu_json_decode_update_node_property_req_free(
|
||||
neu_json_update_node_property_req_t *req);
|
||||
int neu_json_encode_get_node_properties_resp(
|
||||
neu_json_get_node_properties_resp_t *resp, char **result);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -360,6 +360,34 @@ int neu_persister_update_node_state(const char *node_name, int state)
|
||||
return g_impl->vtbl->update_node_state(g_impl, node_name, state);
|
||||
}
|
||||
|
||||
int neu_persister_add_node_properties(const char * node,
|
||||
const neu_node_property_t *properties,
|
||||
size_t n_property)
|
||||
{
|
||||
return g_impl->vtbl->add_node_properties(g_impl, node, properties,
|
||||
n_property);
|
||||
}
|
||||
|
||||
int neu_persister_delete_node_properties(const char * node,
|
||||
const char *const *keys, size_t n_key)
|
||||
{
|
||||
return g_impl->vtbl->delete_node_properties(g_impl, node, keys, n_key);
|
||||
}
|
||||
|
||||
int neu_persister_update_node_property(const char * node,
|
||||
const neu_node_property_t *property)
|
||||
{
|
||||
return g_impl->vtbl->update_node_property(g_impl, node, property);
|
||||
}
|
||||
|
||||
int neu_persister_load_node_properties(const char * node,
|
||||
neu_node_property_t **properties,
|
||||
size_t * n_property)
|
||||
{
|
||||
return g_impl->vtbl->load_node_properties(g_impl, node, properties,
|
||||
n_property);
|
||||
}
|
||||
|
||||
int neu_persister_store_tag(const char *driver_name, const char *group_name,
|
||||
const neu_datatag_t *tag)
|
||||
{
|
||||
|
||||
@@ -85,6 +85,17 @@ struct neu_persister_vtbl_s {
|
||||
int (*update_node_state)(neu_persister_t *self, const char *node_name,
|
||||
int state);
|
||||
|
||||
int (*add_node_properties)(neu_persister_t *self, const char *node,
|
||||
const neu_node_property_t *properties,
|
||||
size_t n_property);
|
||||
int (*delete_node_properties)(neu_persister_t *self, const char *node,
|
||||
const char *const *keys, size_t n_key);
|
||||
int (*update_node_property)(neu_persister_t *self, const char *node,
|
||||
const neu_node_property_t *property);
|
||||
int (*load_node_properties)(neu_persister_t *self, const char *node,
|
||||
neu_node_property_t **properties,
|
||||
size_t * n_property);
|
||||
|
||||
/**
|
||||
* Persist node setting.
|
||||
* @param adapter_name name of the adapter who owns the
|
||||
|
||||
+315
-9
@@ -29,6 +29,7 @@
|
||||
|
||||
#include "errcodes.h"
|
||||
#include "utils/log.h"
|
||||
#include "utils/set.h"
|
||||
|
||||
#include "sqlite.h"
|
||||
|
||||
@@ -376,9 +377,18 @@ static int apply_schemas(sqlite3 *db, const char *dir)
|
||||
static inline int open_db(const char *schema_dir, sqlite3 **db_p)
|
||||
{
|
||||
sqlite3 *db = NULL;
|
||||
int rv = sqlite3_open(DB_FILE, &db);
|
||||
int rv = sqlite3_open_v2(DB_FILE, &db,
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
|
||||
SQLITE_OPEN_FULLMUTEX,
|
||||
NULL);
|
||||
if (SQLITE_OK != rv) {
|
||||
nlog_fatal("db `%s` fail: %s", DB_FILE, sqlite3_errstr(rv));
|
||||
sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
if (NULL == sqlite3_db_mutex(db)) {
|
||||
nlog_fatal("db mutex unavailable");
|
||||
sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
sqlite3_busy_timeout(db, 100 * 1000);
|
||||
@@ -416,6 +426,10 @@ static struct neu_persister_vtbl_s g_sqlite_persister_vtbl = {
|
||||
.delete_node = neu_sqlite_persister_delete_node,
|
||||
.update_node = neu_sqlite_persister_update_node,
|
||||
.update_node_state = neu_sqlite_persister_update_node_state,
|
||||
.add_node_properties = neu_sqlite_persister_add_node_properties,
|
||||
.delete_node_properties = neu_sqlite_persister_delete_node_properties,
|
||||
.update_node_property = neu_sqlite_persister_update_node_property,
|
||||
.load_node_properties = neu_sqlite_persister_load_node_properties,
|
||||
.store_tag = neu_sqlite_persister_store_tag,
|
||||
.store_tags = neu_sqlite_persister_store_tags,
|
||||
.load_tags = neu_sqlite_persister_load_tags,
|
||||
@@ -612,6 +626,276 @@ int neu_sqlite_persister_update_node_state(neu_persister_t *self,
|
||||
node_name);
|
||||
}
|
||||
|
||||
static int begin_transaction(sqlite3 *db)
|
||||
{
|
||||
if (!sqlite3_get_autocommit(db) ||
|
||||
SQLITE_OK != sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL)) {
|
||||
nlog_error("begin transaction fail: %s", sqlite3_errmsg(db));
|
||||
return NEU_ERR_EINTERNAL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void rollback_transaction(sqlite3 *db)
|
||||
{
|
||||
if (!sqlite3_get_autocommit(db) &&
|
||||
SQLITE_OK != sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL)) {
|
||||
nlog_error("rollback transaction fail: %s", sqlite3_errmsg(db));
|
||||
}
|
||||
}
|
||||
|
||||
static int node_property_json(const char *text, char **normalized)
|
||||
{
|
||||
*normalized = NULL;
|
||||
if (NULL == text) {
|
||||
return NEU_ERR_PARAM_IS_WRONG;
|
||||
}
|
||||
|
||||
json_error_t error;
|
||||
json_t *value = json_loads(text, JSON_DECODE_ANY | JSON_ALLOW_NUL, &error);
|
||||
if (NULL == value) {
|
||||
return json_error_code(&error) == json_error_out_of_memory
|
||||
? NEU_ERR_EINTERNAL
|
||||
: NEU_ERR_PARAM_IS_WRONG;
|
||||
}
|
||||
if (!neu_node_property_value_valid(value)) {
|
||||
json_decref(value);
|
||||
return NEU_ERR_PARAM_IS_WRONG;
|
||||
}
|
||||
|
||||
*normalized = neu_node_property_json_dump(value);
|
||||
json_decref(value);
|
||||
return *normalized ? 0 : NEU_ERR_EINTERNAL;
|
||||
}
|
||||
|
||||
enum node_property_operation {
|
||||
NODE_PROPERTY_ADD,
|
||||
NODE_PROPERTY_DELETE,
|
||||
NODE_PROPERTY_UPDATE,
|
||||
};
|
||||
|
||||
static int write_node_properties(neu_persister_t *self, const char *node,
|
||||
const neu_node_property_t *properties,
|
||||
const char *const *keys, size_t n,
|
||||
enum node_property_operation operation)
|
||||
{
|
||||
if (NULL == self || NULL == node || '\0' == *node || 0 == n ||
|
||||
(NODE_PROPERTY_DELETE == operation ? NULL == keys
|
||||
: NULL == properties)) {
|
||||
return NEU_ERR_PARAM_IS_WRONG;
|
||||
}
|
||||
|
||||
sqlite3 * db = ((neu_sqlite_persister_t *) self)->db;
|
||||
sqlite3_mutex *mutex = db ? sqlite3_db_mutex(db) : NULL;
|
||||
if (NULL == mutex) {
|
||||
return NEU_ERR_EINTERNAL;
|
||||
}
|
||||
sqlite3_mutex_enter(mutex);
|
||||
|
||||
int rv = NEU_ERR_EINTERNAL;
|
||||
bool begun = false;
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
char * value_json = NULL;
|
||||
neu_strset_t seen = NULL;
|
||||
const char * query = NODE_PROPERTY_ADD == operation
|
||||
? "INSERT INTO node_properties(node_name, key, value_json) VALUES(?1, "
|
||||
"?2, ?3)"
|
||||
: NODE_PROPERTY_DELETE == operation
|
||||
? "DELETE FROM node_properties WHERE node_name=?1 AND key=?2"
|
||||
: "UPDATE node_properties SET value_json=?3 WHERE node_name=?1 AND "
|
||||
"key=?2";
|
||||
|
||||
if (0 != begin_transaction(db)) {
|
||||
goto end;
|
||||
}
|
||||
begun = true;
|
||||
if (SQLITE_OK != sqlite3_prepare_v2(db, query, -1, &stmt, NULL) ||
|
||||
SQLITE_OK != sqlite3_bind_text(stmt, 1, node, -1, SQLITE_TRANSIENT)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const char *key =
|
||||
NODE_PROPERTY_DELETE == operation ? keys[i] : properties[i].key;
|
||||
if (NULL == key || '\0' == *key) {
|
||||
rv = NEU_ERR_PARAM_IS_WRONG;
|
||||
goto end;
|
||||
}
|
||||
if (NODE_PROPERTY_DELETE == operation) {
|
||||
int added = neu_strset_add(&seen, key);
|
||||
if (1 != added) {
|
||||
rv = added == 0 ? NEU_ERR_PARAM_IS_WRONG : NEU_ERR_EINTERNAL;
|
||||
goto end;
|
||||
}
|
||||
} else {
|
||||
rv = node_property_json(properties[i].value_json, &value_json);
|
||||
if (0 != rv) {
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
rv = NEU_ERR_EINTERNAL;
|
||||
if (SQLITE_OK != sqlite3_reset(stmt) ||
|
||||
SQLITE_OK !=
|
||||
sqlite3_bind_text(stmt, 2, key, -1, SQLITE_TRANSIENT) ||
|
||||
(NODE_PROPERTY_DELETE != operation &&
|
||||
SQLITE_OK !=
|
||||
sqlite3_bind_text(stmt, 3, value_json, -1,
|
||||
SQLITE_TRANSIENT))) {
|
||||
goto end;
|
||||
}
|
||||
free(value_json);
|
||||
value_json = NULL;
|
||||
|
||||
if (SQLITE_DONE != sqlite3_step(stmt)) {
|
||||
int code = sqlite3_extended_errcode(db);
|
||||
if (NODE_PROPERTY_ADD == operation &&
|
||||
(SQLITE_CONSTRAINT_PRIMARYKEY == code ||
|
||||
SQLITE_CONSTRAINT_UNIQUE == code)) {
|
||||
rv = NEU_ERR_NODE_PROPERTY_EXIST;
|
||||
}
|
||||
goto end;
|
||||
}
|
||||
if (NODE_PROPERTY_UPDATE == operation && 0 == sqlite3_changes(db)) {
|
||||
rv = NEU_ERR_NODE_PROPERTY_NOT_EXIST;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
rv = sqlite3_finalize(stmt);
|
||||
stmt = NULL;
|
||||
if (SQLITE_OK != rv ||
|
||||
SQLITE_OK != sqlite3_exec(db, "COMMIT", NULL, NULL, NULL)) {
|
||||
rv = NEU_ERR_EINTERNAL;
|
||||
goto end;
|
||||
}
|
||||
begun = false;
|
||||
rv = 0;
|
||||
|
||||
end:
|
||||
neu_strset_free(&seen);
|
||||
free(value_json);
|
||||
sqlite3_finalize(stmt);
|
||||
if (begun) {
|
||||
rollback_transaction(db);
|
||||
}
|
||||
sqlite3_mutex_leave(mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
int neu_sqlite_persister_add_node_properties(
|
||||
neu_persister_t *self, const char *node,
|
||||
const neu_node_property_t *properties, size_t n_property)
|
||||
{
|
||||
return write_node_properties(self, node, properties, NULL, n_property,
|
||||
NODE_PROPERTY_ADD);
|
||||
}
|
||||
|
||||
int neu_sqlite_persister_delete_node_properties(neu_persister_t * self,
|
||||
const char * node,
|
||||
const char *const *keys,
|
||||
size_t n_key)
|
||||
{
|
||||
return write_node_properties(self, node, NULL, keys, n_key,
|
||||
NODE_PROPERTY_DELETE);
|
||||
}
|
||||
|
||||
int neu_sqlite_persister_update_node_property(
|
||||
neu_persister_t *self, const char *node,
|
||||
const neu_node_property_t *property)
|
||||
{
|
||||
return write_node_properties(self, node, property, NULL, 1,
|
||||
NODE_PROPERTY_UPDATE);
|
||||
}
|
||||
|
||||
int neu_sqlite_persister_load_node_properties(neu_persister_t * self,
|
||||
const char * node,
|
||||
neu_node_property_t **properties,
|
||||
size_t * n_property)
|
||||
{
|
||||
if (properties) {
|
||||
*properties = NULL;
|
||||
}
|
||||
if (n_property) {
|
||||
*n_property = 0;
|
||||
}
|
||||
if (NULL == self || NULL == node || '\0' == *node || NULL == properties ||
|
||||
NULL == n_property) {
|
||||
return NEU_ERR_PARAM_IS_WRONG;
|
||||
}
|
||||
|
||||
sqlite3 * db = ((neu_sqlite_persister_t *) self)->db;
|
||||
sqlite3_mutex *mutex = db ? sqlite3_db_mutex(db) : NULL;
|
||||
if (NULL == mutex) {
|
||||
return NEU_ERR_EINTERNAL;
|
||||
}
|
||||
sqlite3_mutex_enter(mutex);
|
||||
|
||||
int rv = NEU_ERR_EINTERNAL;
|
||||
sqlite3_stmt * stmt = NULL;
|
||||
neu_node_property_t *items = NULL;
|
||||
size_t n = 0;
|
||||
size_t capacity = 0;
|
||||
const char * query = "SELECT key, value_json FROM node_properties "
|
||||
"WHERE node_name=?1 ORDER BY key";
|
||||
if (!sqlite3_get_autocommit(db) ||
|
||||
SQLITE_OK != sqlite3_prepare_v2(db, query, -1, &stmt, NULL) ||
|
||||
SQLITE_OK != sqlite3_bind_text(stmt, 1, node, -1, SQLITE_TRANSIENT)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
int step;
|
||||
while (SQLITE_ROW == (step = sqlite3_step(stmt))) {
|
||||
const char *key = (const char *) sqlite3_column_text(stmt, 0);
|
||||
const char *text = (const char *) sqlite3_column_text(stmt, 1);
|
||||
if (NULL == key || '\0' == *key || NULL == text ||
|
||||
strlen(key) != (size_t) sqlite3_column_bytes(stmt, 0) ||
|
||||
strlen(text) != (size_t) sqlite3_column_bytes(stmt, 1)) {
|
||||
goto end;
|
||||
}
|
||||
if (n == capacity) {
|
||||
if (capacity > SIZE_MAX / 2 / sizeof(*items)) {
|
||||
goto end;
|
||||
}
|
||||
size_t next = capacity ? capacity * 2 : 8;
|
||||
neu_node_property_t *grown = realloc(items, next * sizeof(*items));
|
||||
if (NULL == grown) {
|
||||
goto end;
|
||||
}
|
||||
items = grown;
|
||||
capacity = next;
|
||||
}
|
||||
neu_node_property_t *item = &items[n++];
|
||||
*item = (neu_node_property_t) { 0 };
|
||||
item->key = strdup(key);
|
||||
if (NULL == item->key ||
|
||||
0 != node_property_json(text, &item->value_json)) {
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
if (SQLITE_DONE != step) {
|
||||
goto end;
|
||||
}
|
||||
rv = sqlite3_finalize(stmt);
|
||||
stmt = NULL;
|
||||
if (SQLITE_OK != rv) {
|
||||
rv = NEU_ERR_EINTERNAL;
|
||||
goto end;
|
||||
}
|
||||
|
||||
*properties = items;
|
||||
*n_property = n;
|
||||
items = NULL;
|
||||
n = 0;
|
||||
rv = 0;
|
||||
|
||||
end:
|
||||
sqlite3_finalize(stmt);
|
||||
neu_node_properties_free(items, n);
|
||||
sqlite3_mutex_leave(mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
int neu_sqlite_persister_store_tag(neu_persister_t * self,
|
||||
const char * driver_name,
|
||||
const char * group_name,
|
||||
@@ -646,7 +930,10 @@ static int put_tags(sqlite3 *db, const char *query, sqlite3_stmt *stmt,
|
||||
neu_tag_format_str(tag, format_buf, sizeof(format_buf));
|
||||
}
|
||||
|
||||
sqlite3_reset(stmt);
|
||||
if (SQLITE_OK != sqlite3_reset(stmt)) {
|
||||
nlog_error("reset `%s` fail: %s", query, sqlite3_errmsg(db));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (SQLITE_OK != sqlite3_bind_text(stmt, 3, tag->name, -1, NULL)) {
|
||||
nlog_error("bind `%s` with name=`%s` fail: %s", query, tag->name,
|
||||
@@ -703,7 +990,8 @@ static int put_tags(sqlite3 *db, const char *query, sqlite3_stmt *stmt,
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (SQLITE_OK != sqlite3_bind_text(stmt, 12, format_buf, -1, NULL)) {
|
||||
if (SQLITE_OK !=
|
||||
sqlite3_bind_text(stmt, 12, format_buf, -1, SQLITE_TRANSIENT)) {
|
||||
nlog_error("bind `%s` with format=`%s` fail: %s", query, format_buf,
|
||||
sqlite3_errmsg(db));
|
||||
return -1;
|
||||
@@ -738,10 +1026,18 @@ int neu_sqlite_persister_store_tags(neu_persister_t * self,
|
||||
" precision, type, decimal, bias, description, value, format, unit"
|
||||
") VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)";
|
||||
|
||||
if (SQLITE_OK != sqlite3_exec(persister->db, "BEGIN", NULL, NULL, NULL)) {
|
||||
nlog_error("begin transaction fail: %s", sqlite3_errmsg(persister->db));
|
||||
sqlite3_mutex *mutex = sqlite3_db_mutex(persister->db);
|
||||
if (NULL == mutex) {
|
||||
return NEU_ERR_EINTERNAL;
|
||||
}
|
||||
sqlite3_mutex_enter(mutex);
|
||||
bool begun = false;
|
||||
int rv = NEU_ERR_EINTERNAL;
|
||||
|
||||
if (0 != begin_transaction(persister->db)) {
|
||||
goto end;
|
||||
}
|
||||
begun = true;
|
||||
|
||||
if (SQLITE_OK !=
|
||||
sqlite3_prepare_v2(persister->db, query, -1, &stmt, NULL)) {
|
||||
@@ -766,20 +1062,30 @@ int neu_sqlite_persister_store_tags(neu_persister_t * self,
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (SQLITE_OK != sqlite3_finalize(stmt)) {
|
||||
stmt = NULL;
|
||||
goto error;
|
||||
}
|
||||
stmt = NULL;
|
||||
if (SQLITE_OK != sqlite3_exec(persister->db, "COMMIT", NULL, NULL, NULL)) {
|
||||
nlog_error("commit transaction fail: %s",
|
||||
sqlite3_errmsg(persister->db));
|
||||
goto error;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return 0;
|
||||
begun = false;
|
||||
rv = 0;
|
||||
goto end;
|
||||
|
||||
error:
|
||||
nlog_warn("rollback transaction");
|
||||
sqlite3_exec(persister->db, "ROLLBACK", NULL, NULL, NULL);
|
||||
end:
|
||||
sqlite3_finalize(stmt);
|
||||
return NEU_ERR_EINTERNAL;
|
||||
if (begun) {
|
||||
rollback_transaction(persister->db);
|
||||
}
|
||||
sqlite3_mutex_leave(mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
static int collect_tag_info(sqlite3_stmt *stmt, UT_array **tags)
|
||||
|
||||
@@ -50,6 +50,21 @@ int neu_sqlite_persister_update_node_tags(neu_persister_t *self,
|
||||
const char * tags);
|
||||
int neu_sqlite_persister_update_node_state(neu_persister_t *self,
|
||||
const char *node_name, int state);
|
||||
int neu_sqlite_persister_add_node_properties(
|
||||
neu_persister_t *self, const char *node,
|
||||
const neu_node_property_t *properties, size_t n_property);
|
||||
int neu_sqlite_persister_delete_node_properties(neu_persister_t * self,
|
||||
const char * node,
|
||||
const char *const *keys,
|
||||
size_t n_key);
|
||||
int neu_sqlite_persister_update_node_property(
|
||||
neu_persister_t *self, const char *node,
|
||||
const neu_node_property_t *property);
|
||||
int neu_sqlite_persister_load_node_properties(neu_persister_t * self,
|
||||
const char * node,
|
||||
neu_node_property_t **properties,
|
||||
size_t * n_property);
|
||||
|
||||
int neu_sqlite_persister_store_tag(neu_persister_t * self,
|
||||
const char * driver_name,
|
||||
const char * group_name,
|
||||
|
||||
@@ -364,6 +364,7 @@ int neu_http_response(nng_aio *aio, neu_err_code_e code, char *content)
|
||||
case NEU_ERR_FILE_READ_FAILURE:
|
||||
case NEU_ERR_LIBRARY_NOT_FOUND:
|
||||
case NEU_ERR_NODE_NOT_EXIST:
|
||||
case NEU_ERR_NODE_PROPERTY_NOT_EXIST:
|
||||
case NEU_ERR_TAG_NOT_EXIST:
|
||||
case NEU_ERR_LICENSE_NOT_FOUND:
|
||||
case NEU_ERR_LICENSE_TOKEN_NOT_FOUND:
|
||||
@@ -377,6 +378,7 @@ int neu_http_response(nng_aio *aio, neu_err_code_e code, char *content)
|
||||
status = NNG_HTTP_STATUS_NOT_FOUND;
|
||||
break;
|
||||
case NEU_ERR_NODE_EXIST:
|
||||
case NEU_ERR_NODE_PROPERTY_EXIST:
|
||||
case NEU_ERR_NODE_NOT_READY:
|
||||
case NEU_ERR_NODE_IS_RUNNING:
|
||||
case NEU_ERR_NODE_NOT_RUNNING:
|
||||
|
||||
@@ -107,6 +107,25 @@ def get_nodes_by_tags(type, tags, jwt=config.default_jwt):
|
||||
return requests.get(url=config.BASE_URL + '/api/v2/node', headers={"Authorization": jwt}, params={"type": type, "node": tags})
|
||||
|
||||
|
||||
def get_node_properties(node, jwt=config.default_jwt):
|
||||
return requests.get(url=config.BASE_URL + '/api/v2/node/properties', headers={"Authorization": jwt}, params={"node": node})
|
||||
|
||||
|
||||
@gen_check
|
||||
def add_node_properties(node, properties, jwt=config.default_jwt):
|
||||
return requests.post(url=config.BASE_URL + '/api/v2/node/properties', headers={"Authorization": jwt}, json={"node": node, "properties": properties})
|
||||
|
||||
|
||||
@gen_check
|
||||
def update_node_property(node, key, value, jwt=config.default_jwt):
|
||||
return requests.put(url=config.BASE_URL + '/api/v2/node/properties', headers={"Authorization": jwt}, json={"node": node, "key": key, "value": value})
|
||||
|
||||
|
||||
@gen_check
|
||||
def delete_node_properties(node, keys, jwt=config.default_jwt):
|
||||
return requests.delete(url=config.BASE_URL + '/api/v2/node/properties', headers={"Authorization": jwt}, json={"node": node, "keys": keys})
|
||||
|
||||
|
||||
@gen_check
|
||||
def node_ctl(node, ctl, jwt=config.default_jwt):
|
||||
return requests.post(url=config.BASE_URL + '/api/v2/node/ctl', headers={"Authorization": jwt}, json={"node": node, "cmd": ctl})
|
||||
|
||||
@@ -36,6 +36,8 @@ NEU_ERR_NODE_NAME_EMPTY = 2015
|
||||
NEU_ERR_NODE_TAGS_TOO_LONG = 2016
|
||||
NEU_ERR_NODE_TAGS_TOO_MANY = 2017
|
||||
NEU_ERR_NODE_TAGS_INVALID = 2018
|
||||
NEU_ERR_NODE_PROPERTY_EXIST = 2019
|
||||
NEU_ERR_NODE_PROPERTY_NOT_EXIST = 2020
|
||||
|
||||
NEU_ERR_GROUP_ALREADY_SUBSCRIBED = 2101
|
||||
NEU_ERR_GROUP_NOT_SUBSCRIBE = 2102
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import math
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
import neuron.api as api
|
||||
import neuron.config as config
|
||||
from neuron.common import class_setup_and_teardown, case_time
|
||||
from neuron.error import (
|
||||
NEU_ERR_BODY_IS_WRONG,
|
||||
NEU_ERR_NEED_TOKEN,
|
||||
NEU_ERR_NODE_NOT_EXIST,
|
||||
NEU_ERR_NODE_PROPERTY_EXIST,
|
||||
NEU_ERR_NODE_PROPERTY_NOT_EXIST,
|
||||
NEU_ERR_PARAM_IS_WRONG,
|
||||
)
|
||||
|
||||
|
||||
VALUES = {
|
||||
"string": "hello",
|
||||
"empty-string": "",
|
||||
"true": True,
|
||||
"false": False,
|
||||
"zero": 0,
|
||||
"integer-min": -(2 ** 63),
|
||||
"integer-max": 2 ** 63 - 1,
|
||||
"large-integer": 2 ** 53 + 1,
|
||||
"float": 1.25,
|
||||
"float-zero": 0.0,
|
||||
"negative-zero": -0.0,
|
||||
"float-max": 1.7976931348623157e308,
|
||||
"float-min": 5e-324,
|
||||
"strings": ["", "text", "中文"],
|
||||
"booleans": [False, True],
|
||||
"integers": [-(2 ** 63), 0, 2 ** 63 - 1],
|
||||
"floats": [0.0, -1.5, 2.5],
|
||||
"numbers": [0, 1.5, -2, 0.0],
|
||||
"empty-array": [],
|
||||
"转义\"\\\n": "中文\"\\\n\r\t\b\f\u0000end",
|
||||
"nul-array": ["\u0000", "a\u0000b"],
|
||||
}
|
||||
|
||||
|
||||
def assert_error(response, status, code):
|
||||
assert response.status_code == status, response.text
|
||||
assert response.json() == {"error": code}
|
||||
|
||||
|
||||
def assert_value(actual, expected):
|
||||
assert type(actual) is type(expected)
|
||||
assert actual == expected
|
||||
if isinstance(expected, list):
|
||||
for actual_item, expected_item in zip(actual, expected):
|
||||
assert_value(actual_item, expected_item)
|
||||
elif isinstance(expected, float) and expected == 0:
|
||||
assert math.copysign(1, actual) == math.copysign(1, expected)
|
||||
|
||||
|
||||
def assert_properties(node, expected):
|
||||
response = api.get_node_properties(node)
|
||||
assert response.status_code == 200, response.text
|
||||
assert set(response.json()) == {"properties"}
|
||||
properties = response.json()["properties"]
|
||||
assert len(properties) == len(expected)
|
||||
actual = {}
|
||||
for item in properties:
|
||||
assert set(item) == {"key", "value"}
|
||||
assert item["key"] not in actual
|
||||
actual[item["key"]] = item["value"]
|
||||
assert actual.keys() == expected.keys()
|
||||
for key, value in expected.items():
|
||||
assert_value(actual[key], value)
|
||||
|
||||
|
||||
@pytest.fixture(params=[config.PLUGIN_MODBUS_TCP, config.PLUGIN_MQTT],
|
||||
ids=["southbound", "northbound"])
|
||||
def property_node(request, class_setup_and_teardown):
|
||||
node = "property-driver" if request.param == config.PLUGIN_MODBUS_TCP else "property-app"
|
||||
api.add_node_check(node, request.param)
|
||||
state = {"name": node, "plugin": request.param}
|
||||
try:
|
||||
yield state
|
||||
finally:
|
||||
api.del_node_check(state["name"])
|
||||
|
||||
|
||||
class TestNodeProperty:
|
||||
def test_crud_and_value_types(self, property_node):
|
||||
node = property_node["name"]
|
||||
assert_properties(node, {})
|
||||
api.add_node_properties_check(
|
||||
node, [{"key": key, "value": value} for key, value in VALUES.items()])
|
||||
assert_properties(node, VALUES)
|
||||
|
||||
for value in VALUES.values():
|
||||
api.update_node_property_check(node, "string", value)
|
||||
assert_properties(node, dict(VALUES, string=value))
|
||||
api.update_node_property_check(node, "string", VALUES["string"])
|
||||
|
||||
api.delete_node_properties_check(node, ["false", "zero", "absent"])
|
||||
remaining = {key: value for key, value in VALUES.items()
|
||||
if key not in ("false", "zero")}
|
||||
assert_properties(node, remaining)
|
||||
api.delete_node_properties_check(node, ["absent"])
|
||||
assert_properties(node, remaining)
|
||||
api.delete_node_properties_check(node, list(remaining))
|
||||
assert_properties(node, {})
|
||||
api.add_node_properties_check(node, [{"key": "zero", "value": 0}])
|
||||
assert_properties(node, {"zero": 0})
|
||||
|
||||
@pytest.mark.parametrize("conflict_first", [False, True])
|
||||
def test_post_conflict_is_atomic(self, property_node, conflict_first):
|
||||
node = property_node["name"]
|
||||
api.add_node_properties_check(node, [{"key": "existing", "value": False}])
|
||||
properties = [{"key": "new", "value": 0},
|
||||
{"key": "existing", "value": "replacement"}]
|
||||
if conflict_first:
|
||||
properties.reverse()
|
||||
assert_error(api.add_node_properties(node, properties),
|
||||
409, NEU_ERR_NODE_PROPERTY_EXIST)
|
||||
assert_properties(node, {"existing": False})
|
||||
assert_error(api.update_node_property(node, "missing", 0),
|
||||
404, NEU_ERR_NODE_PROPERTY_NOT_EXIST)
|
||||
assert_properties(node, {"existing": False})
|
||||
|
||||
def test_missing_node(self, property_node):
|
||||
node = property_node["name"] + "-missing"
|
||||
responses = [
|
||||
api.get_node_properties(node),
|
||||
api.add_node_properties(node, [{"key": "key", "value": 0}]),
|
||||
api.update_node_property(node, "key", False),
|
||||
api.delete_node_properties(node, ["missing"]),
|
||||
]
|
||||
for response in responses:
|
||||
assert_error(response, 404, NEU_ERR_NODE_NOT_EXIST)
|
||||
|
||||
def test_rename_delete_and_recreate(self, property_node):
|
||||
old_name = property_node["name"]
|
||||
values = {"false": False, "zero": 0, "empty": [], "escaped": "a\u0000b"}
|
||||
api.add_node_properties_check(
|
||||
old_name, [{"key": key, "value": value} for key, value in values.items()])
|
||||
new_name = old_name + "-renamed"
|
||||
api.update_node_check(old_name, new_name)
|
||||
property_node["name"] = new_name
|
||||
assert_error(api.get_node_properties(old_name), 404, NEU_ERR_NODE_NOT_EXIST)
|
||||
assert_properties(new_name, values)
|
||||
api.update_node_property_check(new_name, "false", True)
|
||||
values["false"] = True
|
||||
api.delete_node_properties_check(new_name, ["zero"])
|
||||
del values["zero"]
|
||||
assert_properties(new_name, values)
|
||||
|
||||
api.add_node_check(old_name, property_node["plugin"])
|
||||
try:
|
||||
assert_properties(old_name, {})
|
||||
finally:
|
||||
api.del_node_check(old_name)
|
||||
api.del_node_check(new_name)
|
||||
assert_error(api.get_node_properties(new_name), 404, NEU_ERR_NODE_NOT_EXIST)
|
||||
api.add_node_check(new_name, property_node["plugin"])
|
||||
assert_properties(new_name, {})
|
||||
api.add_node_properties_check(new_name, [{"key": "false", "value": False}])
|
||||
assert_properties(new_name, {"false": False})
|
||||
|
||||
def test_restart_persistence(self, property_node, class_setup_and_teardown):
|
||||
node = property_node["name"]
|
||||
api.add_node_properties_check(
|
||||
node, [{"key": key, "value": value} for key, value in VALUES.items()])
|
||||
api.update_node_property_check(node, "string", False)
|
||||
api.delete_node_properties_check(node, ["zero", "absent"])
|
||||
new_name = node + "-persisted"
|
||||
api.update_node_check(node, new_name)
|
||||
property_node["name"] = new_name
|
||||
class_setup_and_teardown.restart()
|
||||
expected = dict(VALUES, string=False)
|
||||
del expected["zero"]
|
||||
assert_properties(new_name, expected)
|
||||
assert_error(api.get_node_properties(node), 404, NEU_ERR_NODE_NOT_EXIST)
|
||||
|
||||
api.del_node_check(new_name)
|
||||
api.add_node_check(new_name, property_node["plugin"])
|
||||
class_setup_and_teardown.restart()
|
||||
assert_properties(new_name, {})
|
||||
|
||||
@pytest.mark.parametrize("value", [None, {}, {"a": 1}, [[1]], [None], [{}],
|
||||
[1, "1"], [True, 1], [False, 0.0],
|
||||
["true", True], ["", []]])
|
||||
def test_invalid_value_is_atomic(self, property_node, value):
|
||||
node = property_node["name"]
|
||||
api.add_node_properties_check(node, [{"key": "existing", "value": False}])
|
||||
assert_error(api.add_node_properties(node, [
|
||||
{"key": "new", "value": 0}, {"key": "bad", "value": value}]),
|
||||
400, NEU_ERR_BODY_IS_WRONG)
|
||||
assert_error(api.update_node_property(node, "existing", value),
|
||||
400, NEU_ERR_BODY_IS_WRONG)
|
||||
assert_properties(node, {"existing": False})
|
||||
|
||||
@pytest.mark.parametrize("method,fields", [
|
||||
("POST", {}),
|
||||
("POST", {"properties": []}),
|
||||
("POST", {"properties": None}),
|
||||
("POST", {"properties": {}}),
|
||||
("POST", {"properties": [None]}),
|
||||
("POST", {"properties": [{"value": 1}]}),
|
||||
("POST", {"properties": [{"key": "", "value": 1}]}),
|
||||
("POST", {"properties": [{"key": "bad\u0000key", "value": 1}]}),
|
||||
("POST", {"properties": [{"key": 1, "value": 1}]}),
|
||||
("POST", {"properties": [{"key": "bad"}]}),
|
||||
("POST", {"properties": [{"key": "new", "value": 1},
|
||||
{"key": "new", "value": 2}]}),
|
||||
("PUT", {}),
|
||||
("PUT", {"key": "existing"}),
|
||||
("PUT", {"value": False}),
|
||||
("PUT", {"key": "", "value": False}),
|
||||
("PUT", {"key": "bad\u0000key", "value": False}),
|
||||
("PUT", {"key": 1, "value": False}),
|
||||
("DELETE", {}),
|
||||
("DELETE", {"keys": []}),
|
||||
("DELETE", {"keys": None}),
|
||||
("DELETE", {"keys": "existing"}),
|
||||
("DELETE", {"keys": ["existing", "existing"]}),
|
||||
("DELETE", {"keys": ["existing", ""]}),
|
||||
("DELETE", {"keys": ["existing", "bad\u0000key"]}),
|
||||
("DELETE", {"keys": ["existing", None]}),
|
||||
("DELETE", {"keys": ["existing", 1]}),
|
||||
])
|
||||
def test_invalid_request_is_atomic(self, property_node, method, fields):
|
||||
node = property_node["name"]
|
||||
api.add_node_properties_check(node, [{"key": "existing", "value": False}])
|
||||
body = dict(fields, node=node)
|
||||
if (method == "POST" and isinstance(body.get("properties"), list)
|
||||
and body["properties"]):
|
||||
body["properties"] = [{"key": "valid", "value": 0}] + body["properties"]
|
||||
response = requests.request(
|
||||
method, config.BASE_URL + "/api/v2/node/properties",
|
||||
headers={"Authorization": config.default_jwt}, json=body)
|
||||
assert_error(response, 400, NEU_ERR_BODY_IS_WRONG)
|
||||
assert_properties(node, {"existing": False})
|
||||
|
||||
@pytest.mark.parametrize("node", [None, "", 1, "bad\u0000node"])
|
||||
def test_invalid_node(self, property_node, node):
|
||||
for response in [
|
||||
api.add_node_properties(node, [{"key": "key", "value": 0}]),
|
||||
api.update_node_property(node, "key", False),
|
||||
api.delete_node_properties(node, ["key"]),
|
||||
]:
|
||||
assert_error(response, 400, NEU_ERR_BODY_IS_WRONG)
|
||||
assert_properties(property_node["name"], {})
|
||||
|
||||
@pytest.mark.parametrize("params", [{}, {"node": ""}, {"node": "bad\u0000node"}])
|
||||
def test_invalid_query(self, property_node, params):
|
||||
response = requests.get(
|
||||
config.BASE_URL + "/api/v2/node/properties",
|
||||
headers={"Authorization": config.default_jwt}, params=params)
|
||||
assert_error(response, 400, NEU_ERR_PARAM_IS_WRONG)
|
||||
assert_properties(property_node["name"], {})
|
||||
|
||||
@pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE"])
|
||||
def test_jwt_required(self, property_node, method):
|
||||
node = property_node["name"]
|
||||
body = {"node": node, "properties": [{"key": "key", "value": 0}],
|
||||
"key": "key", "value": False, "keys": ["key"]}
|
||||
response = requests.request(
|
||||
method, config.BASE_URL + "/api/v2/node/properties",
|
||||
params={"node": node} if method == "GET" else None,
|
||||
json=body if method != "GET" else None)
|
||||
assert_error(response, 401, NEU_ERR_NEED_TOKEN)
|
||||
assert_properties(node, {})
|
||||
@@ -77,6 +77,14 @@ target_include_directories(common_test PRIVATE
|
||||
)
|
||||
target_link_libraries(common_test neuron-base gtest_main gtest)
|
||||
|
||||
add_executable(cache_test cache_test.cc
|
||||
${CMAKE_SOURCE_DIR}/src/adapter/driver/cache.c)
|
||||
target_include_directories(cache_test PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
target_link_libraries(cache_test gtest_main gtest pthread jansson m)
|
||||
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/tests/ut/serverBMS_3_test.cid DESTINATION ${UT_DIRECTORY}/config)
|
||||
add_executable(cid_test cid_test.cc)
|
||||
target_include_directories(cid_test PRIVATE
|
||||
@@ -101,6 +109,16 @@ target_include_directories(ede_test PRIVATE
|
||||
)
|
||||
target_link_libraries(ede_test neuron-base gtest_main gtest)
|
||||
|
||||
add_executable(node_property_persist_test node_property_persist_test.cc)
|
||||
target_include_directories(node_property_persist_test PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
target_compile_definitions(node_property_persist_test PRIVATE
|
||||
NEU_PROPERTY_SCHEMA_DIR="${CMAKE_SOURCE_DIR}/persistence"
|
||||
)
|
||||
target_link_libraries(node_property_persist_test neuron-base gtest_main gtest pthread jansson sqlite3)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(json_test)
|
||||
gtest_discover_tests(http_test)
|
||||
@@ -112,6 +130,8 @@ gtest_discover_tests(async_queue_test)
|
||||
gtest_discover_tests(rolling_counter_test)
|
||||
gtest_discover_tests(mqtt_client_test)
|
||||
gtest_discover_tests(common_test)
|
||||
gtest_discover_tests(cache_test)
|
||||
gtest_discover_tests(cid_test)
|
||||
gtest_discover_tests(mqtt_schema_test)
|
||||
gtest_discover_tests(ede_test)
|
||||
gtest_discover_tests(node_property_persist_test)
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
extern "C" {
|
||||
#include "adapter/driver/cache.h"
|
||||
|
||||
bool sub_filter_err = false;
|
||||
}
|
||||
|
||||
class DriverCacheSample : public testing::Test {
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
cache = neu_driver_cache_new();
|
||||
neu_dvalue_t initial = {};
|
||||
initial.type = NEU_TYPE_ERROR;
|
||||
neu_driver_cache_add(cache, "group", "tag", initial);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
neu_driver_cache_destroy(cache);
|
||||
sub_filter_err = false;
|
||||
}
|
||||
|
||||
neu_dvalue_t sample()
|
||||
{
|
||||
neu_dvalue_t value = {};
|
||||
value.type = NEU_TYPE_DOUBLE;
|
||||
value.value.d64 = 1.25;
|
||||
value.attrs.has_timestamp = true;
|
||||
value.attrs.timestamp = 100;
|
||||
value.attrs.has_quality = true;
|
||||
value.attrs.quality = 192;
|
||||
return value;
|
||||
}
|
||||
|
||||
bool update(neu_dvalue_t value)
|
||||
{
|
||||
return neu_driver_cache_update_change(cache, "group", "tag", 200, value,
|
||||
NULL, 0, false);
|
||||
}
|
||||
|
||||
int changed()
|
||||
{
|
||||
return neu_driver_cache_meta_get_changed(cache, "group", "tag", &out,
|
||||
&metas, &n_meta);
|
||||
}
|
||||
|
||||
neu_driver_cache_t * cache = NULL;
|
||||
neu_driver_cache_value_t out = {};
|
||||
neu_tag_meta_t * metas = NULL;
|
||||
int n_meta = 0;
|
||||
};
|
||||
|
||||
TEST_F(DriverCacheSample, existing_update_preserves_optional_attrs)
|
||||
{
|
||||
for (int flags = 0; flags < 4; ++flags) {
|
||||
neu_dvalue_t value = sample();
|
||||
value.attrs.has_timestamp = flags & 1;
|
||||
value.attrs.has_quality = flags & 2;
|
||||
neu_driver_cache_update(cache, "group", "tag", 200, value, NULL, 0);
|
||||
|
||||
ASSERT_EQ(0,
|
||||
neu_driver_cache_meta_get(cache, "group", "tag", &out, &metas,
|
||||
&n_meta));
|
||||
EXPECT_EQ(200, out.timestamp);
|
||||
EXPECT_EQ(value.attrs.has_timestamp, out.value.attrs.has_timestamp);
|
||||
EXPECT_EQ(value.attrs.has_quality, out.value.attrs.has_quality);
|
||||
EXPECT_EQ(flags & 1 ? 100 : 0, out.value.attrs.timestamp);
|
||||
EXPECT_EQ(flags & 2 ? 192 : 0, out.value.attrs.quality);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DriverCacheSample, timestamp_alone_does_not_trigger_change)
|
||||
{
|
||||
neu_dvalue_t value = sample();
|
||||
ASSERT_TRUE(update(value));
|
||||
ASSERT_EQ(0, changed());
|
||||
|
||||
value.attrs.timestamp++;
|
||||
EXPECT_FALSE(update(value));
|
||||
EXPECT_NE(0, changed());
|
||||
ASSERT_EQ(0,
|
||||
neu_driver_cache_meta_get(cache, "group", "tag", &out, &metas,
|
||||
&n_meta));
|
||||
EXPECT_EQ(value.attrs.timestamp, out.value.attrs.timestamp);
|
||||
|
||||
value.attrs.has_timestamp = false;
|
||||
EXPECT_FALSE(update(value));
|
||||
EXPECT_NE(0, changed());
|
||||
}
|
||||
|
||||
TEST_F(DriverCacheSample, quality_change_stays_pending_until_read)
|
||||
{
|
||||
for (neu_type_e type : { NEU_TYPE_FLOAT, NEU_TYPE_DOUBLE }) {
|
||||
neu_dvalue_t value = sample();
|
||||
value.type = type;
|
||||
if (type == NEU_TYPE_FLOAT) {
|
||||
value.value.f32 = 1.25f;
|
||||
}
|
||||
ASSERT_TRUE(update(value));
|
||||
ASSERT_EQ(0, changed());
|
||||
|
||||
value.attrs.quality = 0;
|
||||
EXPECT_TRUE(update(value));
|
||||
value.attrs.timestamp++;
|
||||
EXPECT_FALSE(update(value));
|
||||
ASSERT_EQ(0, changed());
|
||||
EXPECT_TRUE(out.value.attrs.has_quality);
|
||||
EXPECT_EQ(0, out.value.attrs.quality);
|
||||
EXPECT_NE(0, changed());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DriverCacheSample, absent_attrs_clear_previous_sample)
|
||||
{
|
||||
neu_dvalue_t value = sample();
|
||||
ASSERT_TRUE(update(value));
|
||||
ASSERT_EQ(0, changed());
|
||||
|
||||
value.attrs = {};
|
||||
EXPECT_TRUE(update(value));
|
||||
ASSERT_EQ(0, changed());
|
||||
EXPECT_FALSE(out.value.attrs.has_timestamp);
|
||||
EXPECT_FALSE(out.value.attrs.has_quality);
|
||||
EXPECT_EQ(0, out.value.attrs.timestamp);
|
||||
EXPECT_EQ(0, out.value.attrs.quality);
|
||||
EXPECT_FALSE(update(value));
|
||||
EXPECT_NE(0, changed());
|
||||
}
|
||||
|
||||
TEST_F(DriverCacheSample, forced_update_preserves_attrs)
|
||||
{
|
||||
neu_dvalue_t value = sample();
|
||||
ASSERT_TRUE(update(value));
|
||||
ASSERT_EQ(0, changed());
|
||||
|
||||
neu_driver_cache_update_change(cache, "group", "tag", 300, value, NULL, 0,
|
||||
true);
|
||||
ASSERT_EQ(0, changed());
|
||||
EXPECT_EQ(300, out.timestamp);
|
||||
EXPECT_EQ(value.attrs.timestamp, out.value.attrs.timestamp);
|
||||
EXPECT_EQ(value.attrs.quality, out.value.attrs.quality);
|
||||
}
|
||||
|
||||
TEST_F(DriverCacheSample, filtered_error_compares_last_good_quality)
|
||||
{
|
||||
sub_filter_err = true;
|
||||
neu_dvalue_t value = sample();
|
||||
ASSERT_TRUE(update(value));
|
||||
ASSERT_EQ(0, changed());
|
||||
|
||||
neu_dvalue_t error = {};
|
||||
error.type = NEU_TYPE_ERROR;
|
||||
error.value.i32 = 1;
|
||||
EXPECT_FALSE(update(error));
|
||||
EXPECT_NE(0, changed());
|
||||
EXPECT_FALSE(update(value));
|
||||
EXPECT_NE(0, changed());
|
||||
|
||||
EXPECT_FALSE(update(error));
|
||||
value.attrs.quality = 0;
|
||||
EXPECT_TRUE(update(value));
|
||||
ASSERT_EQ(0, changed());
|
||||
EXPECT_TRUE(out.value.attrs.has_quality);
|
||||
EXPECT_EQ(0, out.value.attrs.quality);
|
||||
}
|
||||
|
||||
TEST_F(DriverCacheSample, copy_dvalue_preserves_attrs)
|
||||
{
|
||||
neu_dvalue_t src = sample();
|
||||
neu_dvalue_t dst = {};
|
||||
neu_copy_dvalue(&src, &dst);
|
||||
EXPECT_EQ(src.attrs.has_timestamp, dst.attrs.has_timestamp);
|
||||
EXPECT_EQ(src.attrs.timestamp, dst.attrs.timestamp);
|
||||
EXPECT_EQ(src.attrs.has_quality, dst.attrs.has_quality);
|
||||
EXPECT_EQ(src.attrs.quality, dst.attrs.quality);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
#include <cmath>
|
||||
#include <gtest/gtest.h>
|
||||
#include <jansson.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "parser/neu_json_node.h"
|
||||
#include "utils/log.h"
|
||||
#include "json/json.h"
|
||||
|
||||
@@ -328,6 +333,309 @@ TEST(JsonTest, EncodeArray)
|
||||
neu_json_decode_free(ob);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using PropertyJson = std::unique_ptr<json_t, decltype(&json_decref)>;
|
||||
|
||||
const char *property_values[] = {
|
||||
R"("text")",
|
||||
R"("")",
|
||||
"true",
|
||||
"false",
|
||||
"0",
|
||||
"-1",
|
||||
"-9223372036854775808",
|
||||
"9223372036854775807",
|
||||
"9007199254740993",
|
||||
"1.25",
|
||||
"0.0",
|
||||
"-0.0",
|
||||
"1.7976931348623157e308",
|
||||
"2.2250738585072014e-308",
|
||||
"5e-324",
|
||||
R"(["", "中文", "a\"b", "a\u0000b"])",
|
||||
"[true, false]",
|
||||
"[-9223372036854775808, 0, 9223372036854775807]",
|
||||
"[0.0, -1.5, 2.5]",
|
||||
"[0, 1.5, -2, 0.0]",
|
||||
"[]",
|
||||
R"("中文\"\\\n\r\t\b\f\u0000end")",
|
||||
};
|
||||
|
||||
void expect_property_json(const char *expected_text, const char *actual_text)
|
||||
{
|
||||
PropertyJson expected(
|
||||
json_loads(expected_text, JSON_DECODE_ANY | JSON_ALLOW_NUL, NULL),
|
||||
json_decref);
|
||||
PropertyJson actual(
|
||||
json_loads(actual_text, JSON_DECODE_ANY | JSON_ALLOW_NUL, NULL),
|
||||
json_decref);
|
||||
ASSERT_NE(nullptr, expected.get());
|
||||
ASSERT_NE(nullptr, actual.get());
|
||||
EXPECT_EQ(json_typeof(expected.get()), json_typeof(actual.get()));
|
||||
EXPECT_TRUE(json_equal(expected.get(), actual.get()));
|
||||
if (json_is_real(expected.get()) && json_real_value(expected.get()) == 0) {
|
||||
EXPECT_EQ(std::signbit(json_real_value(expected.get())),
|
||||
std::signbit(json_real_value(actual.get())));
|
||||
}
|
||||
}
|
||||
|
||||
void expect_property_response(neu_node_property_t *properties, size_t count)
|
||||
{
|
||||
neu_json_get_node_properties_resp_t response = { count, properties };
|
||||
char * encoded = NULL;
|
||||
ASSERT_EQ(0, neu_json_encode_get_node_properties_resp(&response, &encoded));
|
||||
std::unique_ptr<char, decltype(&free)> buffer(encoded, free);
|
||||
ASSERT_NE(nullptr, buffer.get());
|
||||
PropertyJson decoded(json_loads(buffer.get(), JSON_ALLOW_NUL, NULL),
|
||||
json_decref);
|
||||
ASSERT_NE(nullptr, decoded.get());
|
||||
EXPECT_EQ(1, json_object_size(decoded.get()));
|
||||
json_t *array = json_object_get(decoded.get(), "properties");
|
||||
ASSERT_TRUE(json_is_array(array));
|
||||
ASSERT_EQ(count, json_array_size(array));
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
json_t *item = json_array_get(array, i);
|
||||
EXPECT_EQ(2, json_object_size(item));
|
||||
ASSERT_TRUE(json_is_string(json_object_get(item, "key")));
|
||||
EXPECT_STREQ(properties[i].key,
|
||||
json_string_value(json_object_get(item, "key")));
|
||||
char *value =
|
||||
neu_node_property_json_dump(json_object_get(item, "value"));
|
||||
ASSERT_NE(nullptr, value);
|
||||
expect_property_json(properties[i].value_json, value);
|
||||
free(value);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(JsonTest, NodePropertiesAddAndResponsePreserveTypes)
|
||||
{
|
||||
const size_t count = sizeof(property_values) / sizeof(property_values[0]);
|
||||
std::string input = R"({"node":"driver","properties":[)";
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (i != 0) {
|
||||
input += ",";
|
||||
}
|
||||
input += "{\"key\":\"key" + std::to_string(i) +
|
||||
"\",\"value\":" + property_values[i] + "}";
|
||||
}
|
||||
input += "]}";
|
||||
neu_json_add_node_properties_req_t *raw = NULL;
|
||||
ASSERT_EQ(0,
|
||||
neu_json_decode_add_node_properties_req(
|
||||
const_cast<char *>(input.c_str()), &raw));
|
||||
std::unique_ptr<neu_json_add_node_properties_req_t,
|
||||
decltype(&neu_json_decode_add_node_properties_req_free)>
|
||||
request(raw, neu_json_decode_add_node_properties_req_free);
|
||||
ASSERT_NE(nullptr, request.get());
|
||||
EXPECT_STREQ("driver", request->node);
|
||||
ASSERT_EQ(count, request->n_property);
|
||||
for (size_t i = 0; i < request->n_property; ++i) {
|
||||
SCOPED_TRACE(property_values[i]);
|
||||
EXPECT_EQ("key" + std::to_string(i), request->properties[i].key);
|
||||
expect_property_json(property_values[i],
|
||||
request->properties[i].value_json);
|
||||
}
|
||||
expect_property_response(request->properties, request->n_property);
|
||||
}
|
||||
|
||||
TEST(JsonTest, NodePropertyUpdateAndResponsePreserveTypes)
|
||||
{
|
||||
for (const char *value : property_values) {
|
||||
SCOPED_TRACE(value);
|
||||
std::string input = R"({"node":"app","key":"转义\"\\\n","value":)";
|
||||
input += value;
|
||||
input += "}";
|
||||
neu_json_update_node_property_req_t *raw = NULL;
|
||||
ASSERT_EQ(0,
|
||||
neu_json_decode_update_node_property_req(
|
||||
const_cast<char *>(input.c_str()), &raw));
|
||||
std::unique_ptr<neu_json_update_node_property_req_t,
|
||||
decltype(
|
||||
&neu_json_decode_update_node_property_req_free)>
|
||||
request(raw, neu_json_decode_update_node_property_req_free);
|
||||
ASSERT_NE(nullptr, request.get());
|
||||
EXPECT_STREQ("app", request->node);
|
||||
EXPECT_STREQ("转义\"\\\n", request->property.key);
|
||||
expect_property_json(value, request->property.value_json);
|
||||
expect_property_response(&request->property, 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JsonTest, NodePropertiesEmptyResponse)
|
||||
{
|
||||
expect_property_response(NULL, 0);
|
||||
}
|
||||
|
||||
TEST(JsonTest, NodePropertiesDelete)
|
||||
{
|
||||
char input[] = R"({"node":"app","keys":["missing","转义\"\\\n"]})";
|
||||
neu_json_delete_node_properties_req_t *raw = NULL;
|
||||
ASSERT_EQ(0, neu_json_decode_delete_node_properties_req(input, &raw));
|
||||
std::unique_ptr<neu_json_delete_node_properties_req_t,
|
||||
decltype(&neu_json_decode_delete_node_properties_req_free)>
|
||||
request(raw, neu_json_decode_delete_node_properties_req_free);
|
||||
ASSERT_NE(nullptr, request.get());
|
||||
EXPECT_STREQ("app", request->node);
|
||||
ASSERT_EQ(2, request->n_key);
|
||||
EXPECT_STREQ("missing", request->keys[0]);
|
||||
EXPECT_STREQ("转义\"\\\n", request->keys[1]);
|
||||
}
|
||||
|
||||
TEST(JsonTest, NodePropertiesRejectInvalidValues)
|
||||
{
|
||||
const char *invalid[] = {
|
||||
"null",
|
||||
"{}",
|
||||
R"({"a":1})",
|
||||
"[[1]]",
|
||||
"[[]]",
|
||||
"[null]",
|
||||
"[{}]",
|
||||
R"([1,"1"])",
|
||||
"[true,1]",
|
||||
"[false,0.0]",
|
||||
R"(["true",true])",
|
||||
"9223372036854775808",
|
||||
"-9223372036854775809",
|
||||
"1e309",
|
||||
"NaN",
|
||||
"Infinity",
|
||||
};
|
||||
for (const char *value : invalid) {
|
||||
SCOPED_TRACE(value);
|
||||
std::string add =
|
||||
R"({"node":"driver","properties":[{"key":"valid","value":0},{"key":"bad","value":)";
|
||||
add += value;
|
||||
add += "}]}";
|
||||
neu_json_add_node_properties_req_t *add_req = NULL;
|
||||
EXPECT_NE(0,
|
||||
neu_json_decode_add_node_properties_req(
|
||||
const_cast<char *>(add.c_str()), &add_req));
|
||||
EXPECT_EQ(nullptr, add_req);
|
||||
neu_json_decode_add_node_properties_req_free(add_req);
|
||||
|
||||
std::string update = R"({"node":"app","key":"bad","value":)";
|
||||
update += value;
|
||||
update += "}";
|
||||
neu_json_update_node_property_req_t *update_req = NULL;
|
||||
EXPECT_NE(0,
|
||||
neu_json_decode_update_node_property_req(
|
||||
const_cast<char *>(update.c_str()), &update_req));
|
||||
EXPECT_EQ(nullptr, update_req);
|
||||
neu_json_decode_update_node_property_req_free(update_req);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JsonTest, NodePropertiesRejectInvalidAdd)
|
||||
{
|
||||
const char *invalid[] = {
|
||||
"",
|
||||
"{",
|
||||
"[]",
|
||||
"null",
|
||||
"{}",
|
||||
R"({"properties":[{"key":"a","value":0}]})",
|
||||
R"({"node":"","properties":[{"key":"a","value":0}]})",
|
||||
R"({"node":1,"properties":[{"key":"a","value":0}]})",
|
||||
R"({"node":null,"properties":[{"key":"a","value":0}]})",
|
||||
R"({"node":"a\u0000b","properties":[{"key":"a","value":0}]})",
|
||||
R"({"node":"driver"})",
|
||||
R"({"node":"driver","properties":[]})",
|
||||
R"({"node":"driver","properties":{}})",
|
||||
R"({"node":"driver","properties":null})",
|
||||
R"({"node":"driver","properties":[null]})",
|
||||
R"({"node":"driver","properties":[{"value":0}]})",
|
||||
R"({"node":"driver","properties":[{"key":"" ,"value":0}]})",
|
||||
R"({"node":"driver","properties":[{"key":false,"value":0}]})",
|
||||
R"({"node":"driver","properties":[{"key":"a\u0000b","value":0}]})",
|
||||
R"({"node":"driver","properties":[{"key":"a"}]})",
|
||||
R"({"node":"driver","properties":[{"key":"a","value":0},{"key":"a","value":1}]})",
|
||||
R"({"node":"driver","properties":[{"key":"a","key":"b","value":0}]})",
|
||||
R"({"node":"driver","properties":[{"key":"a","value":0,"value":1}]})",
|
||||
R"({"node":"driver","node":"app","properties":[{"key":"a","value":0}]})",
|
||||
};
|
||||
for (const char *input : invalid) {
|
||||
SCOPED_TRACE(input);
|
||||
neu_json_add_node_properties_req_t *request = NULL;
|
||||
EXPECT_NE(0,
|
||||
neu_json_decode_add_node_properties_req(
|
||||
const_cast<char *>(input), &request));
|
||||
EXPECT_EQ(nullptr, request);
|
||||
neu_json_decode_add_node_properties_req_free(request);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JsonTest, NodePropertiesRejectInvalidDelete)
|
||||
{
|
||||
const char *invalid[] = {
|
||||
"",
|
||||
"{",
|
||||
"[]",
|
||||
"null",
|
||||
"{}",
|
||||
R"({"keys":["a"]})",
|
||||
R"({"node":"","keys":["a"]})",
|
||||
R"({"node":null,"keys":["a"]})",
|
||||
R"({"node":1,"keys":["a"]})",
|
||||
R"({"node":"a\u0000b","keys":["a"]})",
|
||||
R"({"node":"app"})",
|
||||
R"({"node":"app","keys":[]})",
|
||||
R"({"node":"app","keys":null})",
|
||||
R"({"node":"app","keys":"a"})",
|
||||
R"({"node":"app","keys":["valid",""]})",
|
||||
R"({"node":"app","keys":["valid",null]})",
|
||||
R"({"node":"app","keys":["valid",1]})",
|
||||
R"({"node":"app","keys":["valid",[]]})",
|
||||
R"({"node":"app","keys":["valid",{}]})",
|
||||
R"({"node":"app","keys":["valid","a\u0000b"]})",
|
||||
R"({"node":"app","keys":["a","a"]})",
|
||||
R"({"node":"app","keys":["a"],"keys":["b"]})",
|
||||
};
|
||||
for (const char *input : invalid) {
|
||||
SCOPED_TRACE(input);
|
||||
neu_json_delete_node_properties_req_t *request = NULL;
|
||||
EXPECT_NE(0,
|
||||
neu_json_decode_delete_node_properties_req(
|
||||
const_cast<char *>(input), &request));
|
||||
EXPECT_EQ(nullptr, request);
|
||||
neu_json_decode_delete_node_properties_req_free(request);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JsonTest, NodePropertiesRejectInvalidUpdate)
|
||||
{
|
||||
const char *invalid[] = {
|
||||
"",
|
||||
"{",
|
||||
"[]",
|
||||
"null",
|
||||
"{}",
|
||||
R"({"key":"a","value":0})",
|
||||
R"({"node":"","key":"a","value":0})",
|
||||
R"({"node":null,"key":"a","value":0})",
|
||||
R"({"node":1,"key":"a","value":0})",
|
||||
R"({"node":"a\u0000b","key":"a","value":0})",
|
||||
R"({"node":"app","value":0})",
|
||||
R"({"node":"app","key":"a"})",
|
||||
R"({"node":"app","key":"","value":0})",
|
||||
R"({"node":"app","key":false,"value":0})",
|
||||
R"({"node":"app","key":"a\u0000b","value":0})",
|
||||
R"({"node":"app","key":"a","value":0,"value":1})",
|
||||
};
|
||||
for (const char *input : invalid) {
|
||||
SCOPED_TRACE(input);
|
||||
neu_json_update_node_property_req_t *request = NULL;
|
||||
EXPECT_NE(0,
|
||||
neu_json_decode_update_node_property_req(
|
||||
const_cast<char *>(input), &request));
|
||||
EXPECT_EQ(nullptr, request);
|
||||
neu_json_decode_update_node_property_req_free(request);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
zlog_init("./config/dev.conf");
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "errcodes.h"
|
||||
#include "persist/persist.h"
|
||||
#include "persist/sqlite.h"
|
||||
#include "utils/log.h"
|
||||
|
||||
zlog_category_t *neuron = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
neu_node_property_t property(const char *key, const char *value)
|
||||
{
|
||||
return { const_cast<char *>(key), const_cast<char *>(value) };
|
||||
}
|
||||
|
||||
struct Properties {
|
||||
neu_node_property_t *items = nullptr;
|
||||
size_t n = 0;
|
||||
|
||||
~Properties() { neu_node_properties_free(items, n); }
|
||||
};
|
||||
|
||||
class NodePropertyPersist : public testing::Test {
|
||||
protected:
|
||||
int cwd = -1;
|
||||
std::string directory;
|
||||
bool opened = false;
|
||||
sqlite3 * db = nullptr;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
cwd = open(".", O_RDONLY | O_DIRECTORY);
|
||||
ASSERT_GE(cwd, 0);
|
||||
char path[] = "/tmp/neuron-property-XXXXXX";
|
||||
ASSERT_NE(nullptr, mkdtemp(path));
|
||||
directory = path;
|
||||
ASSERT_EQ(0, mkdir((directory + "/persistence").c_str(), 0700));
|
||||
ASSERT_EQ(0, mkdir((directory + "/schemas").c_str(), 0700));
|
||||
std::filesystem::copy_file(
|
||||
std::string(NEU_PROPERTY_SCHEMA_DIR) +
|
||||
"/0160_2.16.1_node_properties.sql",
|
||||
directory + "/schemas/0160_2.16.1_node_properties.sql");
|
||||
ASSERT_EQ(0, chdir(directory.c_str()));
|
||||
ASSERT_EQ(SQLITE_OK,
|
||||
sqlite3_open_v2("persistence/sqlite.db", &db,
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
|
||||
SQLITE_OPEN_FULLMUTEX,
|
||||
nullptr));
|
||||
ASSERT_EQ(SQLITE_OK,
|
||||
sqlite3_exec(
|
||||
db,
|
||||
"CREATE TABLE nodes(name TEXT PRIMARY KEY, type INTEGER, "
|
||||
"state INTEGER, plugin_name TEXT, tags TEXT);"
|
||||
"CREATE TABLE settings(node_name TEXT PRIMARY KEY, "
|
||||
"setting TEXT, "
|
||||
"FOREIGN KEY(node_name) REFERENCES nodes(name) ON UPDATE "
|
||||
"CASCADE ON DELETE CASCADE);"
|
||||
"CREATE TABLE groups(driver_name TEXT, name TEXT, "
|
||||
"interval INTEGER, "
|
||||
"context TEXT, PRIMARY KEY(driver_name,name), "
|
||||
"FOREIGN KEY(driver_name) REFERENCES nodes(name) ON "
|
||||
"UPDATE CASCADE ON DELETE CASCADE);"
|
||||
"CREATE TABLE tags(driver_name TEXT, group_name TEXT, "
|
||||
"name TEXT, "
|
||||
"address TEXT, attribute INTEGER, precision INTEGER, "
|
||||
"type INTEGER, "
|
||||
"decimal REAL, bias REAL, description TEXT, value TEXT, "
|
||||
"format TEXT, unit TEXT, "
|
||||
"UNIQUE(driver_name,group_name,name), FOREIGN "
|
||||
"KEY(driver_name,group_name) "
|
||||
"REFERENCES groups(driver_name,name) ON UPDATE CASCADE "
|
||||
"ON DELETE CASCADE);",
|
||||
nullptr, nullptr, nullptr))
|
||||
<< sqlite3_errmsg(db);
|
||||
ASSERT_EQ(SQLITE_OK, sqlite3_close(db));
|
||||
db = nullptr;
|
||||
ASSERT_EQ(0, neu_persister_create("schemas"));
|
||||
opened = true;
|
||||
db = neu_persister_get_db();
|
||||
ASSERT_NE(nullptr, sqlite3_db_mutex(db));
|
||||
neu_persist_node_info_t driver = { const_cast<char *>("driver"), 1,
|
||||
const_cast<char *>("modbus"), 1,
|
||||
nullptr };
|
||||
neu_persist_node_info_t app = { const_cast<char *>("app"), 2,
|
||||
const_cast<char *>("mqtt"), 1,
|
||||
nullptr };
|
||||
ASSERT_EQ(0, neu_persister_store_node(&driver));
|
||||
ASSERT_EQ(0, neu_persister_store_node(&app));
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
if (opened) {
|
||||
neu_persister_destroy();
|
||||
} else if (db) {
|
||||
sqlite3_close(db);
|
||||
}
|
||||
if (cwd >= 0) {
|
||||
EXPECT_EQ(0, fchdir(cwd));
|
||||
close(cwd);
|
||||
}
|
||||
if (!directory.empty()) {
|
||||
std::filesystem::remove_all(directory);
|
||||
}
|
||||
}
|
||||
|
||||
void sql(const char *query)
|
||||
{
|
||||
ASSERT_EQ(SQLITE_OK, sqlite3_exec(db, query, nullptr, nullptr, nullptr))
|
||||
<< sqlite3_errmsg(db);
|
||||
}
|
||||
|
||||
int scalar(const char *query)
|
||||
{
|
||||
sqlite3_stmt *stmt = nullptr;
|
||||
if (SQLITE_OK != sqlite3_prepare_v2(db, query, -1, &stmt, nullptr)) {
|
||||
ADD_FAILURE() << sqlite3_errmsg(db);
|
||||
return -1;
|
||||
}
|
||||
int step = sqlite3_step(stmt);
|
||||
EXPECT_EQ(SQLITE_ROW, step);
|
||||
int value = step == SQLITE_ROW ? sqlite3_column_int(stmt, 0) : -1;
|
||||
EXPECT_EQ(SQLITE_OK, sqlite3_finalize(stmt));
|
||||
return value;
|
||||
}
|
||||
|
||||
void expect(const char * node,
|
||||
const std::vector<std::pair<std::string, std::string>> &values)
|
||||
{
|
||||
Properties loaded;
|
||||
ASSERT_EQ(
|
||||
0,
|
||||
neu_persister_load_node_properties(node, &loaded.items, &loaded.n));
|
||||
ASSERT_EQ(values.size(), loaded.n);
|
||||
if (values.empty()) {
|
||||
EXPECT_EQ(nullptr, loaded.items);
|
||||
}
|
||||
for (size_t i = 0; i < loaded.n; ++i) {
|
||||
EXPECT_EQ(values[i].first, loaded.items[i].key);
|
||||
EXPECT_EQ(values[i].second, loaded.items[i].value_json);
|
||||
}
|
||||
}
|
||||
|
||||
void group()
|
||||
{
|
||||
neu_persist_group_info_t info = { 1000, const_cast<char *>("group"),
|
||||
nullptr };
|
||||
ASSERT_EQ(0, neu_persister_store_group("driver", &info, nullptr));
|
||||
}
|
||||
|
||||
neu_datatag_t tag(const char *name)
|
||||
{
|
||||
neu_datatag_t value = {};
|
||||
value.name = const_cast<char *>(name);
|
||||
value.address = const_cast<char *>("1!400001");
|
||||
value.description = const_cast<char *>("");
|
||||
value.unit = const_cast<char *>("");
|
||||
value.type = NEU_TYPE_INT16;
|
||||
value.attribute = NEU_ATTRIBUTE_READ;
|
||||
value.decimal = 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
void concurrent_rollback(bool tags);
|
||||
};
|
||||
|
||||
TEST_F(NodePropertyPersist, CrudAndNodeIsolation)
|
||||
{
|
||||
expect("driver", {});
|
||||
neu_node_property_t values[] = { property("z", "true"),
|
||||
property("a", "1") };
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("driver", values, 2));
|
||||
auto app = property("a", "\"north\"");
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("app", &app, 1));
|
||||
expect("driver", { { "a", "1" }, { "z", "true" } });
|
||||
expect("app", { { "a", "\"north\"" } });
|
||||
|
||||
auto update = property("a", "[1,2.5]");
|
||||
ASSERT_EQ(0, neu_persister_update_node_property("driver", &update));
|
||||
ASSERT_EQ(0, neu_persister_update_node_property("driver", &update));
|
||||
auto missing = property("missing", "false");
|
||||
EXPECT_EQ(NEU_ERR_NODE_PROPERTY_NOT_EXIST,
|
||||
neu_persister_update_node_property("driver", &missing));
|
||||
expect("driver", { { "a", "[1,2.5]" }, { "z", "true" } });
|
||||
|
||||
const char *keys[] = { "a", "missing" };
|
||||
ASSERT_EQ(0, neu_persister_delete_node_properties("driver", keys, 2));
|
||||
ASSERT_EQ(0, neu_persister_delete_node_properties("driver", keys, 2));
|
||||
expect("driver", { { "z", "true" } });
|
||||
expect("app", { { "a", "\"north\"" } });
|
||||
const char *last[] = { "z" };
|
||||
ASSERT_EQ(0, neu_persister_delete_node_properties("driver", last, 1));
|
||||
expect("driver", {});
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, ValueTypesAndCanonicalRoundTrip)
|
||||
{
|
||||
const char * texts[] = { " \"text\\n\\\"中文\" ",
|
||||
"\"\\u0000\"",
|
||||
"-9223372036854775808",
|
||||
"9223372036854775807",
|
||||
" 1.25e2 ",
|
||||
"1.7976931348623157e308",
|
||||
"2.2250738585072014e-308",
|
||||
"5e-324",
|
||||
"[1.7976931348623157e308,5e-324]",
|
||||
"true",
|
||||
"false",
|
||||
"[]",
|
||||
"[\"a\",\"b\",\"\\u0000\"]",
|
||||
"[1, -2, 3.5]",
|
||||
"[1.0, 2]",
|
||||
"[true,false]" };
|
||||
std::vector<std::string> keys;
|
||||
std::vector<neu_node_property_t> values;
|
||||
for (size_t i = 0; i < std::size(texts); ++i) {
|
||||
keys.push_back("key" + std::to_string(100 + i));
|
||||
}
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
values.push_back(property(keys[i].c_str(), texts[i]));
|
||||
}
|
||||
ASSERT_EQ(0,
|
||||
neu_persister_add_node_properties("driver", values.data(),
|
||||
values.size()));
|
||||
Properties loaded;
|
||||
ASSERT_EQ(
|
||||
0,
|
||||
neu_persister_load_node_properties("driver", &loaded.items, &loaded.n));
|
||||
ASSERT_EQ(values.size(), loaded.n);
|
||||
for (size_t i = 0; i < loaded.n; ++i) {
|
||||
json_t *expected =
|
||||
json_loads(texts[i], JSON_DECODE_ANY | JSON_ALLOW_NUL, nullptr);
|
||||
json_t *actual = json_loads(loaded.items[i].value_json,
|
||||
JSON_DECODE_ANY | JSON_ALLOW_NUL, nullptr);
|
||||
ASSERT_NE(nullptr, expected);
|
||||
ASSERT_NE(nullptr, actual);
|
||||
EXPECT_TRUE(json_equal(expected, actual));
|
||||
char *canonical = neu_node_property_json_dump(expected);
|
||||
EXPECT_STREQ(canonical, loaded.items[i].value_json);
|
||||
free(canonical);
|
||||
json_decref(expected);
|
||||
json_decref(actual);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, ExistingAndBatchDuplicatesRollback)
|
||||
{
|
||||
auto seed = property("existing", "1");
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("driver", &seed, 1));
|
||||
neu_node_property_t existing[] = { property("new", "2"),
|
||||
property("existing", "3") };
|
||||
EXPECT_EQ(NEU_ERR_NODE_PROPERTY_EXIST,
|
||||
neu_persister_add_node_properties("driver", existing, 2));
|
||||
neu_node_property_t duplicate[] = { property("new", "2"),
|
||||
property("new", "2") };
|
||||
EXPECT_EQ(NEU_ERR_NODE_PROPERTY_EXIST,
|
||||
neu_persister_add_node_properties("driver", duplicate, 2));
|
||||
expect("driver", { { "existing", "1" } });
|
||||
EXPECT_TRUE(sqlite3_get_autocommit(db));
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, DeleteDuplicateAndInvalidKeysRollback)
|
||||
{
|
||||
neu_node_property_t values[] = { property("a", "1"), property("b", "2") };
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("driver", values, 2));
|
||||
const char *duplicate[] = { "a", "missing", "a" };
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_delete_node_properties("driver", duplicate, 3));
|
||||
const char *empty[] = { "a", "" };
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_delete_node_properties("driver", empty, 2));
|
||||
const char *null_key[] = { "a", nullptr };
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_delete_node_properties("driver", null_key, 2));
|
||||
const char *missing_duplicate[] = { "missing", "missing" };
|
||||
EXPECT_EQ(
|
||||
NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_delete_node_properties("driver", missing_duplicate, 2));
|
||||
expect("driver", { { "a", "1" }, { "b", "2" } });
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, RejectInvalidParameters)
|
||||
{
|
||||
auto value = property("key", "1");
|
||||
const char *keys[] = { "key" };
|
||||
for (const char *node : { static_cast<const char *>(nullptr), "" }) {
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_add_node_properties(node, &value, 1));
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_delete_node_properties(node, keys, 1));
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_update_node_property(node, &value));
|
||||
Properties loaded;
|
||||
EXPECT_EQ(
|
||||
NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_load_node_properties(node, &loaded.items, &loaded.n));
|
||||
EXPECT_EQ(nullptr, loaded.items);
|
||||
EXPECT_EQ(0u, loaded.n);
|
||||
}
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_add_node_properties("driver", nullptr, 1));
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_add_node_properties("driver", &value, 0));
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_delete_node_properties("driver", nullptr, 1));
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_delete_node_properties("driver", keys, 0));
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_update_node_property("driver", nullptr));
|
||||
for (const char *key : { static_cast<const char *>(nullptr), "" }) {
|
||||
auto invalid = property(key, "1");
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_add_node_properties("driver", &invalid, 1));
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_update_node_property("driver", &invalid));
|
||||
}
|
||||
size_t n = 10;
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_load_node_properties("driver", nullptr, &n));
|
||||
EXPECT_EQ(0u, n);
|
||||
auto *items = reinterpret_cast<neu_node_property_t *>(1);
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_load_node_properties("driver", &items, nullptr));
|
||||
EXPECT_EQ(nullptr, items);
|
||||
EXPECT_EQ(
|
||||
NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_sqlite_persister_add_node_properties(nullptr, "driver", &value, 1));
|
||||
expect("driver", {});
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, RejectInvalidValuesAndRollback)
|
||||
{
|
||||
const char *invalid[] = {
|
||||
nullptr, "", "null", "{}", "[{}]",
|
||||
"[[1]]", "[1,true]", "[false,0.5]", "[\"x\",1]", "[null]",
|
||||
"[1,]", "1 2", "NaN", "Infinity", "\"unfinished"
|
||||
};
|
||||
for (const char *text : invalid) {
|
||||
neu_node_property_t values[] = { property("first", "1"),
|
||||
property("invalid", text) };
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_add_node_properties("driver", values, 2))
|
||||
<< (text ? text : "null pointer");
|
||||
expect("driver", {});
|
||||
}
|
||||
auto value = property("key", "1");
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("driver", &value, 1));
|
||||
for (const char *text : invalid) {
|
||||
auto update = property("key", text);
|
||||
EXPECT_EQ(NEU_ERR_PARAM_IS_WRONG,
|
||||
neu_persister_update_node_property("driver", &update));
|
||||
}
|
||||
expect("driver", { { "key", "1" } });
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, BoundTextAndCascades)
|
||||
{
|
||||
neu_persist_node_info_t node = { const_cast<char *>("node'--"), 2,
|
||||
const_cast<char *>("mqtt"), 1, nullptr };
|
||||
ASSERT_EQ(0, neu_persister_store_node(&node));
|
||||
const char *key = "中文'; DROP TABLE nodes;--";
|
||||
auto value = property(key, "\"quote' and \\\"json\\\"\"");
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties(node.name, &value, 1));
|
||||
expect(node.name, { { key, value.value_json } });
|
||||
ASSERT_EQ(0, neu_persister_update_node(node.name, "renamed"));
|
||||
expect(node.name, {});
|
||||
expect("renamed", { { key, value.value_json } });
|
||||
ASSERT_EQ(0, neu_persister_delete_node("renamed"));
|
||||
expect("renamed", {});
|
||||
EXPECT_EQ(2, scalar("SELECT count(*) FROM nodes"));
|
||||
EXPECT_EQ(0, scalar("SELECT count(*) FROM node_properties"));
|
||||
EXPECT_NE(0, neu_persister_add_node_properties("nonexistent", &value, 1));
|
||||
EXPECT_TRUE(sqlite3_get_autocommit(db));
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, PersistenceReopenAndMigration)
|
||||
{
|
||||
auto value = property("key", "[true,false]");
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("driver", &value, 1));
|
||||
neu_persister_destroy();
|
||||
opened = false;
|
||||
db = nullptr;
|
||||
ASSERT_EQ(0, neu_persister_create("schemas"));
|
||||
opened = true;
|
||||
db = neu_persister_get_db();
|
||||
expect("driver", { { "key", "[true,false]" } });
|
||||
EXPECT_EQ(1,
|
||||
scalar("SELECT count(*) FROM migrations WHERE version='0160' AND "
|
||||
"dirty=0"));
|
||||
EXPECT_EQ(1, scalar("PRAGMA foreign_keys"));
|
||||
EXPECT_NE(nullptr, sqlite3_db_mutex(db));
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, FailedLoadClearsOutputs)
|
||||
{
|
||||
neu_node_property_t values[] = { property("a", "1"), property("b", "2") };
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("driver", values, 2));
|
||||
sql("UPDATE node_properties SET value_json='[1,true]' WHERE key='b'");
|
||||
auto * items = reinterpret_cast<neu_node_property_t *>(1);
|
||||
size_t n = 42;
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_load_node_properties("driver", &items, &n));
|
||||
EXPECT_EQ(nullptr, items);
|
||||
EXPECT_EQ(0u, n);
|
||||
sql("DROP TABLE node_properties");
|
||||
items = reinterpret_cast<neu_node_property_t *>(1);
|
||||
n = 42;
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_load_node_properties("driver", &items, &n));
|
||||
EXPECT_EQ(nullptr, items);
|
||||
EXPECT_EQ(0u, n);
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_add_node_properties("driver", values, 2));
|
||||
EXPECT_TRUE(sqlite3_get_autocommit(db));
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, ExistingTransactionIsNeverRolledBack)
|
||||
{
|
||||
group();
|
||||
sql("BEGIN");
|
||||
sql("UPDATE nodes SET state=2 WHERE name='driver'");
|
||||
auto value = property("key", "1");
|
||||
const char *keys[] = { "key" };
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_add_node_properties("driver", &value, 1));
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_delete_node_properties("driver", keys, 1));
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_update_node_property("driver", &value));
|
||||
Properties loaded;
|
||||
EXPECT_EQ(
|
||||
NEU_ERR_EINTERNAL,
|
||||
neu_persister_load_node_properties("driver", &loaded.items, &loaded.n));
|
||||
auto value_tag = tag("tag");
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_store_tags("driver", "group", &value_tag, 1));
|
||||
EXPECT_FALSE(sqlite3_get_autocommit(db));
|
||||
EXPECT_EQ(2, scalar("SELECT state FROM nodes WHERE name='driver'"));
|
||||
sql("ROLLBACK");
|
||||
EXPECT_EQ(1, scalar("SELECT state FROM nodes WHERE name='driver'"));
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, StepAndBindErrorsRollback)
|
||||
{
|
||||
neu_node_property_t values[] = { property("a", "1"), property("b", "2") };
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("driver", values, 2));
|
||||
sql("CREATE TRIGGER fail_delete BEFORE DELETE ON node_properties "
|
||||
"WHEN OLD.key='b' BEGIN SELECT RAISE(ABORT, 'fail'); END");
|
||||
const char *keys[] = { "a", "b" };
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_delete_node_properties("driver", keys, 2));
|
||||
expect("driver", { { "a", "1" }, { "b", "2" } });
|
||||
std::string large = "\"" + std::string(2048, 'x') + "\"";
|
||||
auto value = property("large", large.c_str());
|
||||
int old = sqlite3_limit(db, SQLITE_LIMIT_LENGTH, 1024);
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_add_node_properties("driver", &value, 1));
|
||||
sqlite3_limit(db, SQLITE_LIMIT_LENGTH, old);
|
||||
expect("driver", { { "a", "1" }, { "b", "2" } });
|
||||
EXPECT_TRUE(sqlite3_get_autocommit(db));
|
||||
}
|
||||
|
||||
int deny_commit(void *, int action, const char *arg, const char *, const char *,
|
||||
const char *)
|
||||
{
|
||||
return action == SQLITE_TRANSACTION && arg && strcmp(arg, "COMMIT") == 0
|
||||
? SQLITE_DENY
|
||||
: SQLITE_OK;
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, CommitErrorsRollback)
|
||||
{
|
||||
group();
|
||||
auto seed = property("a", "1");
|
||||
ASSERT_EQ(0, neu_persister_add_node_properties("driver", &seed, 1));
|
||||
ASSERT_EQ(SQLITE_OK, sqlite3_set_authorizer(db, deny_commit, nullptr));
|
||||
auto added = property("b", "2");
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_add_node_properties("driver", &added, 1));
|
||||
auto updated = property("a", "3");
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_update_node_property("driver", &updated));
|
||||
const char *keys[] = { "a" };
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_delete_node_properties("driver", keys, 1));
|
||||
auto value_tag = tag("tag");
|
||||
EXPECT_EQ(NEU_ERR_EINTERNAL,
|
||||
neu_persister_store_tags("driver", "group", &value_tag, 1));
|
||||
EXPECT_TRUE(sqlite3_get_autocommit(db));
|
||||
ASSERT_EQ(SQLITE_OK, sqlite3_set_authorizer(db, nullptr, nullptr));
|
||||
expect("driver", { { "a", "1" } });
|
||||
EXPECT_EQ(0, scalar("SELECT count(*) FROM tags"));
|
||||
}
|
||||
|
||||
struct TransactionGate {
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
bool paused = false;
|
||||
bool released = false;
|
||||
std::atomic<int> started { 0 };
|
||||
std::atomic<bool> ended { false };
|
||||
std::atomic<bool> interleaved { false };
|
||||
std::thread::id owner;
|
||||
std::string insert;
|
||||
};
|
||||
|
||||
int transaction_trace(unsigned, void *context, void *statement, void *)
|
||||
{
|
||||
auto & gate = *static_cast<TransactionGate *>(context);
|
||||
const char *query = sqlite3_sql(static_cast<sqlite3_stmt *>(statement));
|
||||
if (std::this_thread::get_id() != gate.owner) {
|
||||
if (!gate.ended.load()) {
|
||||
gate.interleaved = true;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(query, "ROLLBACK") == 0) {
|
||||
gate.ended = true;
|
||||
}
|
||||
if (strncmp(query, gate.insert.c_str(), gate.insert.size()) == 0) {
|
||||
std::unique_lock<std::mutex> lock(gate.mutex);
|
||||
if (!gate.paused) {
|
||||
gate.paused = true;
|
||||
gate.changed.notify_all();
|
||||
gate.changed.wait(lock, [&] { return gate.released; });
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void NodePropertyPersist::concurrent_rollback(bool tags)
|
||||
{
|
||||
group();
|
||||
TransactionGate gate;
|
||||
gate.insert = tags ? "INSERT INTO tags" : "INSERT INTO node_properties";
|
||||
ASSERT_EQ(
|
||||
SQLITE_OK,
|
||||
sqlite3_trace_v2(db, SQLITE_TRACE_STMT, transaction_trace, &gate));
|
||||
std::vector<std::string> names;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
names.push_back("batch" + std::to_string(i));
|
||||
}
|
||||
std::vector<neu_node_property_t> properties;
|
||||
std::vector<neu_datatag_t> datatags;
|
||||
for (const auto &name : names) {
|
||||
properties.push_back(property(name.c_str(), "1"));
|
||||
datatags.push_back(tag(name.c_str()));
|
||||
}
|
||||
properties.push_back(properties[0]);
|
||||
datatags.push_back(datatags[0]);
|
||||
int batch_result = -1;
|
||||
int other_result = -1;
|
||||
int ordinary_result = -1;
|
||||
std::thread batch([&] {
|
||||
gate.owner = std::this_thread::get_id();
|
||||
batch_result = tags
|
||||
? neu_persister_store_tags("driver", "group", datatags.data(),
|
||||
datatags.size())
|
||||
: neu_persister_add_node_properties("driver", properties.data(),
|
||||
properties.size());
|
||||
});
|
||||
bool paused;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(gate.mutex);
|
||||
paused = gate.changed.wait_for(lock, std::chrono::seconds(5),
|
||||
[&] { return gate.paused; });
|
||||
}
|
||||
std::thread other([&] {
|
||||
++gate.started;
|
||||
if (tags) {
|
||||
auto value = property("saved", "true");
|
||||
other_result = neu_persister_add_node_properties("app", &value, 1);
|
||||
} else {
|
||||
auto value = tag("saved");
|
||||
other_result =
|
||||
neu_persister_store_tags("driver", "group", &value, 1);
|
||||
}
|
||||
});
|
||||
std::thread ordinary([&] {
|
||||
++gate.started;
|
||||
ordinary_result = neu_persister_store_node_setting("driver", "saved");
|
||||
});
|
||||
while (gate.started.load() != 2) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(gate.mutex);
|
||||
gate.released = true;
|
||||
}
|
||||
gate.changed.notify_all();
|
||||
batch.join();
|
||||
other.join();
|
||||
ordinary.join();
|
||||
EXPECT_EQ(SQLITE_OK, sqlite3_trace_v2(db, 0, nullptr, nullptr));
|
||||
ASSERT_TRUE(paused);
|
||||
EXPECT_TRUE(gate.ended.load());
|
||||
EXPECT_FALSE(gate.interleaved.load());
|
||||
EXPECT_EQ(tags ? NEU_ERR_EINTERNAL : NEU_ERR_NODE_PROPERTY_EXIST,
|
||||
batch_result);
|
||||
EXPECT_EQ(0, other_result);
|
||||
EXPECT_EQ(0, ordinary_result);
|
||||
EXPECT_TRUE(sqlite3_get_autocommit(db));
|
||||
expect("driver", {});
|
||||
expect("app",
|
||||
tags
|
||||
? std::vector<std::pair<std::string, std::string>> { { "saved",
|
||||
"true" } }
|
||||
: std::vector<std::pair<std::string, std::string>> {});
|
||||
EXPECT_EQ(tags ? 0 : 1, scalar("SELECT count(*) FROM tags"));
|
||||
const char *setting = nullptr;
|
||||
ASSERT_EQ(0, neu_persister_load_node_setting("driver", &setting));
|
||||
EXPECT_STREQ("saved", setting);
|
||||
free(const_cast<char *>(setting));
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, PropertyTransactionIsolatesTagsAndOrdinaryWrite)
|
||||
{
|
||||
concurrent_rollback(false);
|
||||
}
|
||||
|
||||
TEST_F(NodePropertyPersist, TagTransactionIsolatesPropertiesAndOrdinaryWrite)
|
||||
{
|
||||
concurrent_rollback(true);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user