From 4a1906f5b7de77776b26db2d3630b1059d032488 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Wed, 29 Oct 2025 17:41:59 +0100 Subject: [PATCH 01/16] feat: Add scaffold --- .../fsm/ProtocolAdapterConnectionState.java | 45 ++ .../protocols/fsm/ProtocolAdapterState.java | 128 +++++ .../fsm/ProtocolAdapterTransitionResult.java | 26 + .../fsm/ProtocolAdapterTransitionStatus.java | 22 + .../protocols/fsm/ProtocolAdapterWrapper.java | 49 ++ .../com/hivemq/protocols/fsm/state-machine.md | 534 +++++++++++++++++ .../com/hivemq/protocols/fsm/transitions.md | 539 ++++++++++++++++++ 7 files changed, 1343 insertions(+) create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResult.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/state-machine.md create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/transitions.md diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java new file mode 100644 index 0000000000..17d87c49ad --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +public enum ProtocolAdapterConnectionState { + Closed(context -> 0), + Closing(context -> 0), + Connected(context -> 0), + Connecting(context -> 0), + Disconnected(context -> 0), + Disconnecting(context -> 0), + Error(context -> 0), + ErrorClosing(context -> 0), + ; + + private final @NotNull Function transitionFunction; + + ProtocolAdapterConnectionState(@NotNull final Function transitionFunction) { + this.transitionFunction = transitionFunction; + } + + public @NotNull Integer transition( + final @NotNull ProtocolAdapterConnectionState targetState, + final @NotNull Object context) { + return transitionFunction.apply(context); + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java new file mode 100644 index 0000000000..1fb5d882d2 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java @@ -0,0 +1,128 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiFunction; + +public enum ProtocolAdapterState { + Starting(ProtocolAdapterState::transitionFromStarting), + Started(ProtocolAdapterState::transitionFromStarted), + Stopping(ProtocolAdapterState::transitionFromStopping), + Stopped(ProtocolAdapterState::transitionFromStopped), + Error(ProtocolAdapterState::transitionFromError), + ; + + private final @NotNull BiFunction + transitionFunction; + + ProtocolAdapterState(@NotNull final BiFunction transitionFunction) { + this.transitionFunction = transitionFunction; + } + + public static ProtocolAdapterTransitionResult transitionFromStarted( + final @NotNull ProtocolAdapterState targetState, + final @NotNull ProtocolAdapterWrapper wrapper) { + switch (targetState) { + case Starting: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); + case Started: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); + case Stopping: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); + case Stopped: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); + default: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); + } + } + + public static ProtocolAdapterTransitionResult transitionFromStarting( + final @NotNull ProtocolAdapterState targetState, + final @NotNull ProtocolAdapterWrapper wrapper) { + switch (targetState) { + case Starting: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); + case Started: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); + case Stopping: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); + case Stopped: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); + default: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); + } + } + + public static ProtocolAdapterTransitionResult transitionFromStopped( + final @NotNull ProtocolAdapterState targetState, + final @NotNull ProtocolAdapterWrapper wrapper) { + switch (targetState) { + case Starting: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); + case Started: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); + case Stopping: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); + case Stopped: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); + default: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); + } + } + + public static ProtocolAdapterTransitionResult transitionFromStopping( + final @NotNull ProtocolAdapterState targetState, + final @NotNull ProtocolAdapterWrapper wrapper) { + switch (targetState) { + case Starting: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); + case Started: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); + case Stopping: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); + case Stopped: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); + default: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); + } + } + + public static ProtocolAdapterTransitionResult transitionFromError( + final @NotNull ProtocolAdapterState targetState, + final @NotNull ProtocolAdapterWrapper wrapper) { + switch (targetState) { + case Starting: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); + case Started: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); + case Stopping: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); + case Stopped: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); + default: + return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); + } + } + + public @NotNull ProtocolAdapterTransitionResult transition( + final @NotNull ProtocolAdapterState targetState, + final @NotNull ProtocolAdapterWrapper wrapper) { + return transitionFunction.apply(targetState, wrapper); + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResult.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResult.java new file mode 100644 index 0000000000..72b38e3534 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResult.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import org.jetbrains.annotations.NotNull; + +public record ProtocolAdapterTransitionResult(ProtocolAdapterState state, ProtocolAdapterTransitionStatus status, + String message, Throwable error) { + public ProtocolAdapterTransitionResult(final @NotNull ProtocolAdapterState state) { + this(state, ProtocolAdapterTransitionStatus.Success, null, null); + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java new file mode 100644 index 0000000000..201e447d38 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java @@ -0,0 +1,22 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +public enum ProtocolAdapterTransitionStatus { + Success, + Failure, +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper.java new file mode 100644 index 0000000000..fb01ab26c4 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import org.jetbrains.annotations.NotNull; + +public class ProtocolAdapterWrapper { + protected @NotNull ProtocolAdapterState state; + protected @NotNull ProtocolAdapterConnectionState connectionState; + + public ProtocolAdapterWrapper() { + this.state = ProtocolAdapterState.Stopped; + this.connectionState = ProtocolAdapterConnectionState.Closed; + } + + public @NotNull ProtocolAdapterState getState() { + return state; + } + + public @NotNull ProtocolAdapterConnectionState getConnectionState() { + return connectionState; + } + + public @NotNull ProtocolAdapterTransitionStatus transitionTo(final @NotNull ProtocolAdapterState protocolAdapterState) { + final ProtocolAdapterTransitionResult result = state.transition(protocolAdapterState, this); + state = result.state(); + // Handle error (logging, throwing exception, etc.) + switch (result.status()) { + default -> { + // TODO + } + } + return result.status(); + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/state-machine.md b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/state-machine.md new file mode 100644 index 0000000000..0ab83e444c --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/state-machine.md @@ -0,0 +1,534 @@ +# Protocol Adapter Finite State Machine Specification + +## Overview + +`ProtocolAdapterFSM` manages three concurrent state machines: + +1. **Adapter State** - Lifecycle control (start/stop) +2. **Northbound State** - Connection to HiveMQ broker +3. **Southbound State** - Connection to physical device + +All three must coordinate. The adapter controls overall lifecycle. Northbound handles MQTT publishing. Southbound handles device communication. + +--- + +## Adapter State Machine + +Four states control adapter lifecycle. + +### States + +1. **STOPPED** + - Default state + - Adapter inactive + - Transitions: → STARTING + +2. **STARTING** + - Initialization in progress + - Calls `onStarting()` hook + - Transitions: → STARTED (success), → STOPPED (failure), → ERROR (non recoverable error) + +3. **STARTED** + - Adapter operational + - Transitions: → STOPPING, → ERROR (non recoverable error) + +4. **STOPPING** + - Cleanup in progress + - Calls `onStopping()` hook + - Transitions: → STOPPED + +5. **ERROR** + - Non-recoverable error, adapter is dead + - This is a terminal state + - Transitions: → STARTING + +### Transition Rules + +``` + + ERROR + +STOPPED → STARTING → STARTED → STOPPING → STOPPED + ↑ ↓ + └────────────────────────────────┘ +``` + +**Constraint:** Must transition through intermediate states. Cannot jump STOPPED → STARTED. + +--- + +## Connection State Machines + +Nine states apply to both northbound and southbound connections. Each connection state machine operates independently but coordinates via the adapter lifecycle. + +### States + +**Note:** The state-machine.png diagram visualizes these states with color coding for clarity. + +1. **DISCONNECTED** + - No connection established + - Initial state + - Transitions: → CONNECTING, → CONNECTED (legacy), → CLOSED (testing) + +2. **CONNECTING** + - Connection attempt in progress + - Transitions: → CONNECTED, → ERROR, → DISCONNECTED + +3. **CONNECTED** + - Active connection established + - Data flow operational + - Transitions: → DISCONNECTING, → CONNECTING, → CLOSING, → ERROR_CLOSING, → DISCONNECTED + +4. **DISCONNECTING** + - Graceful connection teardown + - Transitions: → DISCONNECTED, → CLOSING + +5. **CLOSING** + - Permanent closure in progress + - Transitions: → CLOSED + +6. **CLOSED** + - Connection permanently closed + - Transitions: → DISCONNECTED (restart), → CLOSING (verification) + +7. **ERROR** + - Connection failure + - Transitions: → CONNECTING (recovery), → DISCONNECTED (abort) + +8. **ERROR_CLOSING** + - Error during closure + - Transitions: → ERROR + +9. **NOT_SUPPORTED** + - Stateless operation mode + - No transitions allowed + +### Transition Graph + +![State Machine Diagram](state-machine.png) + +The diagram shows: +- **Left side:** Complete connection state transition graph with all possible states and transitions +- **Right side:** Ideal operational sequence (steps 1-8) showing coordinated northbound and southbound transitions +- **Color coding:** + - Orange (DISCONNECTED) - Initial/inactive state + - Blue (CONNECTING, CONNECTED, DISCONNECTING, CLOSING) - Normal operational states + - Red (ERROR, ERROR_CLOSING, CLOSED) - Error or terminal states + +**Ideal Transition Sequence (Right side of diagram, steps 1-8):** +1. **Step 1:** Both connections DISCONNECTED (initial state) +2. **Step 2:** Northbound transitions to CONNECTING while southbound remains DISCONNECTED +3. **Step 3:** Northbound reaches CONNECTED (automatically triggers `startSouthbound()`), southbound still DISCONNECTED +4. **Step 4:** Southbound transitions to CONNECTING, northbound maintains CONNECTED +5. **Step 5:** Southbound transitions to CLOSING (shutdown initiated), northbound maintains CONNECTED +6. **Step 6:** Southbound reaches CLOSED (terminal state), northbound maintains CONNECTED +7. **Step 7:** Northbound transitions to CLOSING, southbound remains CLOSED +8. **Step 8:** Both connections reach CLOSED (shutdown complete) + +**ASCII Alternative:** +``` + ┌──────┐ + │ERROR │←──────┐ + └───┬──┘ │ + │ │ + ┌──DISCONNECTED──┐ ↓ │ + │ │ │ │ + ↓ │ ↓ ┌────────────┐ +CONNECTING ←─────────┴─→CONNECTED→ERROR_CLOSING + │ │ │ + ↓ ↓ ↓ +DISCONNECTED DISCONNECTING ERROR + ↑ │ + │ ↓ + │ DISCONNECTED + │ │ +CLOSED←─CLOSING←─────────┘ + │ ↑ + └───────┘ +``` + +--- + +## Operational Sequences + +### Startup Sequence + +1. Initial state: Adapter STOPPED, both connections DISCONNECTED +2. Call `startAdapter()` + - Adapter → STARTING + - Execute `onStarting()` + - Adapter → STARTED (success) or STOPPED (failure) +3. Transition northbound: DISCONNECTED → CONNECTING → CONNECTED +4. Automatic southbound start when northbound reaches CONNECTED + - `startSouthbound()` called automatically + - Override to control behavior + +### Normal Operation + +``` +Adapter: STARTED +Northbound: CONNECTED +Southbound: CONNECTED +``` + +### Connection Failure (Northbound) + +``` +Adapter: STARTED +Northbound: ERROR +Southbound: DISCONNECTED +``` + +Southbound never starts because northbound failed to reach CONNECTED. + +### Connection Failure (Southbound) + +``` +Adapter: STARTED +Northbound: CONNECTED +Southbound: ERROR +``` + +Valid state. Adapter can communicate with broker but not with device. + +### Shutdown Sequence + +**Option 1: Adapter Stop Only** +1. Call `stopAdapter()` + - Adapter → STOPPING + - Execute `onStopping()` + - Adapter → STOPPED +2. Connection states preserved + - Connections maintain current state after adapter stops + - Northbound and southbound remain in their current states (e.g., CONNECTED) + +**Option 2: Full Connection Closure (Ideal Sequence)** +1. Close southbound connection + - Southbound: CONNECTED → CLOSING → CLOSED +2. Close northbound connection + - Northbound: CONNECTED → CLOSING → CLOSED +3. Stop adapter + - Adapter → STOPPING → STOPPED + +The diagram's "Ideal State Transition" (steps 5-8) demonstrates Option 2, where connections are explicitly closed before stopping the adapter. This ensures clean resource cleanup and proper connection termination. + +--- + +## API Methods + +### Adapter Lifecycle + +- `startAdapter()` - Transition STOPPED → STARTING → STARTED +- `stopAdapter()` - Transition STARTED → STOPPING → STOPPED +- `onStarting()` - Override for initialization logic +- `onStopping()` - Override for cleanup logic + +### Northbound Control + +- `transitionNorthboundState(ConnectionStatus)` - Manual state transition +- `accept(ConnectionStatus)` - Transition + trigger southbound on CONNECTED +- `startDisconnecting()` - Begin graceful disconnect +- `startClosing()` - Begin permanent closure +- `startErrorClosing()` - Error-state closure +- `markAsClosed()` - Confirm CLOSED state +- `recoverFromError()` - Attempt recovery from ERROR +- `restartFromClosed()` - Restart from CLOSED state + +### Southbound Control + +- `transitionSouthboundState(ConnectionStatus)` - Manual state transition +- `startSouthbound()` - Override to implement southbound startup logic +- `startSouthboundDisconnecting()` +- `startSouthboundClosing()` +- `startSouthboundErrorClosing()` +- `markSouthboundAsClosed()` +- `recoverSouthboundFromError()` +- `restartSouthboundFromClosed()` + +### State Queries + +- `getNorthboundConnectionStatus()` - Current northbound state +- `getSouthboundConnectionStatus()` - Current southbound state +- `getAdapterState()` - Current adapter state + +--- + +## Concurrency + +### Thread Safety + +- State transitions use `AtomicReference` with `compareAndSet` +- Multiple threads can safely call transition methods +- CAS loop ensures atomic state updates +- Return value indicates success/failure + +### Example + +```java +boolean success = fsm.transitionNorthboundState(StateEnum.CONNECTING); +if (!success) { + // Another thread changed state concurrently + // Handle race condition +} +``` + +### State Listeners + +- Register listeners via `registerAdapterStateListener()` or `registerConnectionStateListener()` +- Listeners use `CopyOnWriteArrayList` - thread-safe during iteration +- Can add/remove listeners during state transitions + +--- + +## Implementation Requirements + +### Adapter Implementation + +```java +public class MyAdapter extends ProtocolAdapterFSM { + + @Override + protected boolean onStarting() { + // Initialize resources + // Return true on success, false on failure + return initializeConnection(); + } + + @Override + protected void onStopping() { + // Clean up resources + closeConnection(); + } + + @Override + public boolean startSouthbound() { + // Called automatically when northbound reaches CONNECTED + return transitionSouthboundState(StateEnum.CONNECTING); + } +} +``` + +### State Transition Logic + +```java +// Start adapter +fsm.startAdapter(); + +// Connect northbound +fsm.transitionNorthboundState(StateEnum.CONNECTING); +fsm.transitionNorthboundState(StateEnum.CONNECTED); + +// Southbound starts automatically via startSouthbound() + +// Stop adapter +fsm.stopAdapter(); +``` + +--- + +## Valid State Combinations + +| Adapter State | Northbound | Southbound | Valid | Notes | +|---------------|----------------|----------------|-------|-------| +| STOPPED | DISCONNECTED | DISCONNECTED | Yes | Initial state | +| STARTING | DISCONNECTED | DISCONNECTED | Yes | Startup in progress | +| STARTED | CONNECTED | CONNECTED | Yes | Normal operation | +| STARTED | CONNECTED | ERROR | Yes | Device communication failed | +| STARTED | ERROR | DISCONNECTED | Yes | Broker communication failed | +| STARTED | DISCONNECTED | CONNECTED | No | Invalid - northbound must connect first | +| STOPPING | * | * | Yes | Any connection state during shutdown | + +--- + +## Validation Rules + +1. **Adapter must be STARTED** before northbound/southbound transitions +2. **Transitions must follow** `possibleTransitions` map +3. **Cannot skip intermediate states** in adapter lifecycle +4. **Southbound activation** requires northbound CONNECTED state +5. **Return values** indicate transition success - must be checked + +--- + +## Error Handling + +### Transition Failures + +```java +// Check return value +if (!fsm.transitionNorthboundState(StateEnum.CONNECTED)) { + log.error("Transition failed - state changed concurrently"); + // Retry or handle error +} +``` + +### Illegal Transitions + +```java +// Throws IllegalStateException +try { + fsm.transitionNorthboundState(StateEnum.CLOSING); // From DISCONNECTED +} catch (IllegalStateException e) { + log.error("Invalid transition: " + e.getMessage()); +} +``` + +### Startup Failures + +```java +@Override +protected boolean onStarting() { + try { + initializeResources(); + return true; + } catch (Exception e) { + log.error("Startup failed", e); + return false; // Adapter transitions to STOPPED + } +} +``` + +--- + +## Testing Scenarios + +### Test 1: Legacy Connection + +```java +fsm.startAdapter(); +fsm.accept(StateEnum.CONNECTED); // Direct jump to CONNECTED +// Southbound: DISCONNECTED (startSouthbound() not implemented) +``` + +### Test 2: Standard Connection Flow + +```java +fsm.startAdapter(); +fsm.accept(StateEnum.CONNECTING); +fsm.accept(StateEnum.CONNECTED); +// Southbound: Starts automatically +``` + +### Test 3: Northbound Error + +```java +fsm.startAdapter(); +fsm.transitionNorthboundState(StateEnum.CONNECTING); +fsm.transitionNorthboundState(StateEnum.ERROR); +// Southbound: DISCONNECTED (never started) +``` + +### Test 4: Southbound Error + +```java +fsm.startAdapter(); +fsm.accept(StateEnum.CONNECTED); // Northbound CONNECTED +// startSouthbound() called automatically +fsm.transitionSouthboundState(StateEnum.CONNECTING); +fsm.transitionSouthboundState(StateEnum.ERROR); +// Northbound: Still CONNECTED +// Southbound: ERROR +``` + +--- + +## State Transition Tables + +### Adapter State Transitions + +| From | To | Method | Condition | +|----------|----------|-------------------|-----------| +| STOPPED | STARTING | `startAdapter()` | Always | +| STARTING | STARTED | Internal | `onStarting()` returns true | +| STARTING | STOPPED | Internal | `onStarting()` returns false | +| STARTED | STOPPING | `stopAdapter()` | Always | +| STOPPING | STOPPED | Internal | After `onStopping()` | + +### Connection State Transitions + +| From | To | Allowed | +|-----------------|-----------------|---------| +| DISCONNECTED | CONNECTING | Yes | +| DISCONNECTED | CONNECTED | Yes (legacy) | +| DISCONNECTED | CLOSED | Yes (testing) | +| CONNECTING | CONNECTED | Yes | +| CONNECTING | ERROR | Yes | +| CONNECTING | DISCONNECTED | Yes | +| CONNECTED | DISCONNECTING | Yes | +| CONNECTED | CONNECTING | Yes | +| CONNECTED | CLOSING | Yes | +| CONNECTED | ERROR_CLOSING | Yes | +| CONNECTED | DISCONNECTED | Yes | +| DISCONNECTING | DISCONNECTED | Yes | +| DISCONNECTING | CLOSING | Yes | +| CLOSING | CLOSED | Yes | +| CLOSED | DISCONNECTED | Yes | +| CLOSED | CLOSING | Yes | +| ERROR | CONNECTING | Yes | +| ERROR | DISCONNECTED | Yes | +| ERROR_CLOSING | ERROR | Yes | +| NOT_SUPPORTED | * | No | + +--- + +## Common Implementation Errors + +### Error 1: Adapter Not Started + +```java +// Incorrect +fsm.transitionNorthboundState(StateEnum.CONNECTING); +// Throws IllegalStateException - adapter not started + +// Correct +fsm.startAdapter(); +fsm.transitionNorthboundState(StateEnum.CONNECTING); +``` + +### Error 2: Ignoring Return Values + +```java +// Incorrect +fsm.transitionNorthboundState(StateEnum.CONNECTING); +// May fail silently due to concurrent modification + +// Correct +boolean success = fsm.transitionNorthboundState(StateEnum.CONNECTING); +if (!success) { + // Handle race condition +} +``` + +### Error 3: Missing startSouthbound Implementation + +```java +// Incorrect +@Override +public boolean startSouthbound() { + log.info("Starting southbound"); + return true; // Doesn't actually transition state +} + +// Correct +@Override +public boolean startSouthbound() { + return transitionSouthboundState(StateEnum.CONNECTING); +} +``` + +--- + +## Key Constraints + +1. Adapter state transitions are sequential - no state skipping +2. Northbound must reach CONNECTED before southbound starts +3. Connection states allow multiple valid transition paths +4. All transitions are atomic via CAS operations +5. State listeners execute synchronously during transitions +6. Adapter ID included in all log messages for debugging + +--- + +## Summary + +The `ProtocolAdapterFSM` coordinates three state machines with defined transition rules. Adapter state controls lifecycle. Northbound handles broker communication. Southbound handles device communication. All operations are thread-safe. State transitions must follow defined paths. Return values indicate success. Override `onStarting()`, `onStopping()`, and `startSouthbound()` to implement adapter-specific behavior. diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/transitions.md b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/transitions.md new file mode 100644 index 0000000000..f1c3a6d48c --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/transitions.md @@ -0,0 +1,539 @@ +# Understanding Protocol Adapter State Machines: A Test-Driven Journey + +*5 min read • Learn how three coordinated state machines manage adapter lifecycle and connections* + +--- + +## What Are We Building? + +Protocol adapters connect HiveMQ Edge to physical devices. They need to manage: +1. **Adapter lifecycle** (starting/stopping) +2. **Northbound connection** (to MQTT broker) +3. **Southbound connection** (to physical device) + +These three state machines coordinate to ensure reliable communication. Let's explore them through their tests. + +--- + +## The Three State Machines + +```mermaid +graph TB + subgraph Adapter Lifecycle + STOPPED -->|startAdapter| STARTING + STARTING -->|success| STARTED + STARTING -->|non recoverable error| ERROR + STARTED -->|stopAdapter| STOPPING + STARTED -->|non recoverable error| ERROR + STOPPING --> STOPPED + end +``` + +```mermaid +graph TB + subgraph Connection States - Northbound & Southbound + DISCONNECTED -->|connect| CONNECTING + DISCONNECTED -->|legacy| CONNECTED + CONNECTING -->|success| CONNECTED + CONNECTING -->|fail| ERROR + CONNECTING -->|abort| DISCONNECTED + CONNECTED -->|reconnect| CONNECTING + CONNECTED -->|graceful| DISCONNECTING + CONNECTED -->|terminate| CLOSING + CONNECTED -->|error| ERROR_CLOSING + CONNECTED -->|instant| DISCONNECTED + DISCONNECTING --> DISCONNECTED + DISCONNECTING --> CLOSING + ERROR -->|recover| CONNECTING + ERROR -->|give up| DISCONNECTED + ERROR_CLOSING --> ERROR + CLOSING --> CLOSED + CLOSED -->|restart| DISCONNECTED + CLOSED -->|verify| CLOSING + end +``` + +--- + +## Adapter Lifecycle: The Boss + +The adapter lifecycle controls everything. No connections can transition without the adapter being STARTED. + +### Test: Successful Startup + +```java +@Test +void adapter_successfulStartup() { + fsm.startAdapter(); + assertState(fsm, STARTED, DISCONNECTED, DISCONNECTED); +} +``` + +**What happens:** +- Initial: `STOPPED → STARTING → STARTED` +- Connections stay `DISCONNECTED` +- Ready to establish connections + +### Test: Failed Startup + +```java +@Test +void adapter_failedStartup_returnsToStopped() { + // onStarting() returns false + fsm.startAdapter(); + assertState(fsm, STOPPED, DISCONNECTED, DISCONNECTED); +} +``` + +**What happens:** +- Attempted: `STOPPED → STARTING → STOPPED` +- Initialization failed, rolled back +- Connections never activated + +### Test: Stop Preserves Connections + +```java +@Test +void adapter_stopPreservesConnectionStates() { + fsm.startAdapter(); + fsm.transitionNorthboundState(CONNECTED); + fsm.stopAdapter(); + + assertState(fsm, STOPPED, CONNECTED, DISCONNECTED); +} +``` + +**Key insight:** Stopping the adapter doesn't disconnect connections. They maintain their state. + +--- + +## Northbound: Connection to MQTT Broker + +Northbound manages the connection to HiveMQ broker. It has the most complex state graph. + +### Test: Standard Connection Flow + +```java +@Test +void northbound_standardConnectFlow() { + fsm.startAdapter(); + fsm.accept(CONNECTING); + fsm.accept(CONNECTED); +} +``` + +```mermaid +sequenceDiagram + participant A as Adapter + participant N as Northbound + A->>A: STOPPED → STARTED + N->>N: DISCONNECTED → CONNECTING + N->>N: CONNECTING → CONNECTED +``` + +### Test: Legacy Direct Connect + +```java +@Test +void northbound_legacyDirectConnect() { + fsm.startAdapter(); + fsm.accept(CONNECTED); // Skip CONNECTING +} +``` + +**Why:** Backward compatibility. Old code could jump directly to CONNECTED. + +### Test: Error Handling + +```java +@Test +void northbound_errorRecovery() { + fsm.startAdapter(); + fsm.transitionNorthboundState(CONNECTING); + fsm.transitionNorthboundState(ERROR); + + fsm.recoverFromError(); // ERROR → CONNECTING +} +``` + +**The error recovery cycle:** +```mermaid +graph LR + CONNECTING -->|connection failed| ERROR + ERROR -->|retry| CONNECTING + ERROR -->|give up| DISCONNECTED +``` + +### Test: Graceful Shutdown + +```java +@Test +void northbound_closingSequence() { + fsm.transitionNorthboundState(CONNECTED); + fsm.startClosing(); // CONNECTED → CLOSING + fsm.markAsClosed(); // CLOSING → CLOSED +} +``` + +### Test: Emergency Shutdown + +```java +@Test +void northbound_errorClosingSequence() { + fsm.transitionNorthboundState(CONNECTED); + fsm.startErrorClosing(); // CONNECTED → ERROR_CLOSING + // ERROR_CLOSING → ERROR +} +``` + +**Two shutdown paths:** +```mermaid +graph TB + CONNECTED -->|graceful| CLOSING + CONNECTED -->|error while closing| ERROR_CLOSING + CLOSING --> CLOSED + ERROR_CLOSING --> ERROR +``` + +### Test: Reconnection + +```java +@Test +void northbound_reconnectFromConnected() { + fsm.transitionNorthboundState(CONNECTED); + fsm.transitionNorthboundState(CONNECTING); // Reconnect +} +``` + +**Use case:** Connection degraded, attempt reconnection without full disconnect. + +--- + +## Southbound: Connection to Physical Device + +Southbound mirrors northbound but has a special behavior: **automatic activation**. + +### Test: Automatic Start + +```java +@Test +void southbound_startsWhenNorthboundConnects() { + fsm = createFSMWithAutoSouthbound(); + fsm.startAdapter(); + fsm.accept(CONNECTED); // Northbound connects + + // Southbound automatically starts CONNECTING +} +``` + +**Why:** Device connection only makes sense when broker connection is established. + +```mermaid +sequenceDiagram + participant N as Northbound + participant S as Southbound + N->>N: CONNECTING → CONNECTED + N->>S: Trigger startSouthbound() + S->>S: DISCONNECTED → CONNECTING +``` + +### Test: Southbound Error While Northbound Connected + +```java +@Test +void southbound_errorWhileNorthboundConnected() { + fsm.accept(CONNECTED); // Northbound OK + // Southbound transitions to ERROR asynchronously + + assertState(fsm, STARTED, CONNECTED, ERROR); +} +``` + +**This is valid!** Broker connection works, device doesn't. Adapter can receive commands but can't execute them. + +### Test: Full Lifecycle + +```java +@Test +void southbound_fullLifecycle() { + fsm.accept(CONNECTED); // Auto-start + fsm.transitionSouthboundState(CONNECTED); // Connection succeeds + fsm.startSouthboundClosing(); // Begin shutdown + fsm.markSouthboundAsClosed(); // Complete shutdown +} +``` + +```mermaid +graph LR + DISCONNECTED -->|auto| CONNECTING + CONNECTING --> CONNECTED + CONNECTED --> CLOSING + CLOSING --> CLOSED +``` + +--- + +## Coordination: The Ideal Shutdown Sequence + +The most complex test demonstrates all three state machines coordinating. + +```java +@Test +void diagramSequence_idealShutdown() { + // Step 1: Initial state + assertState(fsm, STOPPED, DISCONNECTED, DISCONNECTED); + + // Step 2: Start adapter, begin northbound connection + fsm.startAdapter(); + fsm.transitionNorthboundState(CONNECTING); + + // Step 3: Northbound connects (triggers southbound) + fsm.accept(CONNECTED); + + // Step 4: Southbound connects + fsm.transitionSouthboundState(CONNECTED); + + // Step 5: Close southbound first + fsm.startSouthboundClosing(); + + // Step 6: Southbound closed + fsm.markSouthboundAsClosed(); + + // Step 7: Close northbound + fsm.startClosing(); + + // Step 8: Northbound closed + fsm.markAsClosed(); +} +``` + +```mermaid +sequenceDiagram + participant A as Adapter + participant N as Northbound + participant S as Southbound + + Note over A,S: Step 1: Initial State + A->>A: STOPPED + + Note over A,S: Step 2-3: Startup + A->>A: STOPPED → STARTED + N->>N: DISCONNECTED → CONNECTING → CONNECTED + N->>S: Trigger startSouthbound() + + Note over A,S: Step 4: Southbound Connects + S->>S: CONNECTING → CONNECTED + + Note over A,S: Step 5-8: Graceful Shutdown + S->>S: CONNECTED → CLOSING → CLOSED + N->>N: CONNECTED → CLOSING → CLOSED +``` + +**Why this order?** Close device connection before broker connection. Ensures clean resource cleanup. + +--- + +## Error Scenarios + +### Test: Northbound Fails, Southbound Never Starts + +```java +@Test +void northbound_errorState() { + fsm.startAdapter(); + fsm.accept(CONNECTING); + fsm.accept(ERROR); + + assertState(fsm, STARTED, ERROR, DISCONNECTED); +} +``` + +**Southbound stays DISCONNECTED** because northbound never reached CONNECTED. + +### Test: Concurrent State Changes + +```java +@Test +void concurrentTransition_casFailure() { + fsm.transitionNorthboundState(CONNECTING); + fsm.transitionNorthboundState(CONNECTED); // Succeeds + fsm.transitionNorthboundState(CONNECTING); // Also succeeds (reconnect) +} +``` + +**Thread-safe:** Uses atomic compare-and-set operations. Multiple threads can safely transition states. + +--- + +## Invalid Transitions + +Not all transitions are allowed. Tests verify enforcement: + +```java +@Test +void invalidTransition_disconnectedToClosing() { + fsm.startAdapter(); + assertThatThrownBy(() -> fsm.transitionNorthboundState(CLOSING)) + .isInstanceOf(IllegalStateException.class); +} +``` + +**Rule:** Cannot close what isn't connected. Must go through CONNECTED first. + +```java +@Test +void invalidTransition_connectingToErrorClosing() { + fsm.transitionNorthboundState(CONNECTING); + assertThatThrownBy(() -> fsm.transitionNorthboundState(ERROR_CLOSING)) + .isInstanceOf(IllegalStateException.class); +} +``` + +**Rule:** ERROR_CLOSING only valid from CONNECTED state. + +--- + +## State Observers + +### Test: Notifications + +```java +@Test +void stateListener_multipleNotifications() { + fsm.registerStateTransitionListener(state -> stateCount.incrementAndGet()); + + fsm.startAdapter(); // +2 (STOPPED→STARTING→STARTED) + fsm.transitionNorthboundState(CONNECTING); // +1 + fsm.transitionNorthboundState(CONNECTED); // +1 + + assertThat(stateCount.get()).isEqualTo(4); +} +``` + +**Key insight:** `startAdapter()` triggers **two** transitions internally. + +```mermaid +graph LR + STOPPED -->|1️⃣| STARTING + STARTING -->|2️⃣| STARTED +``` + +### Test: Unregister + +```java +@Test +void stateListener_unregister() { + fsm.registerStateTransitionListener(listener); + fsm.startAdapter(); // Notified + + fsm.unregisterStateTransitionListener(listener); + fsm.transitionNorthboundState(CONNECTING); // NOT notified +} +``` + +--- + +## State Machine Properties + +From analyzing the tests, we can identify key properties: + +### 1. **Thread-Safety** +All transitions use atomic operations. Concurrent calls are safe. + +### 2. **Separation of Concerns** +Three independent state machines coordinate via triggers, not tight coupling. + +### 3. **Fail-Safe** +- Startup failure returns to STOPPED +- Error states have recovery paths +- Invalid transitions throw exceptions + +### 4. **Flexibility** +- Legacy direct-connect supported +- Multiple shutdown paths (graceful, error, instant) +- Reconnection without full disconnect + +### 5. **Observability** +State listeners enable monitoring and reactive behavior. + +--- + +## Complete State Graphs + +### Adapter States + +```mermaid +stateDiagram-v2 + [*] --> STOPPED + STOPPED --> STARTING: startAdapter() + STARTING --> STARTED: onStarting() = true + STARTING --> STOPPED: onStarting() = false + STARTED --> STOPPING: stopAdapter() + STOPPING --> STOPPED + STOPPED --> [*] +``` + +### Connection States (Both Northbound & Southbound) + +```mermaid +stateDiagram-v2 + [*] --> DISCONNECTED + DISCONNECTED --> CONNECTING: connect attempt + DISCONNECTED --> CONNECTED: legacy direct connect + DISCONNECTED --> CLOSED: testing + + CONNECTING --> CONNECTED: success + CONNECTING --> ERROR: failure + CONNECTING --> DISCONNECTED: abort + + CONNECTED --> CONNECTING: reconnect + CONNECTED --> DISCONNECTING: graceful disconnect + CONNECTED --> CLOSING: terminate + CONNECTED --> ERROR_CLOSING: error during close + CONNECTED --> DISCONNECTED: instant disconnect + + DISCONNECTING --> DISCONNECTED + DISCONNECTING --> CLOSING + + CLOSING --> CLOSED + + CLOSED --> DISCONNECTED: restart + CLOSED --> CLOSING: verification + + ERROR --> CONNECTING: recovery + ERROR --> DISCONNECTED: give up + + ERROR_CLOSING --> ERROR + + DISCONNECTED --> [*] +``` + +--- + +## Key Takeaways + +1. **Three coordinated state machines** manage adapter lifecycle and bi-directional connections +2. **Adapter must be STARTED** before connections can transition +3. **Northbound triggers southbound** when reaching CONNECTED state +4. **Error states have recovery paths** - systems can self-heal +5. **Multiple shutdown paths** handle graceful and emergency scenarios +6. **Thread-safe by design** using atomic operations +7. **State preservation** when adapter stops - connections maintain state +8. **Observability built-in** via state transition listeners + +--- + +## Test Coverage + +**32 tests** verify: +- ✅ Adapter lifecycle (startup, failure, stop) +- ✅ Northbound transitions (13 scenarios) +- ✅ Southbound transitions (7 scenarios) +- ✅ Invalid transition rejection (3 guards) +- ✅ State listeners (register, notify, unregister) +- ✅ Concurrent modifications (CAS validation) +- ✅ Ideal shutdown sequence (8-step coordination) + +--- + +*Read the code: [`ProtocolAdapterFSMTest.java`](src/test/java/com/hivemq/fsm/ProtocolAdapterFSMTest.java)* From e843c4bb7cbd9f1bec998acc2df41f68a5b5e7a0 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Thu, 30 Oct 2025 12:14:01 +0100 Subject: [PATCH 02/16] refactor: Let startWriting() return CompletableFuture instead of boolean --- ext/hivemq-edge-openapi-2025.19.yaml | 2 +- ...InternalProtocolAdapterWritingService.java | 2 + ...pper.java => ProtocolAdapterInstance.java} | 27 +-- .../fsm/ProtocolAdapterOperator.java | 20 +++ .../protocols/fsm/ProtocolAdapterState.java | 163 ++++++++++-------- .../fsm/ProtocolAdapterTransition.java | 26 +++ .../fsm/ProtocolAdapterTransitionRequest.java | 49 ++++++ ...=> ProtocolAdapterTransitionResponse.java} | 6 +- .../protocols/fsmjochen/FsmExperiment.java | 83 +++++++++ .../hivemq/protocols/fsmjochen/PATest.java | 50 ++++++ .../fsmjochen/ProtocolAdapterFsmJochen.java | 22 +++ 11 files changed, 361 insertions(+), 89 deletions(-) rename hivemq-edge/src/main/java/com/hivemq/protocols/fsm/{ProtocolAdapterWrapper.java => ProtocolAdapterInstance.java} (58%) create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransition.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionRequest.java rename hivemq-edge/src/main/java/com/hivemq/protocols/fsm/{ProtocolAdapterTransitionResult.java => ProtocolAdapterTransitionResponse.java} (72%) create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/FsmExperiment.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/PATest.java create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/ProtocolAdapterFsmJochen.java diff --git a/ext/hivemq-edge-openapi-2025.19.yaml b/ext/hivemq-edge-openapi-2025.19.yaml index ccbe851878..1801f7b648 100644 --- a/ext/hivemq-edge-openapi-2025.19.yaml +++ b/ext/hivemq-edge-openapi-2025.19.yaml @@ -14,7 +14,7 @@ info: ## OpenAPI HiveMQ's REST API provides an OpenAPI 3.0 schema definition that can imported into popular API tooling (e.g. Postman) or can be used to generate client-code for multiple programming languages. title: HiveMQ Edge REST API - version: 2025.19-SNAPSHOT + version: 2025.14-SNAPSHOT x-logo: url: https://www.hivemq.com/img/svg/hivemq-bee.svg tags: diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/InternalProtocolAdapterWritingService.java b/hivemq-edge/src/main/java/com/hivemq/protocols/InternalProtocolAdapterWritingService.java index a9c645dd60..389b9f59ef 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/InternalProtocolAdapterWritingService.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/InternalProtocolAdapterWritingService.java @@ -18,10 +18,12 @@ import com.hivemq.adapter.sdk.api.services.ProtocolAdapterMetricsService; import com.hivemq.adapter.sdk.api.services.ProtocolAdapterWritingService; import com.hivemq.adapter.sdk.api.writing.WritingProtocolAdapter; +import com.hivemq.persistence.SingleWriterService; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; public interface InternalProtocolAdapterWritingService extends ProtocolAdapterWritingService { diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java similarity index 58% rename from hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper.java rename to hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java index fb01ab26c4..5a9b53674e 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java @@ -18,13 +18,13 @@ import org.jetbrains.annotations.NotNull; -public class ProtocolAdapterWrapper { +public class ProtocolAdapterInstance { protected @NotNull ProtocolAdapterState state; protected @NotNull ProtocolAdapterConnectionState connectionState; - public ProtocolAdapterWrapper() { - this.state = ProtocolAdapterState.Stopped; - this.connectionState = ProtocolAdapterConnectionState.Closed; + public ProtocolAdapterInstance() { + connectionState = ProtocolAdapterConnectionState.Closed; + state = ProtocolAdapterState.Stopped; } public @NotNull ProtocolAdapterState getState() { @@ -35,15 +35,18 @@ public ProtocolAdapterWrapper() { return connectionState; } - public @NotNull ProtocolAdapterTransitionStatus transitionTo(final @NotNull ProtocolAdapterState protocolAdapterState) { - final ProtocolAdapterTransitionResult result = state.transition(protocolAdapterState, this); - state = result.state(); - // Handle error (logging, throwing exception, etc.) - switch (result.status()) { - default -> { - // TODO + public synchronized @NotNull ProtocolAdapterTransitionResponse transitionTo(final @NotNull ProtocolAdapterState newState) { + final ProtocolAdapterTransitionResponse response = state.transition(newState, this); + if (response.status() == ProtocolAdapterTransitionStatus.Success) { + this.state = response.state(); + } else { + // Handle error (logging, throwing exception, etc.) + switch (response.status()) { + default -> { + // TODO + } } } - return result.status(); + return response; } } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java new file mode 100644 index 0000000000..4bfede77ae --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +public class ProtocolAdapterOperator { +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java index 1fb5d882d2..d9e683b37b 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java @@ -28,101 +28,118 @@ public enum ProtocolAdapterState { Error(ProtocolAdapterState::transitionFromError), ; - private final @NotNull BiFunction + private final @NotNull BiFunction transitionFunction; - ProtocolAdapterState(@NotNull final BiFunction transitionFunction) { + ProtocolAdapterState(@NotNull final BiFunction transitionFunction) { this.transitionFunction = transitionFunction; } - public static ProtocolAdapterTransitionResult transitionFromStarted( - final @NotNull ProtocolAdapterState targetState, - final @NotNull ProtocolAdapterWrapper wrapper) { - switch (targetState) { + public static ProtocolAdapterTransitionResponse transitionFromStarted( + final @NotNull ProtocolAdapterState toState, + final @NotNull ProtocolAdapterInstance instance) { + switch (toState) { case Starting: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); case Started: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Started); case Stopping: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopping); case Stopped: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped); default: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); } } - public static ProtocolAdapterTransitionResult transitionFromStarting( - final @NotNull ProtocolAdapterState targetState, - final @NotNull ProtocolAdapterWrapper wrapper) { - switch (targetState) { - case Starting: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); - case Started: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); - case Stopping: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); - case Stopped: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); - default: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); - } + public static ProtocolAdapterTransitionResponse transitionFromStarting( + final @NotNull ProtocolAdapterState toState, + final @NotNull ProtocolAdapterInstance instance) { + return switch (toState) { + case Starting -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); + case Started -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Started); + case Stopping -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopping); + case Stopped -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped); + default -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); + }; } - public static ProtocolAdapterTransitionResult transitionFromStopped( - final @NotNull ProtocolAdapterState targetState, - final @NotNull ProtocolAdapterWrapper wrapper) { - switch (targetState) { - case Starting: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); - case Started: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); - case Stopping: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); - case Stopped: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); - default: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); - } + public static @NotNull ProtocolAdapterTransitionResponse transitionFromStopped( + final @NotNull ProtocolAdapterState toState, + final @NotNull ProtocolAdapterInstance instance) { + return switch (toState) { + case Starting -> transitionFromStoppedToStarting(instance); + case Started, Stopping -> transitionToError(ProtocolAdapterState.Stopped, toState); + case Stopped -> transitionWithoutChanges(toState); + case Error -> transitionFromStoppedToError(instance); + }; } - public static ProtocolAdapterTransitionResult transitionFromStopping( - final @NotNull ProtocolAdapterState targetState, - final @NotNull ProtocolAdapterWrapper wrapper) { - switch (targetState) { - case Starting: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); - case Started: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); - case Stopping: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); - case Stopped: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); - default: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); + public static ProtocolAdapterTransitionResponse transitionFromStoppedToError(final @NotNull ProtocolAdapterInstance instance) { + try { + // Do something to error. +// instance.doSomething(); + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); + } catch (final Exception e) { + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error, + ProtocolAdapterTransitionStatus.Failure, + "Failed transition from Stopped to Error.", + e); } } - public static ProtocolAdapterTransitionResult transitionFromError( - final @NotNull ProtocolAdapterState targetState, - final @NotNull ProtocolAdapterWrapper wrapper) { - switch (targetState) { - case Starting: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Starting); - case Started: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Started); - case Stopping: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopping); - case Stopped: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Stopped); - default: - return new ProtocolAdapterTransitionResult(ProtocolAdapterState.Error); + public static ProtocolAdapterTransitionResponse transitionFromStoppedToStarting(final @NotNull ProtocolAdapterInstance instance) { + try { + // Do something to start the protocol adapter. + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); + } catch (final Exception e) { + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped, + ProtocolAdapterTransitionStatus.Failure, + "Failed transition from Stopped to Starting.", + e); } } - public @NotNull ProtocolAdapterTransitionResult transition( - final @NotNull ProtocolAdapterState targetState, - final @NotNull ProtocolAdapterWrapper wrapper) { - return transitionFunction.apply(targetState, wrapper); + public static @NotNull ProtocolAdapterTransitionResponse transitionWithoutChanges(final @NotNull ProtocolAdapterState toState) { + return new ProtocolAdapterTransitionResponse(toState); + } + + public static @NotNull ProtocolAdapterTransitionResponse transitionToError( + final @NotNull ProtocolAdapterState fromState, + final @NotNull ProtocolAdapterState toState) { + return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error, + ProtocolAdapterTransitionStatus.Failure, + "Unable to transition from " + fromState + " to " + toState + ".", + null); + } + + public static ProtocolAdapterTransitionResponse transitionFromStopping( + final @NotNull ProtocolAdapterState toState, + final @NotNull ProtocolAdapterInstance instance) { + return switch (toState) { + case Starting -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); + case Started -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Started); + case Stopping -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopping); + case Stopped -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped); + default -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); + }; + } + + public static ProtocolAdapterTransitionResponse transitionFromError( + final @NotNull ProtocolAdapterState toState, + final @NotNull ProtocolAdapterInstance instance) { + return switch (toState) { + case Starting -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); + case Started -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Started); + case Stopping -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopping); + case Stopped -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped); + default -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); + }; + } + + public @NotNull ProtocolAdapterTransitionResponse transition( + final @NotNull ProtocolAdapterState toState, + final @NotNull ProtocolAdapterInstance instance) { + return transitionFunction.apply(toState, instance); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransition.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransition.java new file mode 100644 index 0000000000..b28b114a98 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransition.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import org.jetbrains.annotations.NotNull; + +public interface ProtocolAdapterTransition { + @NotNull ProtocolAdapterTransitionResponse transition( + final @NotNull ProtocolAdapterState toState, + final @NotNull ProtocolAdapterInstance session, + final @NotNull ProtocolAdapterTransitionRequest request); +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionRequest.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionRequest.java new file mode 100644 index 0000000000..18f0e56edb --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionRequest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.TimeUnit; + +public record ProtocolAdapterTransitionRequest(boolean requestWriteLock, long timeout, TimeUnit timeUnit) { + public static class Builder { + private boolean requestWriteLock; + private long timeout; + private @Nullable TimeUnit timeUnit; + + public @NotNull Builder requestWriteLock(final boolean requestWriteLock) { + this.requestWriteLock = requestWriteLock; + return this; + } + + public @NotNull Builder timeout(final long timeout) { + this.timeout = timeout; + return this; + } + + public @NotNull Builder timeUnit(final @NotNull TimeUnit timeUnit) { + this.timeUnit = timeUnit; + return this; + } + + public @NotNull ProtocolAdapterTransitionRequest build() { + return new ProtocolAdapterTransitionRequest(requestWriteLock, timeout, timeUnit); + } + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResult.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java similarity index 72% rename from hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResult.java rename to hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java index 72b38e3534..d572a51894 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResult.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java @@ -18,9 +18,9 @@ import org.jetbrains.annotations.NotNull; -public record ProtocolAdapterTransitionResult(ProtocolAdapterState state, ProtocolAdapterTransitionStatus status, - String message, Throwable error) { - public ProtocolAdapterTransitionResult(final @NotNull ProtocolAdapterState state) { +public record ProtocolAdapterTransitionResponse(ProtocolAdapterState state, ProtocolAdapterTransitionStatus status, + String message, Throwable error) { + public ProtocolAdapterTransitionResponse(final @NotNull ProtocolAdapterState state) { this(state, ProtocolAdapterTransitionStatus.Success, null, null); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/FsmExperiment.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/FsmExperiment.java new file mode 100644 index 0000000000..f5357417db --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/FsmExperiment.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsmjochen; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public class FsmExperiment { + + private volatile State currentState = State.Stopped; + + private final ProtocolAdapterFsmJochen protocolAdapterFsmJochen; + + public enum State { + Stopped, + Starting, + Started, + Stopping, + Error + } + + final Map> stateToStatesMap = Map.of( + State.Stopped, List.of(State.Starting), + State.Starting, List.of(State.Started, State.Stopping, State.Error), + State.Started, List.of(State.Stopping, State.Error), + State.Stopping, List.of(State.Error, State.Stopped), + State.Error, List.of(State.Starting) + ); + + final Map> transitionsMap = Map.of( + State.Stopped, pa -> State.Stopped, + State.Stopping, pa -> { + try { + pa.stop(); + return State.Stopping; + } catch (Exception e) { + return State.Error; + } + }, + State.Started, pa -> State.Started, + State.Starting, pa -> { + pa.start(); + return State.Stopping; + }, + State.Error, pa -> State.Error + ); + + + public FsmExperiment(ProtocolAdapterFsmJochen protocolAdapterFsmJochen) { + this.protocolAdapterFsmJochen = protocolAdapterFsmJochen; + } + + public synchronized boolean transitionTo(State targetState) { + if(stateToStatesMap.get(currentState).contains(targetState)) { + currentState = transitionsMap.get(targetState).apply(protocolAdapterFsmJochen); + return true; + } else { + return false; + } + } + + + public State getCurrentState() { + return currentState; + } + +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/PATest.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/PATest.java new file mode 100644 index 0000000000..6cea7675f7 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/PATest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsmjochen; + +import java.util.concurrent.CompletableFuture; + +public class PATest implements ProtocolAdapterFsmJochen { + FsmExperiment fsmExperiment; + + public PATest(FsmExperiment fsmExperiment) { + this.fsmExperiment = fsmExperiment; + } + + public void start() { + CompletableFuture.runAsync(() -> { + if (true) { + fsmExperiment.transitionTo(FsmExperiment.State.Started); + } else{ + fsmExperiment.transitionTo(FsmExperiment.State.Error); + } + }); + }; + + public void stop() { + CompletableFuture.runAsync(() -> { + + //DO ALL THE WORK + + if (true) { + fsmExperiment.transitionTo(FsmExperiment.State.Stopped); + } else{ + fsmExperiment.transitionTo(FsmExperiment.State.Error); + } + }); + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/ProtocolAdapterFsmJochen.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/ProtocolAdapterFsmJochen.java new file mode 100644 index 0000000000..3435326fbf --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsmjochen/ProtocolAdapterFsmJochen.java @@ -0,0 +1,22 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsmjochen; + +public interface ProtocolAdapterFsmJochen { + void start(); + void stop(); +} From b98eb1f98c8f29f1d2e4d300e94aeb3d13163328 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Thu, 30 Oct 2025 17:39:12 +0100 Subject: [PATCH 03/16] Revert "refactor: Let attemptStartingConsumers() return CompletableFuture instead of Optional" This reverts commit ce118b9e26d96a9bee05f14324f5167805cfd7ee. --- .../main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java index 47edd64411..e31373304e 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java @@ -276,7 +276,7 @@ private void cleanUpScheduler() { log.error("Protocol adapter with id {} failed to be started.", adapter.getId()); future.completeExceptionally(e); } - return future; + return Optional.empty(); } public @NotNull CompletableFuture stopAsync(final boolean destroy) { From fd00211615f7cc186d7bdd2ec4f1f6cd98790c3c Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Fri, 31 Oct 2025 11:31:49 +0100 Subject: [PATCH 04/16] Reapply "refactor: Let attemptStartingConsumers() return CompletableFuture instead of Optional" This reverts commit 8a51c29d5187ef480d5e9e3fd59ea7803373b161. --- .../main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java index e31373304e..47edd64411 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java @@ -276,7 +276,7 @@ private void cleanUpScheduler() { log.error("Protocol adapter with id {} failed to be started.", adapter.getId()); future.completeExceptionally(e); } - return Optional.empty(); + return future; } public @NotNull CompletableFuture stopAsync(final boolean destroy) { From cd005d02873a4b5aad2647b47a54381ce510d7fb Mon Sep 17 00:00:00 2001 From: marregui Date: Fri, 31 Oct 2025 13:09:21 +0100 Subject: [PATCH 05/16] add missing completion for future on adapter start --- .../java/com/hivemq/protocols/ProtocolAdapterWrapper.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java index 47edd64411..cee296d2b1 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java @@ -265,7 +265,11 @@ private void cleanUpScheduler() { if (futureCompleted.compareAndSet(false, true)) { future.complete(false); } + } else { + future.complete(true); } + } else { + future.complete(true); } }); } else { From fc50a9000c09c753105c71083689e78aa4ab0453 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Fri, 31 Oct 2025 15:37:59 +0100 Subject: [PATCH 06/16] fix: Revise connection status handling --- .../com/hivemq/protocols/ProtocolAdapterWrapper.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java index cee296d2b1..be9e0ebb52 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java @@ -265,11 +265,15 @@ private void cleanUpScheduler() { if (futureCompleted.compareAndSet(false, true)) { future.complete(false); } - } else { - future.complete(true); } - } else { - future.complete(true); + case ERROR -> { + if (futureCompleted.compareAndSet(false, true)) { + log.error("Failed to start writing for adapter with id {} because the status is {}.", + adapter.getId(), + status); + future.complete(true); + } + } } }); } else { From 1edb3d3efce0516966d1acb33a2c9edb4e2ac5e1 Mon Sep 17 00:00:00 2001 From: marregui Date: Fri, 31 Oct 2025 15:56:27 +0100 Subject: [PATCH 07/16] CHECKPOINT MIGUEL PollingTask (close scheduled task on cancel) PAStateImpl (last message is now atomicly written) Southbound writing now returns a future to wait on Milo cleanup --- .../hivemq/protocols/InternalProtocolAdapterWritingService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/InternalProtocolAdapterWritingService.java b/hivemq-edge/src/main/java/com/hivemq/protocols/InternalProtocolAdapterWritingService.java index 389b9f59ef..a9c645dd60 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/InternalProtocolAdapterWritingService.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/InternalProtocolAdapterWritingService.java @@ -18,12 +18,10 @@ import com.hivemq.adapter.sdk.api.services.ProtocolAdapterMetricsService; import com.hivemq.adapter.sdk.api.services.ProtocolAdapterWritingService; import com.hivemq.adapter.sdk.api.writing.WritingProtocolAdapter; -import com.hivemq.persistence.SingleWriterService; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; public interface InternalProtocolAdapterWritingService extends ProtocolAdapterWritingService { From 027f5ff34c2f9195e66915aee200fee0d5ca6b17 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Mon, 3 Nov 2025 11:56:09 +0100 Subject: [PATCH 08/16] fix: Add retry to getWritingSchema() --- .../impl/ProtocolAdaptersResourceImpl.java | 69 ++++++++++++------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdaptersResourceImpl.java b/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdaptersResourceImpl.java index 6c8f379092..6e64e65eb1 100644 --- a/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdaptersResourceImpl.java +++ b/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdaptersResourceImpl.java @@ -663,32 +663,51 @@ public int getDepth() { "' exists, but it does not support writing to PLCs.")); } - final TagSchemaCreationOutputImpl tagSchemaCreationOutput = new TagSchemaCreationOutputImpl(); - adapter.createTagSchema(new TagSchemaCreationInputImpl(decodedTagName), tagSchemaCreationOutput); - - try { - return Response.ok(tagSchemaCreationOutput.getFuture().get()).build(); // JSON schema root node - } catch (final @NotNull InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("Creation of json schema for writing to PLCs were interrupted."); - log.debug("Original exception: ", e); - return errorResponse(new InternalServerError(null)); - } catch (final @NotNull ExecutionException e) { - return switch (tagSchemaCreationOutput.getStatus()) { - case NOT_SUPPORTED -> errorResponse(new AdapterOperationNotSupportedError("Operation not supported:" + - e.getCause().getMessage())); - case ADAPTER_NOT_STARTED -> - errorResponse(new AdapterOperationNotSupportedError("Adapter not started: " + - e.getCause().getMessage())); - case TAG_NOT_FOUND -> errorResponse(new DomainTagNotFoundError(tagName)); - default -> { - log.warn("Exception was raised during creation of json schema for writing to PLCs."); - if (log.isDebugEnabled()) { - log.debug("Original exception: ", e); - } - yield errorResponse(new InternalServerError(null)); + final long startTime = System.currentTimeMillis(); + long endTime = startTime; + while (true) { + final TagSchemaCreationOutputImpl tagSchemaCreationOutput = new TagSchemaCreationOutputImpl(); + try { + adapter.createTagSchema(new TagSchemaCreationInputImpl(decodedTagName), tagSchemaCreationOutput); + return Response.ok(tagSchemaCreationOutput.getFuture().get()).build(); // JSON schema root node + } catch (final @NotNull InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Creation of json schema for writing to PLCs were interrupted."); + log.debug("Original exception: ", e); + if (endTime - startTime > RETRY_TIMEOUT_MILLIS) { + return errorResponse(new InternalServerError(null)); } - }; + } catch (final @NotNull ExecutionException e) { + if (endTime - startTime > RETRY_TIMEOUT_MILLIS) { + return switch (tagSchemaCreationOutput.getStatus()) { + case NOT_SUPPORTED -> + errorResponse(new AdapterOperationNotSupportedError("Operation not supported:" + + e.getCause().getMessage())); + case ADAPTER_NOT_STARTED -> + errorResponse(new AdapterOperationNotSupportedError("Adapter not started: " + + e.getCause().getMessage())); + case TAG_NOT_FOUND -> errorResponse(new DomainTagNotFoundError(tagName)); + default -> { + log.warn("Exception was raised during creation of json schema for writing to PLCs."); + if (log.isDebugEnabled()) { + log.debug("Original exception: ", e); + } + yield errorResponse(new InternalServerError(null)); + } + }; + } + } + try { + TimeUnit.MILLISECONDS.sleep(RETRY_INTERVAL_MILLIS); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Creation of json schema for writing to PLCs were interrupted."); + log.debug("Original exception: ", e); + if (endTime - startTime > RETRY_TIMEOUT_MILLIS) { + return errorResponse(new InternalServerError(null)); + } + } + endTime = System.currentTimeMillis(); } } From e544d704c59063b51e11593eeb8fd29b303cb719 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Mon, 3 Nov 2025 14:39:36 +0100 Subject: [PATCH 09/16] feat: Rebase to master --- ext/hivemq-edge-openapi-2025.19.yaml | 2 +- .../impl/ProtocolAdaptersResourceImpl.java | 69 +++++++------------ 2 files changed, 26 insertions(+), 45 deletions(-) diff --git a/ext/hivemq-edge-openapi-2025.19.yaml b/ext/hivemq-edge-openapi-2025.19.yaml index 1801f7b648..ccbe851878 100644 --- a/ext/hivemq-edge-openapi-2025.19.yaml +++ b/ext/hivemq-edge-openapi-2025.19.yaml @@ -14,7 +14,7 @@ info: ## OpenAPI HiveMQ's REST API provides an OpenAPI 3.0 schema definition that can imported into popular API tooling (e.g. Postman) or can be used to generate client-code for multiple programming languages. title: HiveMQ Edge REST API - version: 2025.14-SNAPSHOT + version: 2025.19-SNAPSHOT x-logo: url: https://www.hivemq.com/img/svg/hivemq-bee.svg tags: diff --git a/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdaptersResourceImpl.java b/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdaptersResourceImpl.java index 6e64e65eb1..6c8f379092 100644 --- a/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdaptersResourceImpl.java +++ b/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdaptersResourceImpl.java @@ -663,51 +663,32 @@ public int getDepth() { "' exists, but it does not support writing to PLCs.")); } - final long startTime = System.currentTimeMillis(); - long endTime = startTime; - while (true) { - final TagSchemaCreationOutputImpl tagSchemaCreationOutput = new TagSchemaCreationOutputImpl(); - try { - adapter.createTagSchema(new TagSchemaCreationInputImpl(decodedTagName), tagSchemaCreationOutput); - return Response.ok(tagSchemaCreationOutput.getFuture().get()).build(); // JSON schema root node - } catch (final @NotNull InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("Creation of json schema for writing to PLCs were interrupted."); - log.debug("Original exception: ", e); - if (endTime - startTime > RETRY_TIMEOUT_MILLIS) { - return errorResponse(new InternalServerError(null)); - } - } catch (final @NotNull ExecutionException e) { - if (endTime - startTime > RETRY_TIMEOUT_MILLIS) { - return switch (tagSchemaCreationOutput.getStatus()) { - case NOT_SUPPORTED -> - errorResponse(new AdapterOperationNotSupportedError("Operation not supported:" + - e.getCause().getMessage())); - case ADAPTER_NOT_STARTED -> - errorResponse(new AdapterOperationNotSupportedError("Adapter not started: " + - e.getCause().getMessage())); - case TAG_NOT_FOUND -> errorResponse(new DomainTagNotFoundError(tagName)); - default -> { - log.warn("Exception was raised during creation of json schema for writing to PLCs."); - if (log.isDebugEnabled()) { - log.debug("Original exception: ", e); - } - yield errorResponse(new InternalServerError(null)); - } - }; - } - } - try { - TimeUnit.MILLISECONDS.sleep(RETRY_INTERVAL_MILLIS); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("Creation of json schema for writing to PLCs were interrupted."); - log.debug("Original exception: ", e); - if (endTime - startTime > RETRY_TIMEOUT_MILLIS) { - return errorResponse(new InternalServerError(null)); + final TagSchemaCreationOutputImpl tagSchemaCreationOutput = new TagSchemaCreationOutputImpl(); + adapter.createTagSchema(new TagSchemaCreationInputImpl(decodedTagName), tagSchemaCreationOutput); + + try { + return Response.ok(tagSchemaCreationOutput.getFuture().get()).build(); // JSON schema root node + } catch (final @NotNull InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Creation of json schema for writing to PLCs were interrupted."); + log.debug("Original exception: ", e); + return errorResponse(new InternalServerError(null)); + } catch (final @NotNull ExecutionException e) { + return switch (tagSchemaCreationOutput.getStatus()) { + case NOT_SUPPORTED -> errorResponse(new AdapterOperationNotSupportedError("Operation not supported:" + + e.getCause().getMessage())); + case ADAPTER_NOT_STARTED -> + errorResponse(new AdapterOperationNotSupportedError("Adapter not started: " + + e.getCause().getMessage())); + case TAG_NOT_FOUND -> errorResponse(new DomainTagNotFoundError(tagName)); + default -> { + log.warn("Exception was raised during creation of json schema for writing to PLCs."); + if (log.isDebugEnabled()) { + log.debug("Original exception: ", e); + } + yield errorResponse(new InternalServerError(null)); } - } - endTime = System.currentTimeMillis(); + }; } } From 3b0374061b3dec026a014ab23dba5ac6f216d61b Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Fri, 7 Nov 2025 08:49:05 +0100 Subject: [PATCH 10/16] rebase: To master --- .../java/com/hivemq/protocols/ProtocolAdapterWrapper.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java index be9e0ebb52..47edd64411 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/ProtocolAdapterWrapper.java @@ -266,14 +266,6 @@ private void cleanUpScheduler() { future.complete(false); } } - case ERROR -> { - if (futureCompleted.compareAndSet(false, true)) { - log.error("Failed to start writing for adapter with id {} because the status is {}.", - adapter.getId(), - status); - future.complete(true); - } - } } }); } else { From 0eec40e05c23ec5a58aa8071ac2a8d8c96a8a6b6 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Fri, 7 Nov 2025 10:47:13 +0100 Subject: [PATCH 11/16] refactor: Add i18n --- .../fsm/I18nProtocolAdapterMessage.java | 58 ++++++++++ .../protocols/fsm/ProtocolAdapterState.java | 105 ++++++------------ .../ProtocolAdapterTransitionResponse.java | 57 +++++++++- .../fsm/ProtocolAdapterTransitionStatus.java | 1 + ...protocol-adapter-messages-en_US.properties | 4 + 5 files changed, 147 insertions(+), 78 deletions(-) create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java create mode 100644 hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java new file mode 100644 index 0000000000..e8559b847e --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import com.hivemq.common.i18n.I18nError; +import com.hivemq.common.i18n.I18nErrorTemplate; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public enum I18nProtocolAdapterMessage implements I18nError { + FSM_TRANSITION_FAILURE_TRANSITIONED_FROM_STATE_TO_ERROR, + FSM_TRANSITION_FAILURE_UNABLE_TO_TRANSITION_FROM_STATE_TO_STATE, + FSM_TRANSITION_SUCCESS_STATE_IS_NOT_TRANSITIONED, + FSM_TRANSITION_SUCCESS_TRANSITIONED_FROM_STATE_TO_STATE, + ; + + private static final @NotNull String RESOURCE_NAME_PREFIX = "templates/protocol-adapter-messages-"; + private static final @NotNull String RESOURCE_NAME_SUFFIX = ".properties"; + private static final @NotNull I18nErrorTemplate TEMPLATE = + new I18nErrorTemplate(locale -> RESOURCE_NAME_PREFIX + locale + RESOURCE_NAME_SUFFIX, + I18nProtocolAdapterMessage.class.getClassLoader()); + + private final @NotNull String key; + + I18nProtocolAdapterMessage() { + key = name().toLowerCase().replace("_", "."); + } + + @Override + public @NotNull String get(final @NotNull Map map) { + return TEMPLATE.get(this, map); + } + + @Override + public @NotNull String getKey() { + return key; + } + + @Override + public @NotNull String getName() { + return name(); + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java index d9e683b37b..2404476608 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java @@ -35,105 +35,62 @@ public enum ProtocolAdapterState { this.transitionFunction = transitionFunction; } - public static ProtocolAdapterTransitionResponse transitionFromStarted( + public static @NotNull ProtocolAdapterTransitionResponse transitionFromStarting( final @NotNull ProtocolAdapterState toState, final @NotNull ProtocolAdapterInstance instance) { - switch (toState) { - case Starting: - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); - case Started: - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Started); - case Stopping: - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopping); - case Stopped: - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped); - default: - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); - } + final ProtocolAdapterState fromState = ProtocolAdapterState.Starting; + return switch (toState) { + case Starting -> ProtocolAdapterTransitionResponse.notChanged(fromState); + case Started -> ProtocolAdapterTransitionResponse.success(fromState, toState); + case Stopping, Stopped -> ProtocolAdapterTransitionResponse.failure(fromState, toState); + default -> ProtocolAdapterTransitionResponse.failure(fromState); + }; } - public static ProtocolAdapterTransitionResponse transitionFromStarting( + public static @NotNull ProtocolAdapterTransitionResponse transitionFromStarted( final @NotNull ProtocolAdapterState toState, final @NotNull ProtocolAdapterInstance instance) { + final ProtocolAdapterState fromState = ProtocolAdapterState.Started; return switch (toState) { - case Starting -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); - case Started -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Started); - case Stopping -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopping); - case Stopped -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped); - default -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); + case Starting, Stopped -> ProtocolAdapterTransitionResponse.failure(fromState, toState); + case Started -> ProtocolAdapterTransitionResponse.notChanged(fromState); + case Stopping -> ProtocolAdapterTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterTransitionResponse.failure(fromState); }; } - public static @NotNull ProtocolAdapterTransitionResponse transitionFromStopped( + public static @NotNull ProtocolAdapterTransitionResponse transitionFromStopping( final @NotNull ProtocolAdapterState toState, final @NotNull ProtocolAdapterInstance instance) { + final ProtocolAdapterState fromState = ProtocolAdapterState.Stopping; return switch (toState) { - case Starting -> transitionFromStoppedToStarting(instance); - case Started, Stopping -> transitionToError(ProtocolAdapterState.Stopped, toState); - case Stopped -> transitionWithoutChanges(toState); - case Error -> transitionFromStoppedToError(instance); + case Starting, Started -> ProtocolAdapterTransitionResponse.failure(fromState, toState); + case Stopping -> ProtocolAdapterTransitionResponse.notChanged(fromState); + case Stopped -> ProtocolAdapterTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterTransitionResponse.failure(fromState); }; } - public static ProtocolAdapterTransitionResponse transitionFromStoppedToError(final @NotNull ProtocolAdapterInstance instance) { - try { - // Do something to error. -// instance.doSomething(); - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); - } catch (final Exception e) { - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error, - ProtocolAdapterTransitionStatus.Failure, - "Failed transition from Stopped to Error.", - e); - } - } - - public static ProtocolAdapterTransitionResponse transitionFromStoppedToStarting(final @NotNull ProtocolAdapterInstance instance) { - try { - // Do something to start the protocol adapter. - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); - } catch (final Exception e) { - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped, - ProtocolAdapterTransitionStatus.Failure, - "Failed transition from Stopped to Starting.", - e); - } - } - - public static @NotNull ProtocolAdapterTransitionResponse transitionWithoutChanges(final @NotNull ProtocolAdapterState toState) { - return new ProtocolAdapterTransitionResponse(toState); - } - - public static @NotNull ProtocolAdapterTransitionResponse transitionToError( - final @NotNull ProtocolAdapterState fromState, - final @NotNull ProtocolAdapterState toState) { - return new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error, - ProtocolAdapterTransitionStatus.Failure, - "Unable to transition from " + fromState + " to " + toState + ".", - null); - } - - public static ProtocolAdapterTransitionResponse transitionFromStopping( + public static @NotNull ProtocolAdapterTransitionResponse transitionFromStopped( final @NotNull ProtocolAdapterState toState, final @NotNull ProtocolAdapterInstance instance) { + final ProtocolAdapterState fromState = ProtocolAdapterState.Stopped; return switch (toState) { - case Starting -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); - case Started -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Started); - case Stopping -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopping); - case Stopped -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped); - default -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); + case Starting -> ProtocolAdapterTransitionResponse.success(fromState, toState); + case Started, Stopping -> ProtocolAdapterTransitionResponse.failure(fromState, toState); + case Stopped -> ProtocolAdapterTransitionResponse.notChanged(fromState); + case Error -> ProtocolAdapterTransitionResponse.failure(fromState); }; } - public static ProtocolAdapterTransitionResponse transitionFromError( + public static @NotNull ProtocolAdapterTransitionResponse transitionFromError( final @NotNull ProtocolAdapterState toState, final @NotNull ProtocolAdapterInstance instance) { + final ProtocolAdapterState fromState = ProtocolAdapterState.Error; return switch (toState) { - case Starting -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Starting); - case Started -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Started); - case Stopping -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopping); - case Stopped -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Stopped); - default -> new ProtocolAdapterTransitionResponse(ProtocolAdapterState.Error); + case Starting -> ProtocolAdapterTransitionResponse.success(fromState, toState); + case Started, Stopping, Stopped -> ProtocolAdapterTransitionResponse.failure(fromState, toState); + default -> ProtocolAdapterTransitionResponse.notChanged(toState); }; } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java index d572a51894..679bf962f4 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java @@ -18,9 +18,58 @@ import org.jetbrains.annotations.NotNull; -public record ProtocolAdapterTransitionResponse(ProtocolAdapterState state, ProtocolAdapterTransitionStatus status, - String message, Throwable error) { - public ProtocolAdapterTransitionResponse(final @NotNull ProtocolAdapterState state) { - this(state, ProtocolAdapterTransitionStatus.Success, null, null); +import java.util.Map; + +public record ProtocolAdapterTransitionResponse(ProtocolAdapterState fromState, ProtocolAdapterState toState, + ProtocolAdapterTransitionStatus status, String message, + Throwable error) { + + public static final String FROM_STATE = "fromState"; + public static final String TO_STATE = "toState"; + public static final String STATE = "state"; + + public static ProtocolAdapterTransitionResponse success( + final @NotNull ProtocolAdapterState fromState, + final @NotNull ProtocolAdapterState toState) { + return new ProtocolAdapterTransitionResponse(fromState, + toState, + ProtocolAdapterTransitionStatus.Success, + I18nProtocolAdapterMessage.FSM_TRANSITION_SUCCESS_TRANSITIONED_FROM_STATE_TO_STATE.get(Map.of(FROM_STATE, + fromState.name(), + TO_STATE, + toState.name())), + null); + } + + public static ProtocolAdapterTransitionResponse notChanged(final @NotNull ProtocolAdapterState state) { + return new ProtocolAdapterTransitionResponse(state, + state, + ProtocolAdapterTransitionStatus.NotChanged, + I18nProtocolAdapterMessage.FSM_TRANSITION_SUCCESS_STATE_IS_NOT_TRANSITIONED.get(Map.of(STATE, + state.name())), + null); + } + + public static ProtocolAdapterTransitionResponse failure(final @NotNull ProtocolAdapterState state) { + return new ProtocolAdapterTransitionResponse(state, + ProtocolAdapterState.Error, + ProtocolAdapterTransitionStatus.Failure, + I18nProtocolAdapterMessage.FSM_TRANSITION_FAILURE_TRANSITIONED_FROM_STATE_TO_ERROR.get(Map.of(STATE, + state.name())), + null); + } + + public static ProtocolAdapterTransitionResponse failure( + final @NotNull ProtocolAdapterState fromState, + final @NotNull ProtocolAdapterState toState) { + return new ProtocolAdapterTransitionResponse(fromState, + toState, + ProtocolAdapterTransitionStatus.Failure, + I18nProtocolAdapterMessage.FSM_TRANSITION_FAILURE_UNABLE_TO_TRANSITION_FROM_STATE_TO_STATE.get(Map.of( + FROM_STATE, + fromState.name(), + TO_STATE, + toState.name())), + null); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java index 201e447d38..c55edd5744 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java @@ -19,4 +19,5 @@ public enum ProtocolAdapterTransitionStatus { Success, Failure, + NotChanged, } diff --git a/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties b/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties new file mode 100644 index 0000000000..910d1e05a5 --- /dev/null +++ b/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties @@ -0,0 +1,4 @@ +fsm.transition.failure.unable.to.transition.from.state.to.state=Unable to transition from ${fromState} to ${toState}. +fsm.transition.failure.transitioned.from.state.to.error=Transitioned from ${state} to Error. +fsm.transition.success.state.is.not.transitioned=${state} is not transitioned. +fsm.transition.success.transitioned.from.state.to.state=Transitioned from ${fromState} to ${toState}. From a721194937432b46adc69dfe9f96640b9c4c668e Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Fri, 7 Nov 2025 14:35:32 +0100 Subject: [PATCH 12/16] feat: Add StringTemplate --- .../hivemq/common/i18n/StringTemplate.java | 50 +++++++++++ .../fsm/I18nProtocolAdapterMessage.java | 3 +- .../fsm/ProtocolAdapterInstance.java | 15 ++-- .../protocols/fsm/ProtocolAdapterState.java | 22 ++--- .../ProtocolAdapterTransitionResponse.java | 11 +-- ...protocol-adapter-messages-en_US.properties | 3 +- .../fsm/ProtocolAdapterStateTest.java | 82 +++++++++++++++++++ 7 files changed, 151 insertions(+), 35 deletions(-) create mode 100644 hivemq-edge/src/main/java/com/hivemq/common/i18n/StringTemplate.java create mode 100644 hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterStateTest.java diff --git a/hivemq-edge/src/main/java/com/hivemq/common/i18n/StringTemplate.java b/hivemq-edge/src/main/java/com/hivemq/common/i18n/StringTemplate.java new file mode 100644 index 0000000000..19158bb39a --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/common/i18n/StringTemplate.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.common.i18n; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import org.jetbrains.annotations.NotNull; + +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Map; + +public final class StringTemplate { + private final static @NotNull Configuration CONFIGURATION = new Configuration(Configuration.VERSION_2_3_22); + + static { + CONFIGURATION.setDefaultEncoding(StandardCharsets.UTF_8.name()); + CONFIGURATION.setLocale(Locale.US); + } + + private StringTemplate() { + } + + public static @NotNull String format( + final @NotNull String stringTemplate, + final @NotNull Map arguments) { + try (final StringWriter stringWriter = new StringWriter();) { + final Template template = new Template(stringTemplate, stringTemplate, CONFIGURATION); + template.process(arguments, stringWriter); + return stringWriter.toString(); + } catch (final Exception e) { + return e.getMessage(); + } + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java index e8559b847e..387f433a24 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java @@ -23,9 +23,8 @@ import java.util.Map; public enum I18nProtocolAdapterMessage implements I18nError { - FSM_TRANSITION_FAILURE_TRANSITIONED_FROM_STATE_TO_ERROR, FSM_TRANSITION_FAILURE_UNABLE_TO_TRANSITION_FROM_STATE_TO_STATE, - FSM_TRANSITION_SUCCESS_STATE_IS_NOT_TRANSITIONED, + FSM_TRANSITION_SUCCESS_STATE_IS_UNCHANGED, FSM_TRANSITION_SUCCESS_TRANSITIONED_FROM_STATE_TO_STATE, ; diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java index 5a9b53674e..3e4723e8cc 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java @@ -37,14 +37,13 @@ public ProtocolAdapterInstance() { public synchronized @NotNull ProtocolAdapterTransitionResponse transitionTo(final @NotNull ProtocolAdapterState newState) { final ProtocolAdapterTransitionResponse response = state.transition(newState, this); - if (response.status() == ProtocolAdapterTransitionStatus.Success) { - this.state = response.state(); - } else { - // Handle error (logging, throwing exception, etc.) - switch (response.status()) { - default -> { - // TODO - } + this.state = response.toState(); + switch (response.status()) { + case Success -> { + } + case Failure -> { + } + case NotChanged -> { } } return response; diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java index 2404476608..36225a408a 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java @@ -41,9 +41,8 @@ public enum ProtocolAdapterState { final ProtocolAdapterState fromState = ProtocolAdapterState.Starting; return switch (toState) { case Starting -> ProtocolAdapterTransitionResponse.notChanged(fromState); - case Started -> ProtocolAdapterTransitionResponse.success(fromState, toState); - case Stopping, Stopped -> ProtocolAdapterTransitionResponse.failure(fromState, toState); - default -> ProtocolAdapterTransitionResponse.failure(fromState); + case Started, Stopping, Error -> ProtocolAdapterTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterTransitionResponse.failure(fromState, toState); }; } @@ -52,10 +51,9 @@ public enum ProtocolAdapterState { final @NotNull ProtocolAdapterInstance instance) { final ProtocolAdapterState fromState = ProtocolAdapterState.Started; return switch (toState) { - case Starting, Stopped -> ProtocolAdapterTransitionResponse.failure(fromState, toState); case Started -> ProtocolAdapterTransitionResponse.notChanged(fromState); - case Stopping -> ProtocolAdapterTransitionResponse.success(fromState, toState); - default -> ProtocolAdapterTransitionResponse.failure(fromState); + case Stopping, Error -> ProtocolAdapterTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterTransitionResponse.failure(fromState, toState); }; } @@ -64,10 +62,9 @@ public enum ProtocolAdapterState { final @NotNull ProtocolAdapterInstance instance) { final ProtocolAdapterState fromState = ProtocolAdapterState.Stopping; return switch (toState) { - case Starting, Started -> ProtocolAdapterTransitionResponse.failure(fromState, toState); case Stopping -> ProtocolAdapterTransitionResponse.notChanged(fromState); - case Stopped -> ProtocolAdapterTransitionResponse.success(fromState, toState); - default -> ProtocolAdapterTransitionResponse.failure(fromState); + case Stopped, Error -> ProtocolAdapterTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterTransitionResponse.failure(fromState, toState); }; } @@ -77,9 +74,8 @@ public enum ProtocolAdapterState { final ProtocolAdapterState fromState = ProtocolAdapterState.Stopped; return switch (toState) { case Starting -> ProtocolAdapterTransitionResponse.success(fromState, toState); - case Started, Stopping -> ProtocolAdapterTransitionResponse.failure(fromState, toState); case Stopped -> ProtocolAdapterTransitionResponse.notChanged(fromState); - case Error -> ProtocolAdapterTransitionResponse.failure(fromState); + default -> ProtocolAdapterTransitionResponse.failure(fromState, toState); }; } @@ -89,8 +85,8 @@ public enum ProtocolAdapterState { final ProtocolAdapterState fromState = ProtocolAdapterState.Error; return switch (toState) { case Starting -> ProtocolAdapterTransitionResponse.success(fromState, toState); - case Started, Stopping, Stopped -> ProtocolAdapterTransitionResponse.failure(fromState, toState); - default -> ProtocolAdapterTransitionResponse.notChanged(toState); + case Error -> ProtocolAdapterTransitionResponse.notChanged(fromState); + default -> ProtocolAdapterTransitionResponse.failure(fromState, toState); }; } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java index 679bf962f4..e81d7afb77 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionResponse.java @@ -45,16 +45,7 @@ public static ProtocolAdapterTransitionResponse notChanged(final @NotNull Protoc return new ProtocolAdapterTransitionResponse(state, state, ProtocolAdapterTransitionStatus.NotChanged, - I18nProtocolAdapterMessage.FSM_TRANSITION_SUCCESS_STATE_IS_NOT_TRANSITIONED.get(Map.of(STATE, - state.name())), - null); - } - - public static ProtocolAdapterTransitionResponse failure(final @NotNull ProtocolAdapterState state) { - return new ProtocolAdapterTransitionResponse(state, - ProtocolAdapterState.Error, - ProtocolAdapterTransitionStatus.Failure, - I18nProtocolAdapterMessage.FSM_TRANSITION_FAILURE_TRANSITIONED_FROM_STATE_TO_ERROR.get(Map.of(STATE, + I18nProtocolAdapterMessage.FSM_TRANSITION_SUCCESS_STATE_IS_UNCHANGED.get(Map.of(STATE, state.name())), null); } diff --git a/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties b/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties index 910d1e05a5..544398746d 100644 --- a/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties +++ b/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties @@ -1,4 +1,3 @@ fsm.transition.failure.unable.to.transition.from.state.to.state=Unable to transition from ${fromState} to ${toState}. -fsm.transition.failure.transitioned.from.state.to.error=Transitioned from ${state} to Error. -fsm.transition.success.state.is.not.transitioned=${state} is not transitioned. +fsm.transition.success.state.is.unchanged=${state} is unchanged. fsm.transition.success.transitioned.from.state.to.state=Transitioned from ${fromState} to ${toState}. diff --git a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterStateTest.java b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterStateTest.java new file mode 100644 index 0000000000..07032fbc58 --- /dev/null +++ b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterStateTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import com.hivemq.common.i18n.StringTemplate; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class ProtocolAdapterStateTest { + private static final Map> PROTOCOL_ADAPTER_STATE_MAP_MAP = Map.of( + ProtocolAdapterState.Starting, + Set.of(ProtocolAdapterState.Started, ProtocolAdapterState.Stopping, ProtocolAdapterState.Error), + ProtocolAdapterState.Started, + Set.of(ProtocolAdapterState.Stopping, ProtocolAdapterState.Error), + ProtocolAdapterState.Stopping, + Set.of(ProtocolAdapterState.Stopped, ProtocolAdapterState.Error), + ProtocolAdapterState.Stopped, + Set.of(ProtocolAdapterState.Starting), + ProtocolAdapterState.Error, + Set.of(ProtocolAdapterState.Starting)); + @Mock + private @NotNull ProtocolAdapterInstance protocolAdapterInstance; + + @Test + public void whenEverythingWorks_thenTransitionShouldWork() { + final List states = List.of(ProtocolAdapterState.values()); + states.forEach(fromState -> { + final Set possibleToStates = PROTOCOL_ADAPTER_STATE_MAP_MAP.get(fromState); + assertThat(possibleToStates).isNotNull(); + states.forEach(toState -> { + final ProtocolAdapterTransitionResponse response = + fromState.transition(toState, protocolAdapterInstance); + switch (response.status()) { + case Success -> { + assertThat(possibleToStates).contains(toState); + assertThat(response.message()).isEqualTo(StringTemplate.format( + "Transitioned from ${fromState} to ${toState}.", + Map.of("fromState", fromState, "toState", toState))); + } + case Failure -> { + assertThat(possibleToStates).doesNotContain(toState); + assertThat(response.message()).isEqualTo(StringTemplate.format( + "Unable to transition from ${fromState} to ${toState}.", + Map.of("fromState", fromState, "toState", toState))); + } + case NotChanged -> { + assertThat(toState).isEqualTo(fromState); + assertThat(response.message()).isEqualTo(StringTemplate.format("${state} is unchanged.", + Map.of("state", fromState))); + } + } + }); + }); + } +} From 01a1b46a3e618c7fb0994045d121c927b6ea182a Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Sat, 8 Nov 2025 08:23:29 +0100 Subject: [PATCH 13/16] feat: FSM 1 --- .../fsm/ProtocolAdapterInstance.java | 62 +++++- .../fsm/ProtocolAdapterOperator.java | 201 ++++++++++++++++++ .../fsm/ProtocolAdapterOperatorState.java | 23 ++ .../fsm/ProtocolAdapterTransitionStatus.java | 5 + 4 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperatorState.java diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java index 3e4723e8cc..4adc80891c 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java @@ -16,34 +16,80 @@ package com.hivemq.protocols.fsm; +import com.hivemq.adapter.sdk.api.ProtocolAdapter; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ProtocolAdapterInstance { - protected @NotNull ProtocolAdapterState state; - protected @NotNull ProtocolAdapterConnectionState connectionState; + private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolAdapterInstance.class); + protected final @NotNull ProtocolAdapter adapter; + protected volatile @NotNull ProtocolAdapterState state; + protected volatile @NotNull ProtocolAdapterConnectionState northboundConnectionState; + protected volatile @NotNull ProtocolAdapterConnectionState southboundConnectionState; - public ProtocolAdapterInstance() { - connectionState = ProtocolAdapterConnectionState.Closed; + public ProtocolAdapterInstance(final @NotNull ProtocolAdapter adapter) { + this.adapter = adapter; + northboundConnectionState = ProtocolAdapterConnectionState.Closed; + southboundConnectionState = ProtocolAdapterConnectionState.Closed; state = ProtocolAdapterState.Stopped; } + public @NotNull ProtocolAdapterConnectionState getSouthboundConnectionState() { + return southboundConnectionState; + } + public @NotNull ProtocolAdapterState getState() { return state; } - public @NotNull ProtocolAdapterConnectionState getConnectionState() { - return connectionState; + public @NotNull ProtocolAdapterConnectionState getNorthboundConnectionState() { + return northboundConnectionState; + } + + public @NotNull String getAdapterId() { + return adapter.getId(); + } + + public void start() { + final ProtocolAdapterTransitionResponse response = transitionTo(ProtocolAdapterState.Starting); + if (response.status().isSuccess()) { + startNorthbound(); + startSouthbound(); + } + } + + public void stop() { + transitionTo(ProtocolAdapterState.Stopping); + } + + protected void startNorthbound() { + + } + + protected void startSouthbound() { + } public synchronized @NotNull ProtocolAdapterTransitionResponse transitionTo(final @NotNull ProtocolAdapterState newState) { - final ProtocolAdapterTransitionResponse response = state.transition(newState, this); - this.state = response.toState(); + final ProtocolAdapterState fromState = state; + final ProtocolAdapterTransitionResponse response = fromState.transition(newState, this); + state = response.toState(); switch (response.status()) { case Success -> { + LOGGER.debug("Protocol adapter '{}' transitioned from {} to {} successfully.", + fromState, + state, + getAdapterId()); } case Failure -> { + LOGGER.error("Protocol adapter '{}' failed to transition from {} to {}.", + fromState, + state, + getAdapterId()); } case NotChanged -> { + LOGGER.warn("Protocol adapter '{}' state {} is unchanged.", state, getAdapterId()); } } return response; diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java index 4bfede77ae..382ce8f7dd 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java @@ -16,5 +16,206 @@ package com.hivemq.protocols.fsm; +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.Sets; +import com.hivemq.adapter.sdk.api.events.EventService; +import com.hivemq.adapter.sdk.api.events.model.Event; +import com.hivemq.configuration.entity.adapter.ProtocolAdapterEntity; +import com.hivemq.configuration.reader.ProtocolAdapterExtractor; +import com.hivemq.edge.HiveMQEdgeRemoteService; +import com.hivemq.edge.VersionProvider; +import com.hivemq.edge.modules.adapters.data.TagManager; +import com.hivemq.edge.modules.adapters.impl.ModuleServicesImpl; +import com.hivemq.edge.modules.api.adapters.ProtocolAdapterPollingService; +import com.hivemq.protocols.InternalProtocolAdapterWritingService; +import com.hivemq.protocols.ProtocolAdapterConfig; +import com.hivemq.protocols.ProtocolAdapterConfigConverter; +import com.hivemq.protocols.ProtocolAdapterFactoryManager; +import com.hivemq.protocols.ProtocolAdapterMetrics; +import com.hivemq.protocols.northbound.NorthboundConsumerFactory; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.stream.Collectors; + + public class ProtocolAdapterOperator { + private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolAdapterOperator.class); + + private final @NotNull Map protocolAdapterMap; + private final @NotNull MetricRegistry metricRegistry; + private final @NotNull ModuleServicesImpl moduleServices; + private final @NotNull HiveMQEdgeRemoteService remoteService; + private final @NotNull EventService eventService; + private final @NotNull ProtocolAdapterConfigConverter configConverter; + private final @NotNull VersionProvider versionProvider; + private final @NotNull ProtocolAdapterPollingService protocolAdapterPollingService; + private final @NotNull ProtocolAdapterMetrics protocolAdapterMetrics; + private final @NotNull InternalProtocolAdapterWritingService protocolAdapterWritingService; + private final @NotNull ProtocolAdapterFactoryManager protocolAdapterFactoryManager; + private final @NotNull NorthboundConsumerFactory northboundConsumerFactory; + private final @NotNull TagManager tagManager; + private final @NotNull ProtocolAdapterExtractor protocolAdapterConfig; + private final @NotNull ExecutorService executorService; + private volatile @NotNull ProtocolAdapterOperatorState state; + + public ProtocolAdapterOperator( + final @NotNull MetricRegistry metricRegistry, + final @NotNull ModuleServicesImpl moduleServices, + final @NotNull HiveMQEdgeRemoteService remoteService, + final @NotNull EventService eventService, + final @NotNull ProtocolAdapterConfigConverter configConverter, + final @NotNull VersionProvider versionProvider, + final @NotNull ProtocolAdapterPollingService protocolAdapterPollingService, + final @NotNull ProtocolAdapterMetrics protocolAdapterMetrics, + final @NotNull InternalProtocolAdapterWritingService protocolAdapterWritingService, + final @NotNull ProtocolAdapterFactoryManager protocolAdapterFactoryManager, + final @NotNull NorthboundConsumerFactory northboundConsumerFactory, + final @NotNull TagManager tagManager, + final @NotNull ProtocolAdapterExtractor protocolAdapterConfig) { + this.protocolAdapterMap = new ConcurrentHashMap<>(); + this.metricRegistry = metricRegistry; + this.moduleServices = moduleServices; + this.remoteService = remoteService; + this.eventService = eventService; + this.configConverter = configConverter; + this.versionProvider = versionProvider; + this.protocolAdapterPollingService = protocolAdapterPollingService; + this.protocolAdapterMetrics = protocolAdapterMetrics; + this.protocolAdapterWritingService = protocolAdapterWritingService; + this.protocolAdapterFactoryManager = protocolAdapterFactoryManager; + this.northboundConsumerFactory = northboundConsumerFactory; + this.tagManager = tagManager; + this.protocolAdapterConfig = protocolAdapterConfig; + this.executorService = Executors.newSingleThreadExecutor(); + this.state = ProtocolAdapterOperatorState.Idle; + Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdown)); + protocolAdapterWritingService.addWritingChangedCallback(() -> protocolAdapterFactoryManager.writingEnabledChanged( + protocolAdapterWritingService.writingEnabled())); + } + + public @NotNull ProtocolAdapterOperatorState getState() { + return state; + } + + public void start() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Starting adapters"); + } + protocolAdapterConfig.registerConsumer(this::refresh); + } + + public void refresh(final @NotNull List configs) { + executorService.submit(() -> { + state = ProtocolAdapterOperatorState.Running; + LOGGER.info("Refreshing adapters"); + + final Map protocolAdapterConfigs = configs.stream() + .map(configConverter::fromEntity) + .collect(Collectors.toMap(ProtocolAdapterConfig::getAdapterId, Function.identity())); + + final Set oldProtocolAdapterIdSet = new HashSet<>(protocolAdapterMap.keySet()); + final Set newProtocolAdapterIdSet = new HashSet<>(protocolAdapterConfigs.keySet()); + + final Set toBeDeletedProtocolAdapterIdSet = + new HashSet<>(Sets.difference(oldProtocolAdapterIdSet, newProtocolAdapterIdSet)); + final Set toBeCreatedProtocolAdapterIdSet = + new HashSet<>(Sets.difference(newProtocolAdapterIdSet, oldProtocolAdapterIdSet)); + final Set toBeUpdatedProtocolAdapterIdSet = + new HashSet<>(Sets.intersection(newProtocolAdapterIdSet, oldProtocolAdapterIdSet)); + + final List failedAdapters = new ArrayList<>(); + + toBeDeletedProtocolAdapterIdSet.forEach(adapterId -> { +// try { +// if (LOGGER.isDebugEnabled()) { +// LOGGER.debug("Deleting adapter '{}'", adapterId); +// } +// stopAsync(adapterId, true).whenComplete((ignored, t) -> deleteAdapterInternal(adapterId)).get(); +// } catch (final InterruptedException e) { +// Thread.currentThread().interrupt(); +// failedAdapters.add(adapterId); +// LOGGER.error("Interrupted while deleting adapter {}", adapterId, e); +// } catch (final ExecutionException e) { +// failedAdapters.add(adapterId); +// LOGGER.error("Failed deleting adapter {}", adapterId, e); +// } + }); + + toBeCreatedProtocolAdapterIdSet.forEach(name -> { +// try { +// if (LOGGER.isDebugEnabled()) { +// LOGGER.debug("Creating adapter '{}'", name); +// } +// startAsync(createAdapterInternal(protocolAdapterConfigs.get(name), +// versionProvider.getVersion())).get(); +// } catch (final InterruptedException e) { +// Thread.currentThread().interrupt(); +// failedAdapters.add(name); +// LOGGER.error("Interrupted while adding adapter {}", name, e); +// } catch (final ExecutionException e) { +// failedAdapters.add(name); +// LOGGER.error("Failed adding adapter {}", name, e); +// } + }); + + toBeUpdatedProtocolAdapterIdSet.forEach(name -> { +// try { +// final var wrapper = protocolAdapters.get(name); +// if (wrapper == null) { +// LOGGER.error( +// "Existing adapters were modified while a refresh was ongoing, adapter with name '{}' was deleted and could not be updated", +// name); +// } +// if (wrapper != null && !protocolAdapterConfigs.get(name).equals(wrapper.getConfig())) { +// if (LOGGER.isDebugEnabled()) { +// LOGGER.debug("Updating adapter '{}'", name); +// } +// stopAsync(name, true).thenApply(v -> { +// deleteAdapterInternal(name); +// return null; +// }) +// .thenCompose(ignored -> startAsync(createAdapterInternal(protocolAdapterConfigs.get(name), +// versionProvider.getVersion()))) +// .get(); +// } else { +// if (LOGGER.isDebugEnabled()) { +// LOGGER.debug("Not-updating adapter '{}' since the config is unchanged", name); +// } +// } +// } catch (final InterruptedException e) { +// Thread.currentThread().interrupt(); +// failedAdapters.add(name); +// LOGGER.error("Interrupted while updating adapter {}", name, e); +// } catch (final ExecutionException e) { +// failedAdapters.add(name); +// LOGGER.error("Failed updating adapter {}", name, e); +// } + }); + + if (failedAdapters.isEmpty()) { + eventService.configurationEvent() + .withSeverity(Event.SEVERITY.INFO) + .withMessage("Configuration has been successfully updated") + .fire(); + } else { + eventService.configurationEvent() + .withSeverity(Event.SEVERITY.CRITICAL) + .withMessage("Reloading of configuration failed") + .fire(); + } + state = ProtocolAdapterOperatorState.Idle; + }); + } } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperatorState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperatorState.java new file mode 100644 index 0000000000..8672ea84f9 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperatorState.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +public enum ProtocolAdapterOperatorState { + Idle, + Running, + ; +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java index c55edd5744..e957722713 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionStatus.java @@ -20,4 +20,9 @@ public enum ProtocolAdapterTransitionStatus { Success, Failure, NotChanged, + ; + + public boolean isSuccess() { + return this == Success; + } } From dabeefebed6cc55a13d39ea85c9f7181e3e27333 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Tue, 11 Nov 2025 16:11:25 +0100 Subject: [PATCH 14/16] feat: FSM 2 --- ...rTransition.java => ClassLoaderUtils.java} | 22 +- .../fsm/I18nProtocolAdapterMessage.java | 2 + .../fsm/ProtocolAdapterConnectionState.java | 32 ++ .../fsm/ProtocolAdapterManager2.java | 351 ++++++++++++++++++ ....java => ProtocolAdapterManagerState.java} | 2 +- .../fsm/ProtocolAdapterOperator.java | 221 ----------- .../protocols/fsm/ProtocolAdapterState.java | 47 ++- .../fsm/ProtocolAdapterTransitionRequest.java | 49 --- ...ance.java => ProtocolAdapterWrapper2.java} | 60 ++- ...protocol-adapter-messages-en_US.properties | 2 + .../fsm/ProtocolAdapterStateTest.java | 7 +- .../fsm/ProtocolAdapterWrapperTest.java | 52 +++ 12 files changed, 529 insertions(+), 318 deletions(-) rename hivemq-edge/src/main/java/com/hivemq/protocols/fsm/{ProtocolAdapterTransition.java => ClassLoaderUtils.java} (52%) create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterManager2.java rename hivemq-edge/src/main/java/com/hivemq/protocols/fsm/{ProtocolAdapterOperatorState.java => ProtocolAdapterManagerState.java} (94%) delete mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java delete mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionRequest.java rename hivemq-edge/src/main/java/com/hivemq/protocols/fsm/{ProtocolAdapterInstance.java => ProtocolAdapterWrapper2.java} (57%) create mode 100644 hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapperTest.java diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransition.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ClassLoaderUtils.java similarity index 52% rename from hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransition.java rename to hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ClassLoaderUtils.java index b28b114a98..ea3f68d560 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransition.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ClassLoaderUtils.java @@ -18,9 +18,21 @@ import org.jetbrains.annotations.NotNull; -public interface ProtocolAdapterTransition { - @NotNull ProtocolAdapterTransitionResponse transition( - final @NotNull ProtocolAdapterState toState, - final @NotNull ProtocolAdapterInstance session, - final @NotNull ProtocolAdapterTransitionRequest request); +import java.util.function.Supplier; + +public final class ClassLoaderUtils { + private ClassLoaderUtils() { + } + + public static @NotNull T runWithContextLoader( + final @NotNull ClassLoader contextLoader, + final @NotNull Supplier wrapperSupplier) { + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(contextLoader); + return wrapperSupplier.get(); + } finally { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java index 387f433a24..9b873e656c 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java @@ -26,6 +26,8 @@ public enum I18nProtocolAdapterMessage implements I18nError { FSM_TRANSITION_FAILURE_UNABLE_TO_TRANSITION_FROM_STATE_TO_STATE, FSM_TRANSITION_SUCCESS_STATE_IS_UNCHANGED, FSM_TRANSITION_SUCCESS_TRANSITIONED_FROM_STATE_TO_STATE, + PROTOCOL_ADAPTER_MANAGER_PROTOCOL_ADAPTER_DELETED, + PROTOCOL_ADAPTER_MANAGER_PROTOCOL_ADAPTER_NOT_FOUND, ; private static final @NotNull String RESOURCE_NAME_PREFIX = "templates/protocol-adapter-messages-"; diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java index 17d87c49ad..99b6aabb1e 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java @@ -42,4 +42,36 @@ public enum ProtocolAdapterConnectionState { final @NotNull Object context) { return transitionFunction.apply(context); } + + public boolean isClosed() { + return this == Closed; + } + + public boolean isClosing() { + return this == Closing; + } + + public boolean isConnected() { + return this == Connected; + } + + public boolean isConnecting() { + return this == Connecting; + } + + public boolean isDisconnecting() { + return this == Disconnecting; + } + + public boolean isDisconnected() { + return this == Disconnected; + } + + public boolean isError() { + return this == Error; + } + + public boolean isErrorClosing() { + return this == ErrorClosing; + } } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterManager2.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterManager2.java new file mode 100644 index 0000000000..208a0a47b6 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterManager2.java @@ -0,0 +1,351 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import com.hivemq.adapter.sdk.api.ProtocolAdapter; +import com.hivemq.adapter.sdk.api.events.EventService; +import com.hivemq.adapter.sdk.api.events.model.Event; +import com.hivemq.adapter.sdk.api.exceptions.ProtocolAdapterException; +import com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory; +import com.hivemq.adapter.sdk.api.services.ProtocolAdapterMetricsService; +import com.hivemq.configuration.entity.adapter.ProtocolAdapterEntity; +import com.hivemq.configuration.reader.ProtocolAdapterExtractor; +import com.hivemq.edge.HiveMQEdgeRemoteService; +import com.hivemq.edge.VersionProvider; +import com.hivemq.edge.modules.adapters.data.TagManager; +import com.hivemq.edge.modules.adapters.impl.ModuleServicesImpl; +import com.hivemq.edge.modules.adapters.impl.ModuleServicesPerModuleImpl; +import com.hivemq.edge.modules.adapters.impl.ProtocolAdapterStateImpl; +import com.hivemq.edge.modules.adapters.metrics.ProtocolAdapterMetricsServiceImpl; +import com.hivemq.edge.modules.api.adapters.ProtocolAdapterPollingService; +import com.hivemq.protocols.InternalProtocolAdapterWritingService; +import com.hivemq.protocols.ProtocolAdapterConfig; +import com.hivemq.protocols.ProtocolAdapterConfigConverter; +import com.hivemq.protocols.ProtocolAdapterFactoryManager; +import com.hivemq.protocols.ProtocolAdapterInputImpl; +import com.hivemq.protocols.ProtocolAdapterMetrics; +import com.hivemq.protocols.northbound.NorthboundConsumerFactory; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.stream.Collectors; + + +public class ProtocolAdapterManager2 { + public static final String ADAPTER_ID = "adapterId"; + private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolAdapterManager2.class); + private final @NotNull Map protocolAdapterMap; + private final @NotNull MetricRegistry metricRegistry; + private final @NotNull ModuleServicesImpl moduleServices; + private final @NotNull HiveMQEdgeRemoteService remoteService; + private final @NotNull EventService eventService; + private final @NotNull ProtocolAdapterConfigConverter configConverter; + private final @NotNull VersionProvider versionProvider; + private final @NotNull ProtocolAdapterPollingService protocolAdapterPollingService; + private final @NotNull ProtocolAdapterMetrics protocolAdapterMetrics; + private final @NotNull InternalProtocolAdapterWritingService protocolAdapterWritingService; + private final @NotNull ProtocolAdapterFactoryManager protocolAdapterFactoryManager; + private final @NotNull NorthboundConsumerFactory northboundConsumerFactory; + private final @NotNull TagManager tagManager; + private final @NotNull ProtocolAdapterExtractor protocolAdapterConfig; + private final @NotNull ExecutorService executorService; + private final @NotNull ReentrantReadWriteLock readWriteLock; + + public ProtocolAdapterManager2( + final @NotNull MetricRegistry metricRegistry, + final @NotNull ModuleServicesImpl moduleServices, + final @NotNull HiveMQEdgeRemoteService remoteService, + final @NotNull EventService eventService, + final @NotNull ProtocolAdapterConfigConverter configConverter, + final @NotNull VersionProvider versionProvider, + final @NotNull ProtocolAdapterPollingService protocolAdapterPollingService, + final @NotNull ProtocolAdapterMetrics protocolAdapterMetrics, + final @NotNull InternalProtocolAdapterWritingService protocolAdapterWritingService, + final @NotNull ProtocolAdapterFactoryManager protocolAdapterFactoryManager, + final @NotNull NorthboundConsumerFactory northboundConsumerFactory, + final @NotNull TagManager tagManager, + final @NotNull ProtocolAdapterExtractor protocolAdapterConfig) { + this.protocolAdapterMap = new HashMap<>(); + this.metricRegistry = metricRegistry; + this.moduleServices = moduleServices; + this.remoteService = remoteService; + this.eventService = eventService; + this.configConverter = configConverter; + this.versionProvider = versionProvider; + this.protocolAdapterPollingService = protocolAdapterPollingService; + this.protocolAdapterMetrics = protocolAdapterMetrics; + this.protocolAdapterWritingService = protocolAdapterWritingService; + this.protocolAdapterFactoryManager = protocolAdapterFactoryManager; + this.northboundConsumerFactory = northboundConsumerFactory; + this.tagManager = tagManager; + this.protocolAdapterConfig = protocolAdapterConfig; + this.executorService = Executors.newSingleThreadExecutor(); + this.readWriteLock = new ReentrantReadWriteLock(); + Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdown)); + protocolAdapterWritingService.addWritingChangedCallback(() -> protocolAdapterFactoryManager.writingEnabledChanged( + protocolAdapterWritingService.writingEnabled())); + } + + public boolean isBusy() { + return readWriteLock.isWriteLocked(); + } + + public void register() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Starting adapters"); + } + protocolAdapterConfig.registerConsumer(this::refresh); + } + + public @NotNull Optional getProtocolAdapterWrapperByAdapterId(final @NotNull String adapterId) { + Preconditions.checkNotNull(adapterId); + final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); + try { + readLock.lock(); + return Optional.ofNullable(protocolAdapterMap.get(adapterId)); + } finally { + readLock.unlock(); + } + } + + protected @NotNull Set getProtocolAdapterIdSet() { + final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); + try { + readLock.lock(); + return new HashSet<>(protocolAdapterMap.keySet()); + } finally { + readLock.unlock(); + } + } + + protected void createProtocolAdapter(final @NotNull ProtocolAdapterConfig config, final @NotNull String version) { + Preconditions.checkNotNull(config); + final String adapterId = config.getAdapterId(); + final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); + try { + writeLock.lock(); + if (!protocolAdapterMap.containsKey(adapterId)) { + final String configProtocolId = config.getProtocolId(); + // legacy handling, hardcoded here, to not add legacy stuff into the adapter-sdk + final String adapterType = switch (configProtocolId) { + case "ethernet-ip" -> "eip"; + case "opc-ua-client" -> "opcua"; + case "file_input" -> "file"; + default -> configProtocolId; + }; + + final Optional> maybeFactory = protocolAdapterFactoryManager.get(adapterType); + if (maybeFactory.isEmpty()) { + throw new IllegalArgumentException("Protocol adapter for config " + adapterType + " not found."); + } + final ProtocolAdapterFactory factory = maybeFactory.get(); + + LOGGER.info("Found configuration for adapter {} / {}", config.getAdapterId(), adapterType); + config.missingTags().ifPresent(missingTag -> { + throw new IllegalArgumentException("Tags used in mappings but not configured in adapter " + + adapterType + + ": " + + missingTag); + }); + + final ProtocolAdapterWrapper2 wrapper = + ClassLoaderUtils.runWithContextLoader(factory.getClass().getClassLoader(), () -> { + final ProtocolAdapterMetricsService metricsService = new ProtocolAdapterMetricsServiceImpl( + configProtocolId, + config.getAdapterId(), + metricRegistry); + final ProtocolAdapterStateImpl state = + new ProtocolAdapterStateImpl(moduleServices.eventService(), + config.getAdapterId(), + configProtocolId); + final ModuleServicesPerModuleImpl perModule = + new ModuleServicesPerModuleImpl(moduleServices.adapterPublishService(), + eventService, + protocolAdapterWritingService, + tagManager); + final ProtocolAdapter protocolAdapter = factory.createAdapter(factory.getInformation(), + new ProtocolAdapterInputImpl(configProtocolId, + config.getAdapterId(), + config.getAdapterConfig(), + config.getTags(), + config.getNorthboundMappings(), + version, + state, + perModule, + metricsService)); + // hen-egg problem. Rather solve this here as have not final fields in the adapter. + perModule.setAdapter(protocolAdapter); + protocolAdapterMetrics.increaseProtocolAdapterMetric(configProtocolId); + return new ProtocolAdapterWrapper2(protocolAdapter); + }); + protocolAdapterMap.put(adapterId, wrapper); + } + } finally { + writeLock.unlock(); + } + } + + protected @NotNull Optional deleteProtocolAdapterWrapperByAdapterId(final @NotNull String adapterId) { + Preconditions.checkNotNull(adapterId); + final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); + try { + writeLock.lock(); + return Optional.ofNullable(protocolAdapterMap.remove(adapterId)); + } finally { + writeLock.unlock(); + } + } + + public void start(final @NotNull String adapterId) throws ProtocolAdapterException { + final var optionalWrapper = getProtocolAdapterWrapperByAdapterId(adapterId); + if (optionalWrapper.isEmpty()) { + throw new ProtocolAdapterException(I18nProtocolAdapterMessage.PROTOCOL_ADAPTER_MANAGER_PROTOCOL_ADAPTER_NOT_FOUND.get( + Map.of(ADAPTER_ID, adapterId))); + } + optionalWrapper.get().start(); + } + + public void stop(final @NotNull String adapterId, final boolean destroy) throws ProtocolAdapterException { + final var optionalWrapper = getProtocolAdapterWrapperByAdapterId(adapterId); + if (optionalWrapper.isEmpty()) { + throw new ProtocolAdapterException(I18nProtocolAdapterMessage.PROTOCOL_ADAPTER_MANAGER_PROTOCOL_ADAPTER_NOT_FOUND.get( + Map.of(ADAPTER_ID, adapterId))); + } + optionalWrapper.get().stop(destroy); + } + + protected void deleteProtocolAdapterByAdapterId(final @NotNull String adapterId) { + deleteProtocolAdapterWrapperByAdapterId(adapterId).ifPresentOrElse(wrapper -> { + final String protocolId = wrapper.getProtocolAdapterInformation().getProtocolId(); + protocolAdapterMetrics.decreaseProtocolAdapterMetric(protocolId); + eventService.createAdapterEvent(adapterId, protocolId) + .withSeverity(Event.SEVERITY.WARN) + .withMessage(I18nProtocolAdapterMessage.PROTOCOL_ADAPTER_MANAGER_PROTOCOL_ADAPTER_DELETED.get(Map.of( + ADAPTER_ID, + adapterId))) + .fire(); + }, () -> LOGGER.warn("Tried to delete adapter '{}' but it was not found in the system.", adapterId)); + } + + protected void refresh(final @NotNull List configs) { + executorService.submit(() -> { + LOGGER.info("Refreshing adapters"); + try { + final Map protocolAdapterConfigs = configs.stream() + .map(configConverter::fromEntity) + .collect(Collectors.toMap(ProtocolAdapterConfig::getAdapterId, Function.identity())); + + final Set oldProtocolAdapterIdSet = getProtocolAdapterIdSet(); + final Set newProtocolAdapterIdSet = new HashSet<>(protocolAdapterConfigs.keySet()); + + final Set toBeDeletedProtocolAdapterIdSet = + new HashSet<>(Sets.difference(oldProtocolAdapterIdSet, newProtocolAdapterIdSet)); + final Set toBeCreatedProtocolAdapterIdSet = + new HashSet<>(Sets.difference(newProtocolAdapterIdSet, oldProtocolAdapterIdSet)); + final Set toBeUpdatedProtocolAdapterIdSet = + new HashSet<>(Sets.intersection(newProtocolAdapterIdSet, oldProtocolAdapterIdSet)); + + final Set failedAdapterSet = new HashSet<>(); + + CompletableFuture.allOf(toBeDeletedProtocolAdapterIdSet.stream() + .map(adapterId -> CompletableFuture.runAsync(() -> { + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Deleting adapter '{}'", adapterId); + } + stop(adapterId, true); + deleteProtocolAdapterByAdapterId(adapterId); + } catch (final Exception e) { + failedAdapterSet.add(adapterId); + LOGGER.error("Failed deleting adapter {}", adapterId, e); + } + })) + .toList() + .toArray(new CompletableFuture[0])).get(); + + CompletableFuture.allOf(toBeCreatedProtocolAdapterIdSet.stream() + .map(adapterId -> CompletableFuture.runAsync(() -> { + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Creating adapter '{}'", adapterId); + } + createProtocolAdapter(protocolAdapterConfigs.get(adapterId), + versionProvider.getVersion()); + start(adapterId); + } catch (final Exception e) { + failedAdapterSet.add(adapterId); + LOGGER.error("Failed creating adapter {}", adapterId, e); + } + })) + .toList() + .toArray(new CompletableFuture[0])).get(); + + CompletableFuture.allOf(toBeUpdatedProtocolAdapterIdSet.stream() + .map(adapterId -> CompletableFuture.runAsync(() -> { + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Updating adapter '{}'", adapterId); + } + stop(adapterId, true); + deleteProtocolAdapterByAdapterId(adapterId); + createProtocolAdapter(protocolAdapterConfigs.get(adapterId), + versionProvider.getVersion()); + start(adapterId); + } catch (final Exception e) { + failedAdapterSet.add(adapterId); + LOGGER.error("Failed updating adapter {}", adapterId, e); + } + })) + .toList() + .toArray(new CompletableFuture[0])).get(); + + if (failedAdapterSet.isEmpty()) { + eventService.configurationEvent() + .withSeverity(Event.SEVERITY.INFO) + .withMessage("Configuration has been successfully updated") + .fire(); + } else { + eventService.configurationEvent() + .withSeverity(Event.SEVERITY.CRITICAL) + .withMessage("Reloading of configuration failed") + .fire(); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Interrupted while refreshing adapters", e); + } catch (final ExecutionException e) { + LOGGER.error("Failed refreshing adapters", e); + } + }); + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperatorState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterManagerState.java similarity index 94% rename from hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperatorState.java rename to hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterManagerState.java index 8672ea84f9..f50007376d 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperatorState.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterManagerState.java @@ -16,7 +16,7 @@ package com.hivemq.protocols.fsm; -public enum ProtocolAdapterOperatorState { +public enum ProtocolAdapterManagerState { Idle, Running, ; diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java deleted file mode 100644 index 382ce8f7dd..0000000000 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterOperator.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2019-present HiveMQ GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hivemq.protocols.fsm; - -import com.codahale.metrics.MetricRegistry; -import com.google.common.collect.Sets; -import com.hivemq.adapter.sdk.api.events.EventService; -import com.hivemq.adapter.sdk.api.events.model.Event; -import com.hivemq.configuration.entity.adapter.ProtocolAdapterEntity; -import com.hivemq.configuration.reader.ProtocolAdapterExtractor; -import com.hivemq.edge.HiveMQEdgeRemoteService; -import com.hivemq.edge.VersionProvider; -import com.hivemq.edge.modules.adapters.data.TagManager; -import com.hivemq.edge.modules.adapters.impl.ModuleServicesImpl; -import com.hivemq.edge.modules.api.adapters.ProtocolAdapterPollingService; -import com.hivemq.protocols.InternalProtocolAdapterWritingService; -import com.hivemq.protocols.ProtocolAdapterConfig; -import com.hivemq.protocols.ProtocolAdapterConfigConverter; -import com.hivemq.protocols.ProtocolAdapterFactoryManager; -import com.hivemq.protocols.ProtocolAdapterMetrics; -import com.hivemq.protocols.northbound.NorthboundConsumerFactory; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.function.Function; -import java.util.stream.Collectors; - - -public class ProtocolAdapterOperator { - private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolAdapterOperator.class); - - private final @NotNull Map protocolAdapterMap; - private final @NotNull MetricRegistry metricRegistry; - private final @NotNull ModuleServicesImpl moduleServices; - private final @NotNull HiveMQEdgeRemoteService remoteService; - private final @NotNull EventService eventService; - private final @NotNull ProtocolAdapterConfigConverter configConverter; - private final @NotNull VersionProvider versionProvider; - private final @NotNull ProtocolAdapterPollingService protocolAdapterPollingService; - private final @NotNull ProtocolAdapterMetrics protocolAdapterMetrics; - private final @NotNull InternalProtocolAdapterWritingService protocolAdapterWritingService; - private final @NotNull ProtocolAdapterFactoryManager protocolAdapterFactoryManager; - private final @NotNull NorthboundConsumerFactory northboundConsumerFactory; - private final @NotNull TagManager tagManager; - private final @NotNull ProtocolAdapterExtractor protocolAdapterConfig; - private final @NotNull ExecutorService executorService; - private volatile @NotNull ProtocolAdapterOperatorState state; - - public ProtocolAdapterOperator( - final @NotNull MetricRegistry metricRegistry, - final @NotNull ModuleServicesImpl moduleServices, - final @NotNull HiveMQEdgeRemoteService remoteService, - final @NotNull EventService eventService, - final @NotNull ProtocolAdapterConfigConverter configConverter, - final @NotNull VersionProvider versionProvider, - final @NotNull ProtocolAdapterPollingService protocolAdapterPollingService, - final @NotNull ProtocolAdapterMetrics protocolAdapterMetrics, - final @NotNull InternalProtocolAdapterWritingService protocolAdapterWritingService, - final @NotNull ProtocolAdapterFactoryManager protocolAdapterFactoryManager, - final @NotNull NorthboundConsumerFactory northboundConsumerFactory, - final @NotNull TagManager tagManager, - final @NotNull ProtocolAdapterExtractor protocolAdapterConfig) { - this.protocolAdapterMap = new ConcurrentHashMap<>(); - this.metricRegistry = metricRegistry; - this.moduleServices = moduleServices; - this.remoteService = remoteService; - this.eventService = eventService; - this.configConverter = configConverter; - this.versionProvider = versionProvider; - this.protocolAdapterPollingService = protocolAdapterPollingService; - this.protocolAdapterMetrics = protocolAdapterMetrics; - this.protocolAdapterWritingService = protocolAdapterWritingService; - this.protocolAdapterFactoryManager = protocolAdapterFactoryManager; - this.northboundConsumerFactory = northboundConsumerFactory; - this.tagManager = tagManager; - this.protocolAdapterConfig = protocolAdapterConfig; - this.executorService = Executors.newSingleThreadExecutor(); - this.state = ProtocolAdapterOperatorState.Idle; - Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdown)); - protocolAdapterWritingService.addWritingChangedCallback(() -> protocolAdapterFactoryManager.writingEnabledChanged( - protocolAdapterWritingService.writingEnabled())); - } - - public @NotNull ProtocolAdapterOperatorState getState() { - return state; - } - - public void start() { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Starting adapters"); - } - protocolAdapterConfig.registerConsumer(this::refresh); - } - - public void refresh(final @NotNull List configs) { - executorService.submit(() -> { - state = ProtocolAdapterOperatorState.Running; - LOGGER.info("Refreshing adapters"); - - final Map protocolAdapterConfigs = configs.stream() - .map(configConverter::fromEntity) - .collect(Collectors.toMap(ProtocolAdapterConfig::getAdapterId, Function.identity())); - - final Set oldProtocolAdapterIdSet = new HashSet<>(protocolAdapterMap.keySet()); - final Set newProtocolAdapterIdSet = new HashSet<>(protocolAdapterConfigs.keySet()); - - final Set toBeDeletedProtocolAdapterIdSet = - new HashSet<>(Sets.difference(oldProtocolAdapterIdSet, newProtocolAdapterIdSet)); - final Set toBeCreatedProtocolAdapterIdSet = - new HashSet<>(Sets.difference(newProtocolAdapterIdSet, oldProtocolAdapterIdSet)); - final Set toBeUpdatedProtocolAdapterIdSet = - new HashSet<>(Sets.intersection(newProtocolAdapterIdSet, oldProtocolAdapterIdSet)); - - final List failedAdapters = new ArrayList<>(); - - toBeDeletedProtocolAdapterIdSet.forEach(adapterId -> { -// try { -// if (LOGGER.isDebugEnabled()) { -// LOGGER.debug("Deleting adapter '{}'", adapterId); -// } -// stopAsync(adapterId, true).whenComplete((ignored, t) -> deleteAdapterInternal(adapterId)).get(); -// } catch (final InterruptedException e) { -// Thread.currentThread().interrupt(); -// failedAdapters.add(adapterId); -// LOGGER.error("Interrupted while deleting adapter {}", adapterId, e); -// } catch (final ExecutionException e) { -// failedAdapters.add(adapterId); -// LOGGER.error("Failed deleting adapter {}", adapterId, e); -// } - }); - - toBeCreatedProtocolAdapterIdSet.forEach(name -> { -// try { -// if (LOGGER.isDebugEnabled()) { -// LOGGER.debug("Creating adapter '{}'", name); -// } -// startAsync(createAdapterInternal(protocolAdapterConfigs.get(name), -// versionProvider.getVersion())).get(); -// } catch (final InterruptedException e) { -// Thread.currentThread().interrupt(); -// failedAdapters.add(name); -// LOGGER.error("Interrupted while adding adapter {}", name, e); -// } catch (final ExecutionException e) { -// failedAdapters.add(name); -// LOGGER.error("Failed adding adapter {}", name, e); -// } - }); - - toBeUpdatedProtocolAdapterIdSet.forEach(name -> { -// try { -// final var wrapper = protocolAdapters.get(name); -// if (wrapper == null) { -// LOGGER.error( -// "Existing adapters were modified while a refresh was ongoing, adapter with name '{}' was deleted and could not be updated", -// name); -// } -// if (wrapper != null && !protocolAdapterConfigs.get(name).equals(wrapper.getConfig())) { -// if (LOGGER.isDebugEnabled()) { -// LOGGER.debug("Updating adapter '{}'", name); -// } -// stopAsync(name, true).thenApply(v -> { -// deleteAdapterInternal(name); -// return null; -// }) -// .thenCompose(ignored -> startAsync(createAdapterInternal(protocolAdapterConfigs.get(name), -// versionProvider.getVersion()))) -// .get(); -// } else { -// if (LOGGER.isDebugEnabled()) { -// LOGGER.debug("Not-updating adapter '{}' since the config is unchanged", name); -// } -// } -// } catch (final InterruptedException e) { -// Thread.currentThread().interrupt(); -// failedAdapters.add(name); -// LOGGER.error("Interrupted while updating adapter {}", name, e); -// } catch (final ExecutionException e) { -// failedAdapters.add(name); -// LOGGER.error("Failed updating adapter {}", name, e); -// } - }); - - if (failedAdapters.isEmpty()) { - eventService.configurationEvent() - .withSeverity(Event.SEVERITY.INFO) - .withMessage("Configuration has been successfully updated") - .fire(); - } else { - eventService.configurationEvent() - .withSeverity(Event.SEVERITY.CRITICAL) - .withMessage("Reloading of configuration failed") - .fire(); - } - state = ProtocolAdapterOperatorState.Idle; - }); - } -} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java index 36225a408a..7f8c742fe6 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterState.java @@ -18,7 +18,7 @@ import org.jetbrains.annotations.NotNull; -import java.util.function.BiFunction; +import java.util.function.Function; public enum ProtocolAdapterState { Starting(ProtocolAdapterState::transitionFromStarting), @@ -28,16 +28,14 @@ public enum ProtocolAdapterState { Error(ProtocolAdapterState::transitionFromError), ; - private final @NotNull BiFunction - transitionFunction; + private final @NotNull Function transitionFunction; - ProtocolAdapterState(@NotNull final BiFunction transitionFunction) { + ProtocolAdapterState(@NotNull final Function transitionFunction) { this.transitionFunction = transitionFunction; } public static @NotNull ProtocolAdapterTransitionResponse transitionFromStarting( - final @NotNull ProtocolAdapterState toState, - final @NotNull ProtocolAdapterInstance instance) { + final @NotNull ProtocolAdapterState toState) { final ProtocolAdapterState fromState = ProtocolAdapterState.Starting; return switch (toState) { case Starting -> ProtocolAdapterTransitionResponse.notChanged(fromState); @@ -47,8 +45,7 @@ public enum ProtocolAdapterState { } public static @NotNull ProtocolAdapterTransitionResponse transitionFromStarted( - final @NotNull ProtocolAdapterState toState, - final @NotNull ProtocolAdapterInstance instance) { + final @NotNull ProtocolAdapterState toState) { final ProtocolAdapterState fromState = ProtocolAdapterState.Started; return switch (toState) { case Started -> ProtocolAdapterTransitionResponse.notChanged(fromState); @@ -58,8 +55,7 @@ public enum ProtocolAdapterState { } public static @NotNull ProtocolAdapterTransitionResponse transitionFromStopping( - final @NotNull ProtocolAdapterState toState, - final @NotNull ProtocolAdapterInstance instance) { + final @NotNull ProtocolAdapterState toState) { final ProtocolAdapterState fromState = ProtocolAdapterState.Stopping; return switch (toState) { case Stopping -> ProtocolAdapterTransitionResponse.notChanged(fromState); @@ -69,8 +65,7 @@ public enum ProtocolAdapterState { } public static @NotNull ProtocolAdapterTransitionResponse transitionFromStopped( - final @NotNull ProtocolAdapterState toState, - final @NotNull ProtocolAdapterInstance instance) { + final @NotNull ProtocolAdapterState toState) { final ProtocolAdapterState fromState = ProtocolAdapterState.Stopped; return switch (toState) { case Starting -> ProtocolAdapterTransitionResponse.success(fromState, toState); @@ -80,8 +75,7 @@ public enum ProtocolAdapterState { } public static @NotNull ProtocolAdapterTransitionResponse transitionFromError( - final @NotNull ProtocolAdapterState toState, - final @NotNull ProtocolAdapterInstance instance) { + final @NotNull ProtocolAdapterState toState) { final ProtocolAdapterState fromState = ProtocolAdapterState.Error; return switch (toState) { case Starting -> ProtocolAdapterTransitionResponse.success(fromState, toState); @@ -91,8 +85,27 @@ public enum ProtocolAdapterState { } public @NotNull ProtocolAdapterTransitionResponse transition( - final @NotNull ProtocolAdapterState toState, - final @NotNull ProtocolAdapterInstance instance) { - return transitionFunction.apply(toState, instance); + final @NotNull ProtocolAdapterState toState) { + return transitionFunction.apply(toState); + } + + public boolean isStarting() { + return this == Starting; + } + + public boolean isStarted() { + return this == Started; + } + + public boolean isStopping() { + return this == Stopping; + } + + public boolean isStopped() { + return this == Stopped; + } + + public boolean isError() { + return this == Error; } } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionRequest.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionRequest.java deleted file mode 100644 index 18f0e56edb..0000000000 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterTransitionRequest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2019-present HiveMQ GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hivemq.protocols.fsm; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.TimeUnit; - -public record ProtocolAdapterTransitionRequest(boolean requestWriteLock, long timeout, TimeUnit timeUnit) { - public static class Builder { - private boolean requestWriteLock; - private long timeout; - private @Nullable TimeUnit timeUnit; - - public @NotNull Builder requestWriteLock(final boolean requestWriteLock) { - this.requestWriteLock = requestWriteLock; - return this; - } - - public @NotNull Builder timeout(final long timeout) { - this.timeout = timeout; - return this; - } - - public @NotNull Builder timeUnit(final @NotNull TimeUnit timeUnit) { - this.timeUnit = timeUnit; - return this; - } - - public @NotNull ProtocolAdapterTransitionRequest build() { - return new ProtocolAdapterTransitionRequest(requestWriteLock, timeout, timeUnit); - } - } -} diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper2.java similarity index 57% rename from hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java rename to hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper2.java index 4adc80891c..cb982a6081 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterInstance.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper2.java @@ -17,18 +17,19 @@ package com.hivemq.protocols.fsm; import com.hivemq.adapter.sdk.api.ProtocolAdapter; +import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ProtocolAdapterInstance { - private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolAdapterInstance.class); +public class ProtocolAdapterWrapper2 { + private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolAdapterWrapper2.class); protected final @NotNull ProtocolAdapter adapter; protected volatile @NotNull ProtocolAdapterState state; protected volatile @NotNull ProtocolAdapterConnectionState northboundConnectionState; protected volatile @NotNull ProtocolAdapterConnectionState southboundConnectionState; - public ProtocolAdapterInstance(final @NotNull ProtocolAdapter adapter) { + public ProtocolAdapterWrapper2(final @NotNull ProtocolAdapter adapter) { this.adapter = adapter; northboundConnectionState = ProtocolAdapterConnectionState.Closed; southboundConnectionState = ProtocolAdapterConnectionState.Closed; @@ -51,45 +52,66 @@ public ProtocolAdapterInstance(final @NotNull ProtocolAdapter adapter) { return adapter.getId(); } - public void start() { - final ProtocolAdapterTransitionResponse response = transitionTo(ProtocolAdapterState.Starting); - if (response.status().isSuccess()) { - startNorthbound(); - startSouthbound(); - } + public @NotNull ProtocolAdapterInformation getProtocolAdapterInformation() { + return adapter.getProtocolAdapterInformation(); } - public void stop() { - transitionTo(ProtocolAdapterState.Stopping); + public boolean start() { + LOGGER.info("Starting protocol adapter {}.", getAdapterId()); + ProtocolAdapterTransitionResponse response = transitionTo(ProtocolAdapterState.Starting); + if (response.status().isSuccess()) { + boolean success = startNorthbound(); + success = success || startSouthbound(); + if (success) { + response = transitionTo(ProtocolAdapterState.Started); + } else { + response = transitionTo(ProtocolAdapterState.Error); + } + } + return response.status().isSuccess(); } - protected void startNorthbound() { + public boolean stop(final boolean destroy) { + LOGGER.info("Stopping protocol adapter {}.", getAdapterId()); + ProtocolAdapterTransitionResponse response = transitionTo(ProtocolAdapterState.Stopping); + if (response.status().isSuccess()) { + response = transitionTo(ProtocolAdapterState.Stopped); + } + return response.status().isSuccess(); } - protected void startSouthbound() { + protected boolean startNorthbound() { + LOGGER.info("Starting northbound for protocol adapter {}.", getAdapterId()); + northboundConnectionState = ProtocolAdapterConnectionState.Connected; + return northboundConnectionState.isConnected(); + } + protected boolean startSouthbound() { + LOGGER.info("Starting southbound for protocol adapter {}.", getAdapterId()); + southboundConnectionState = ProtocolAdapterConnectionState.Connected; + return southboundConnectionState.isConnected(); } public synchronized @NotNull ProtocolAdapterTransitionResponse transitionTo(final @NotNull ProtocolAdapterState newState) { final ProtocolAdapterState fromState = state; - final ProtocolAdapterTransitionResponse response = fromState.transition(newState, this); + final ProtocolAdapterTransitionResponse response = fromState.transition(newState); state = response.toState(); switch (response.status()) { case Success -> { LOGGER.debug("Protocol adapter '{}' transitioned from {} to {} successfully.", + getAdapterId(), fromState, - state, - getAdapterId()); + state); } case Failure -> { LOGGER.error("Protocol adapter '{}' failed to transition from {} to {}.", + getAdapterId(), fromState, - state, - getAdapterId()); + state); } case NotChanged -> { - LOGGER.warn("Protocol adapter '{}' state {} is unchanged.", state, getAdapterId()); + LOGGER.warn("Protocol adapter '{}' state {} is unchanged.", getAdapterId(), state); } } return response; diff --git a/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties b/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties index 544398746d..ea417cdf81 100644 --- a/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties +++ b/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties @@ -1,3 +1,5 @@ fsm.transition.failure.unable.to.transition.from.state.to.state=Unable to transition from ${fromState} to ${toState}. fsm.transition.success.state.is.unchanged=${state} is unchanged. fsm.transition.success.transitioned.from.state.to.state=Transitioned from ${fromState} to ${toState}. +protocol.adapter.manager.protocol.adapter.deleted=Adapter '${adapterId}' was deleted from the system permanently. +protocol.adapter.manager.protocol.adapter.not.found=Adapter '${adapterId}' not found. diff --git a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterStateTest.java b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterStateTest.java index 07032fbc58..48b2b953e0 100644 --- a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterStateTest.java +++ b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterStateTest.java @@ -17,10 +17,8 @@ package com.hivemq.protocols.fsm; import com.hivemq.common.i18n.StringTemplate; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @@ -45,8 +43,6 @@ public class ProtocolAdapterStateTest { Set.of(ProtocolAdapterState.Starting), ProtocolAdapterState.Error, Set.of(ProtocolAdapterState.Starting)); - @Mock - private @NotNull ProtocolAdapterInstance protocolAdapterInstance; @Test public void whenEverythingWorks_thenTransitionShouldWork() { @@ -55,8 +51,7 @@ public void whenEverythingWorks_thenTransitionShouldWork() { final Set possibleToStates = PROTOCOL_ADAPTER_STATE_MAP_MAP.get(fromState); assertThat(possibleToStates).isNotNull(); states.forEach(toState -> { - final ProtocolAdapterTransitionResponse response = - fromState.transition(toState, protocolAdapterInstance); + final ProtocolAdapterTransitionResponse response = fromState.transition(toState); switch (response.status()) { case Success -> { assertThat(possibleToStates).contains(toState); diff --git a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapperTest.java b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapperTest.java new file mode 100644 index 0000000000..0237ae8125 --- /dev/null +++ b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapperTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import com.hivemq.adapter.sdk.api.ProtocolAdapter; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class ProtocolAdapterWrapperTest { + @Mock + private @NotNull ProtocolAdapter protocolAdapter; + + @BeforeEach + public void setUp() { + when(protocolAdapter.getId()).thenReturn("test"); + } + + @Test + public void whenAdapterIsValid_thenStartAndStopWork() { + final ProtocolAdapterWrapper2 wrapper = new ProtocolAdapterWrapper2(protocolAdapter); + assertThat(wrapper.getState()).isEqualTo(ProtocolAdapterState.Stopped); + assertThat(wrapper.start()).isTrue(); + assertThat(wrapper.getState()).isEqualTo(ProtocolAdapterState.Started); + assertThat(wrapper.stop(true)).isTrue(); + assertThat(wrapper.getState()).isEqualTo(ProtocolAdapterState.Stopped); + } +} From c1c87c07a279a8635b2028249360ce8b7c659c71 Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Tue, 11 Nov 2025 16:45:46 +0100 Subject: [PATCH 15/16] feat: FSM 3 --- .../fsm/I18nProtocolAdapterMessage.java | 3 + .../fsm/ProtocolAdapterConnectionState.java | 109 +++++++++++++++--- ...olAdapterConnectionTransitionResponse.java | 65 +++++++++++ ...protocol-adapter-messages-en_US.properties | 3 + .../ProtocolAdapterConnectionStateTest.java | 87 ++++++++++++++ 5 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionTransitionResponse.java create mode 100644 hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionStateTest.java diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java index 9b873e656c..8761420f17 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/I18nProtocolAdapterMessage.java @@ -26,6 +26,9 @@ public enum I18nProtocolAdapterMessage implements I18nError { FSM_TRANSITION_FAILURE_UNABLE_TO_TRANSITION_FROM_STATE_TO_STATE, FSM_TRANSITION_SUCCESS_STATE_IS_UNCHANGED, FSM_TRANSITION_SUCCESS_TRANSITIONED_FROM_STATE_TO_STATE, + FSM_CONNECTION_TRANSITION_FAILURE_UNABLE_TO_TRANSITION_FROM_STATE_TO_STATE, + FSM_CONNECTION_TRANSITION_SUCCESS_STATE_IS_UNCHANGED, + FSM_CONNECTION_TRANSITION_SUCCESS_TRANSITIONED_FROM_STATE_TO_STATE, PROTOCOL_ADAPTER_MANAGER_PROTOCOL_ADAPTER_DELETED, PROTOCOL_ADAPTER_MANAGER_PROTOCOL_ADAPTER_NOT_FOUND, ; diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java index 99b6aabb1e..83142846a2 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java @@ -21,26 +21,107 @@ import java.util.function.Function; public enum ProtocolAdapterConnectionState { - Closed(context -> 0), - Closing(context -> 0), - Connected(context -> 0), - Connecting(context -> 0), - Disconnected(context -> 0), - Disconnecting(context -> 0), - Error(context -> 0), - ErrorClosing(context -> 0), + Closed(ProtocolAdapterConnectionState::transitionFromClosed), + Closing(ProtocolAdapterConnectionState::transitionFromClosing), + Connected(ProtocolAdapterConnectionState::transitionFromConnected), + Connecting(ProtocolAdapterConnectionState::transitionFromConnecting), + Disconnected(ProtocolAdapterConnectionState::transitionFromDisconnected), + Disconnecting(ProtocolAdapterConnectionState::transitionFromDisconnecting), + Error(ProtocolAdapterConnectionState::transitionFromError), + ErrorClosing(ProtocolAdapterConnectionState::transitionFromErrorClosing), ; - private final @NotNull Function transitionFunction; + private final @NotNull Function + transitionFunction; - ProtocolAdapterConnectionState(@NotNull final Function transitionFunction) { + ProtocolAdapterConnectionState(@NotNull final Function transitionFunction) { this.transitionFunction = transitionFunction; } - public @NotNull Integer transition( - final @NotNull ProtocolAdapterConnectionState targetState, - final @NotNull Object context) { - return transitionFunction.apply(context); + public static @NotNull ProtocolAdapterConnectionTransitionResponse transitionFromClosed( + final @NotNull ProtocolAdapterConnectionState toState) { + final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.Closed; + return switch (toState) { + case Closed -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); + case Disconnected -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); + }; + } + + public static @NotNull ProtocolAdapterConnectionTransitionResponse transitionFromClosing( + final @NotNull ProtocolAdapterConnectionState toState) { + final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.Closing; + return switch (toState) { + case Closing -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); + case Closed -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); + }; + } + + public static @NotNull ProtocolAdapterConnectionTransitionResponse transitionFromConnected( + final @NotNull ProtocolAdapterConnectionState toState) { + final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.Connected; + return switch (toState) { + case Connected -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); + case Disconnecting, Closing, ErrorClosing -> + ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); + }; + } + + public static @NotNull ProtocolAdapterConnectionTransitionResponse transitionFromConnecting( + final @NotNull ProtocolAdapterConnectionState toState) { + final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.Connecting; + return switch (toState) { + case Connecting -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); + case Connected, Error -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); + }; + } + + public static @NotNull ProtocolAdapterConnectionTransitionResponse transitionFromDisconnected( + final @NotNull ProtocolAdapterConnectionState toState) { + final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.Disconnected; + return switch (toState) { + case Disconnected -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); + case Connecting -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); + }; + } + + public static @NotNull ProtocolAdapterConnectionTransitionResponse transitionFromDisconnecting( + final @NotNull ProtocolAdapterConnectionState toState) { + final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.Disconnecting; + return switch (toState) { + case Disconnecting -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); + case Connecting -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); + }; + } + + public static @NotNull ProtocolAdapterConnectionTransitionResponse transitionFromError( + final @NotNull ProtocolAdapterConnectionState toState) { + final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.Error; + return switch (toState) { + case Error -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); + case Disconnected -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); + }; + } + + public static @NotNull ProtocolAdapterConnectionTransitionResponse transitionFromErrorClosing( + final @NotNull ProtocolAdapterConnectionState toState) { + final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.ErrorClosing; + return switch (toState) { + case ErrorClosing -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); + case Error -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); + }; + } + + public @NotNull ProtocolAdapterConnectionTransitionResponse transition( + final @NotNull ProtocolAdapterConnectionState toState) { + return transitionFunction.apply(toState); } public boolean isClosed() { diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionTransitionResponse.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionTransitionResponse.java new file mode 100644 index 0000000000..884d19c785 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionTransitionResponse.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public record ProtocolAdapterConnectionTransitionResponse(ProtocolAdapterConnectionState fromState, + ProtocolAdapterConnectionState toState, + ProtocolAdapterTransitionStatus status, String message, + Throwable error) { + + public static final String FROM_STATE = "fromState"; + public static final String TO_STATE = "toState"; + public static final String STATE = "state"; + + public static ProtocolAdapterConnectionTransitionResponse success( + final @NotNull ProtocolAdapterConnectionState fromState, + final @NotNull ProtocolAdapterConnectionState toState) { + return new ProtocolAdapterConnectionTransitionResponse(fromState, + toState, + ProtocolAdapterTransitionStatus.Success, + I18nProtocolAdapterMessage.FSM_CONNECTION_TRANSITION_SUCCESS_TRANSITIONED_FROM_STATE_TO_STATE.get(Map.of( + FROM_STATE, + fromState.name(), + TO_STATE, + toState.name())), + null); + } + + public static ProtocolAdapterConnectionTransitionResponse notChanged(final @NotNull ProtocolAdapterConnectionState state) { + return new ProtocolAdapterConnectionTransitionResponse(state, + state, + ProtocolAdapterTransitionStatus.NotChanged, + I18nProtocolAdapterMessage.FSM_CONNECTION_TRANSITION_SUCCESS_STATE_IS_UNCHANGED.get(Map.of(STATE, + state.name())), + null); + } + + public static ProtocolAdapterConnectionTransitionResponse failure( + final @NotNull ProtocolAdapterConnectionState fromState, + final @NotNull ProtocolAdapterConnectionState toState) { + return new ProtocolAdapterConnectionTransitionResponse(fromState, + toState, + ProtocolAdapterTransitionStatus.Failure, + I18nProtocolAdapterMessage.FSM_CONNECTION_TRANSITION_FAILURE_UNABLE_TO_TRANSITION_FROM_STATE_TO_STATE.get( + Map.of(FROM_STATE, fromState.name(), TO_STATE, toState.name())), + null); + } +} diff --git a/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties b/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties index ea417cdf81..0685db5287 100644 --- a/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties +++ b/hivemq-edge/src/main/resources/templates/protocol-adapter-messages-en_US.properties @@ -1,5 +1,8 @@ fsm.transition.failure.unable.to.transition.from.state.to.state=Unable to transition from ${fromState} to ${toState}. fsm.transition.success.state.is.unchanged=${state} is unchanged. fsm.transition.success.transitioned.from.state.to.state=Transitioned from ${fromState} to ${toState}. +fsm.connection.transition.failure.unable.to.transition.from.state.to.state=Unable to transition connection from ${fromState} to ${toState}. +fsm.connection.transition.success.state.is.unchanged=Connection ${state} is unchanged. +fsm.connection.transition.success.transitioned.from.state.to.state=Connection transitioned from ${fromState} to ${toState}. protocol.adapter.manager.protocol.adapter.deleted=Adapter '${adapterId}' was deleted from the system permanently. protocol.adapter.manager.protocol.adapter.not.found=Adapter '${adapterId}' not found. diff --git a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionStateTest.java b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionStateTest.java new file mode 100644 index 0000000000..a7fb104234 --- /dev/null +++ b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionStateTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hivemq.protocols.fsm; + +import com.hivemq.common.i18n.StringTemplate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class ProtocolAdapterConnectionStateTest { + private static final Map> + PROTOCOL_ADAPTER_CONNECTION_STATE_MAP = Map.of(ProtocolAdapterConnectionState.Closed, + Set.of(ProtocolAdapterConnectionState.Disconnected), + ProtocolAdapterConnectionState.Closing, + Set.of(ProtocolAdapterConnectionState.Closed), + ProtocolAdapterConnectionState.Connected, + Set.of(ProtocolAdapterConnectionState.Disconnecting, + ProtocolAdapterConnectionState.Closing, + ProtocolAdapterConnectionState.ErrorClosing), + ProtocolAdapterConnectionState.Connecting, + Set.of(ProtocolAdapterConnectionState.Connected, ProtocolAdapterConnectionState.Error), + ProtocolAdapterConnectionState.Disconnected, + Set.of(ProtocolAdapterConnectionState.Connecting), + ProtocolAdapterConnectionState.Disconnecting, + Set.of(ProtocolAdapterConnectionState.Connecting), + ProtocolAdapterConnectionState.Error, + Set.of(ProtocolAdapterConnectionState.Disconnected), + ProtocolAdapterConnectionState.ErrorClosing, + Set.of(ProtocolAdapterConnectionState.Error)); + + @Test + public void whenEverythingWorks_thenTransitionShouldWork() { + final List states = List.of(ProtocolAdapterConnectionState.values()); + states.forEach(fromState -> { + final Set possibleToStates = + PROTOCOL_ADAPTER_CONNECTION_STATE_MAP.get(fromState); + assertThat(possibleToStates).isNotNull(); + states.forEach(toState -> { + final ProtocolAdapterConnectionTransitionResponse response = fromState.transition(toState); + switch (response.status()) { + case Success -> { + assertThat(possibleToStates).contains(toState); + assertThat(response.message()).isEqualTo(StringTemplate.format( + "Connection transitioned from ${fromState} to ${toState}.", + Map.of("fromState", fromState, "toState", toState))); + } + case Failure -> { + assertThat(possibleToStates).doesNotContain(toState); + assertThat(response.message()).isEqualTo(StringTemplate.format( + "Unable to transition connection from ${fromState} to ${toState}.", + Map.of("fromState", fromState, "toState", toState))); + } + case NotChanged -> { + assertThat(toState).isEqualTo(fromState); + assertThat(response.message()).isEqualTo(StringTemplate.format( + "Connection ${state} is unchanged.", + Map.of("state", fromState))); + } + } + }); + }); + } +} From 3a04bc17b7cfb722da676339848737cd847a80da Mon Sep 17 00:00:00 2001 From: Sam Cao Date: Tue, 11 Nov 2025 17:10:20 +0100 Subject: [PATCH 16/16] feat: FSM 4 --- .../fsm/ProtocolAdapterConnectionState.java | 2 +- .../fsm/ProtocolAdapterWrapper2.java | 105 ++++++++++++++++-- .../ProtocolAdapterConnectionStateTest.java | 2 +- .../fsm/ProtocolAdapterWrapperTest.java | 6 + 4 files changed, 104 insertions(+), 11 deletions(-) diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java index 83142846a2..6f75ea87a5 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionState.java @@ -94,7 +94,7 @@ public enum ProtocolAdapterConnectionState { final ProtocolAdapterConnectionState fromState = ProtocolAdapterConnectionState.Disconnecting; return switch (toState) { case Disconnecting -> ProtocolAdapterConnectionTransitionResponse.notChanged(fromState); - case Connecting -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); + case Connecting, Disconnected -> ProtocolAdapterConnectionTransitionResponse.success(fromState, toState); default -> ProtocolAdapterConnectionTransitionResponse.failure(fromState, toState); }; } diff --git a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper2.java b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper2.java index cb982a6081..c27a95cb6d 100644 --- a/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper2.java +++ b/hivemq-edge/src/main/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapper2.java @@ -31,8 +31,8 @@ public class ProtocolAdapterWrapper2 { public ProtocolAdapterWrapper2(final @NotNull ProtocolAdapter adapter) { this.adapter = adapter; - northboundConnectionState = ProtocolAdapterConnectionState.Closed; - southboundConnectionState = ProtocolAdapterConnectionState.Closed; + northboundConnectionState = ProtocolAdapterConnectionState.Disconnected; + southboundConnectionState = ProtocolAdapterConnectionState.Disconnected; state = ProtocolAdapterState.Stopped; } @@ -61,7 +61,7 @@ public boolean start() { ProtocolAdapterTransitionResponse response = transitionTo(ProtocolAdapterState.Starting); if (response.status().isSuccess()) { boolean success = startNorthbound(); - success = success || startSouthbound(); + success = success && startSouthbound(); if (success) { response = transitionTo(ProtocolAdapterState.Started); } else { @@ -75,22 +75,55 @@ public boolean stop(final boolean destroy) { LOGGER.info("Stopping protocol adapter {}.", getAdapterId()); ProtocolAdapterTransitionResponse response = transitionTo(ProtocolAdapterState.Stopping); if (response.status().isSuccess()) { - - response = transitionTo(ProtocolAdapterState.Stopped); + final boolean southboundSuccess = stopSouthbound(); + final boolean northboundSuccess = stopNorthbound(); + if (northboundSuccess && southboundSuccess) { + response = transitionTo(ProtocolAdapterState.Stopped); + } else { + response = transitionTo(ProtocolAdapterState.Error); + } } return response.status().isSuccess(); } protected boolean startNorthbound() { LOGGER.info("Starting northbound for protocol adapter {}.", getAdapterId()); - northboundConnectionState = ProtocolAdapterConnectionState.Connected; - return northboundConnectionState.isConnected(); + ProtocolAdapterConnectionTransitionResponse response = + transitionNorthboundConnectionTo(ProtocolAdapterConnectionState.Connecting); + if (response.status().isSuccess()) { + response = transitionNorthboundConnectionTo(ProtocolAdapterConnectionState.Connected); + } + return response.status().isSuccess(); } protected boolean startSouthbound() { LOGGER.info("Starting southbound for protocol adapter {}.", getAdapterId()); - southboundConnectionState = ProtocolAdapterConnectionState.Connected; - return southboundConnectionState.isConnected(); + ProtocolAdapterConnectionTransitionResponse response = + transitionSouthboundConnectionTo(ProtocolAdapterConnectionState.Connecting); + if (response.status().isSuccess()) { + response = transitionSouthboundConnectionTo(ProtocolAdapterConnectionState.Connected); + } + return response.status().isSuccess(); + } + + protected boolean stopNorthbound() { + LOGGER.info("Stopping northbound for protocol adapter {}.", getAdapterId()); + ProtocolAdapterConnectionTransitionResponse response = + transitionNorthboundConnectionTo(ProtocolAdapterConnectionState.Disconnecting); + if (response.status().isSuccess()) { + response = transitionNorthboundConnectionTo(ProtocolAdapterConnectionState.Disconnected); + } + return response.status().isSuccess(); + } + + protected boolean stopSouthbound() { + LOGGER.info("Stopping southbound for protocol adapter {}.", getAdapterId()); + ProtocolAdapterConnectionTransitionResponse response = + transitionSouthboundConnectionTo(ProtocolAdapterConnectionState.Disconnecting); + if (response.status().isSuccess()) { + response = transitionSouthboundConnectionTo(ProtocolAdapterConnectionState.Disconnected); + } + return response.status().isSuccess(); } public synchronized @NotNull ProtocolAdapterTransitionResponse transitionTo(final @NotNull ProtocolAdapterState newState) { @@ -116,4 +149,58 @@ protected boolean startSouthbound() { } return response; } + + public synchronized @NotNull ProtocolAdapterConnectionTransitionResponse transitionSouthboundConnectionTo( + final @NotNull ProtocolAdapterConnectionState newState) { + final ProtocolAdapterConnectionState fromState = southboundConnectionState; + final ProtocolAdapterConnectionTransitionResponse response = fromState.transition(newState); + southboundConnectionState = response.toState(); + switch (response.status()) { + case Success -> { + LOGGER.debug("Protocol adapter '{}' southbound connection transitioned from {} to {} successfully.", + getAdapterId(), + fromState, + southboundConnectionState); + } + case Failure -> { + LOGGER.error("Protocol adapter '{}' southbound connection failed to transition from {} to {}.", + getAdapterId(), + fromState, + southboundConnectionState); + } + case NotChanged -> { + LOGGER.warn("Protocol adapter '{}' southbound connection state {} is unchanged.", + getAdapterId(), + southboundConnectionState); + } + } + return response; + } + + public synchronized @NotNull ProtocolAdapterConnectionTransitionResponse transitionNorthboundConnectionTo( + final @NotNull ProtocolAdapterConnectionState newState) { + final ProtocolAdapterConnectionState fromState = northboundConnectionState; + final ProtocolAdapterConnectionTransitionResponse response = fromState.transition(newState); + northboundConnectionState = response.toState(); + switch (response.status()) { + case Success -> { + LOGGER.debug("Protocol adapter '{}' northbound connection transitioned from {} to {} successfully.", + getAdapterId(), + fromState, + northboundConnectionState); + } + case Failure -> { + LOGGER.error("Protocol adapter '{}' northbound connection failed to transition from {} to {}.", + getAdapterId(), + fromState, + northboundConnectionState); + } + case NotChanged -> { + LOGGER.warn("Protocol adapter '{}' northbound connection state {} is unchanged.", + getAdapterId(), + northboundConnectionState); + } + } + return response; + } } diff --git a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionStateTest.java b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionStateTest.java index a7fb104234..6b839c6ee6 100644 --- a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionStateTest.java +++ b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterConnectionStateTest.java @@ -46,7 +46,7 @@ public class ProtocolAdapterConnectionStateTest { ProtocolAdapterConnectionState.Disconnected, Set.of(ProtocolAdapterConnectionState.Connecting), ProtocolAdapterConnectionState.Disconnecting, - Set.of(ProtocolAdapterConnectionState.Connecting), + Set.of(ProtocolAdapterConnectionState.Connecting, ProtocolAdapterConnectionState.Disconnected), ProtocolAdapterConnectionState.Error, Set.of(ProtocolAdapterConnectionState.Disconnected), ProtocolAdapterConnectionState.ErrorClosing, diff --git a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapperTest.java b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapperTest.java index 0237ae8125..9342d85516 100644 --- a/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapperTest.java +++ b/hivemq-edge/src/test/java/com/hivemq/protocols/fsm/ProtocolAdapterWrapperTest.java @@ -44,9 +44,15 @@ public void setUp() { public void whenAdapterIsValid_thenStartAndStopWork() { final ProtocolAdapterWrapper2 wrapper = new ProtocolAdapterWrapper2(protocolAdapter); assertThat(wrapper.getState()).isEqualTo(ProtocolAdapterState.Stopped); + assertThat(wrapper.getNorthboundConnectionState()).isEqualTo(ProtocolAdapterConnectionState.Disconnected); + assertThat(wrapper.getSouthboundConnectionState()).isEqualTo(ProtocolAdapterConnectionState.Disconnected); assertThat(wrapper.start()).isTrue(); assertThat(wrapper.getState()).isEqualTo(ProtocolAdapterState.Started); + assertThat(wrapper.getNorthboundConnectionState()).isEqualTo(ProtocolAdapterConnectionState.Connected); + assertThat(wrapper.getSouthboundConnectionState()).isEqualTo(ProtocolAdapterConnectionState.Connected); assertThat(wrapper.stop(true)).isTrue(); assertThat(wrapper.getState()).isEqualTo(ProtocolAdapterState.Stopped); + assertThat(wrapper.getNorthboundConnectionState()).isEqualTo(ProtocolAdapterConnectionState.Disconnected); + assertThat(wrapper.getSouthboundConnectionState()).isEqualTo(ProtocolAdapterConnectionState.Disconnected); } }