Erweiterungen
This commit is contained in:
269
CHANGELOG_MQTT.md
Normal file
269
CHANGELOG_MQTT.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
# MQTT System Changelog
|
||||||
|
|
||||||
|
## Version 2.0.0 - 2025-10-22
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
#### 1. MQTT-Broker-Port geändert
|
||||||
|
- **Alt**: Port `1883`
|
||||||
|
- **Neu**: Port `42099`
|
||||||
|
- **Broker**: `mqtt-2.assecutor.de:42099`
|
||||||
|
- **Grund**: Neuer dedizierter Port für die Produktionsumgebung
|
||||||
|
|
||||||
|
#### 2. Konfigurationssystem umgestellt
|
||||||
|
Die alte `app.mqtt.*` Konfiguration wurde durch das neue Plugin-System ersetzt.
|
||||||
|
|
||||||
|
**Entfernte Konfiguration:**
|
||||||
|
```properties
|
||||||
|
app.mqtt.enabled=true
|
||||||
|
app.mqtt.broker-uri=mqtt://mqtt-2.assecutor.de
|
||||||
|
app.mqtt.client-id=server-${random.uuid}
|
||||||
|
app.mqtt.clean-start=false
|
||||||
|
app.mqtt.session-expiry-interval=86400
|
||||||
|
app.mqtt.keep-alive=30
|
||||||
|
app.mqtt.max-inflight=50
|
||||||
|
app.mqtt.automatic-reconnect=true
|
||||||
|
app.mqtt.default-qos=2
|
||||||
|
app.mqtt.default-retained=false
|
||||||
|
```
|
||||||
|
|
||||||
|
**Neue Konfiguration:**
|
||||||
|
```properties
|
||||||
|
# Messaging Plugin Configuration
|
||||||
|
app.messaging.plugin.type=mqtt
|
||||||
|
app.messaging.plugin.mqtt.broker.host=mqtt-2.assecutor.de
|
||||||
|
app.messaging.plugin.mqtt.broker.port=42099
|
||||||
|
app.messaging.plugin.mqtt.username=app
|
||||||
|
app.messaging.plugin.mqtt.password=apppwd
|
||||||
|
app.messaging.plugin.mqtt.client.id=votianlt-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neue Features
|
||||||
|
|
||||||
|
#### 1. Plugin-basiertes Messaging-System
|
||||||
|
- Abstraktionsschicht für verschiedene Messaging-Protokolle
|
||||||
|
- Einfacher Wechsel zwischen MQTT, WebSocket, gRPC möglich
|
||||||
|
- Einheitliche API für alle Messaging-Backends
|
||||||
|
|
||||||
|
**Hauptkomponenten:**
|
||||||
|
- `MessagingPlugin` Interface
|
||||||
|
- `MqttMessagingPlugin` Implementierung
|
||||||
|
- `PluginManager` für Plugin-Verwaltung
|
||||||
|
- `PluginMessagingConfig` für Konfiguration
|
||||||
|
|
||||||
|
#### 2. Message Envelope System
|
||||||
|
Alle Nachrichten werden jetzt in Envelopes verpackt:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class MessageEnvelope {
|
||||||
|
private String messageId; // UUID
|
||||||
|
private Instant timestamp; // Zeitstempel
|
||||||
|
private String topic; // MQTT-Topic
|
||||||
|
private Object payload; // Eigentliche Nachricht
|
||||||
|
private boolean requiresAck; // ACK erforderlich?
|
||||||
|
private int retryCount; // Anzahl Wiederholungen
|
||||||
|
private Instant expiresAt; // Ablaufzeitpunkt
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Acknowledgment-System
|
||||||
|
- Automatische ACK-Verwaltung
|
||||||
|
- Retry-Mechanismus bei fehlenden ACKs
|
||||||
|
- Konfigurierbare Timeouts und Wiederholungen
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class AcknowledgmentMessage {
|
||||||
|
private String messageId;
|
||||||
|
private Instant timestamp;
|
||||||
|
private String status; // SUCCESS oder FAILED
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Verbesserte Fehlerbehandlung
|
||||||
|
- Erhöhter Connection-Timeout: 30s → 60s
|
||||||
|
- Detaillierte Fehlerdiagnose:
|
||||||
|
- `TimeoutException`: Broker nicht erreichbar
|
||||||
|
- `UnknownHostException`: DNS-Fehler
|
||||||
|
- `ConnectException`: Port blockiert
|
||||||
|
- Automatische Wiederverbindung mit exponentieller Backoff
|
||||||
|
|
||||||
|
#### 5. Konfigurierbare Timeouts
|
||||||
|
Neue Konfigurationsoptionen:
|
||||||
|
```properties
|
||||||
|
app.messaging.plugin.mqtt.connection.timeout.seconds=60
|
||||||
|
app.messaging.plugin.mqtt.keep.alive.seconds=60
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entfernte Komponenten
|
||||||
|
|
||||||
|
#### Gelöschte Klassen:
|
||||||
|
1. `MqttProperties.java` - Ersetzt durch Plugin-Konfiguration
|
||||||
|
2. `MqttConfig.java` - Nicht mehr benötigt
|
||||||
|
|
||||||
|
#### Grund für Entfernung:
|
||||||
|
Diese Klassen wurden durch das neue Plugin-System obsolet und waren nicht mehr in Verwendung.
|
||||||
|
|
||||||
|
### Geänderte Komponenten
|
||||||
|
|
||||||
|
#### 1. MqttMessagingPlugin
|
||||||
|
**Datei**: `src/main/java/de/assecutor/votianlt/messaging/plugin/mqtt/MqttMessagingPlugin.java`
|
||||||
|
|
||||||
|
**Änderungen:**
|
||||||
|
- Connection-Timeout von 30s auf 60s erhöht
|
||||||
|
- Konfigurierbare Timeout-Parameter hinzugefügt
|
||||||
|
- Verbesserte Fehlerbehandlung mit spezifischen Exception-Checks
|
||||||
|
- Detailliertes Logging für Verbindungsprobleme
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Neu: Konfigurierbare Timeouts
|
||||||
|
private static final String CONFIG_CONNECTION_TIMEOUT = "connection.timeout.seconds";
|
||||||
|
private static final String CONFIG_KEEP_ALIVE = "keep.alive.seconds";
|
||||||
|
|
||||||
|
// Verbesserte Fehlerbehandlung
|
||||||
|
if (throwable instanceof java.util.concurrent.TimeoutException) {
|
||||||
|
log.error("[MqttPlugin] Connection timeout - broker may be unreachable");
|
||||||
|
} else if (throwable.getCause() instanceof java.net.UnknownHostException) {
|
||||||
|
log.error("[MqttPlugin] Unknown host - DNS resolution failed");
|
||||||
|
} else if (throwable.getCause() instanceof java.net.ConnectException) {
|
||||||
|
log.error("[MqttPlugin] Connection refused - broker may be down");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. PluginMessagingConfig
|
||||||
|
**Datei**: `src/main/java/de/assecutor/votianlt/messaging/config/PluginMessagingConfig.java`
|
||||||
|
|
||||||
|
**Änderungen:**
|
||||||
|
- Verwendet neue Konfigurationsparameter
|
||||||
|
- Initialisiert Plugin mit korrektem Port (42099)
|
||||||
|
- Setzt Authentifizierung (username/password)
|
||||||
|
|
||||||
|
#### 3. application.properties
|
||||||
|
**Datei**: `src/main/resources/application.properties`
|
||||||
|
|
||||||
|
**Änderungen:**
|
||||||
|
- Alte `app.mqtt.*` Konfiguration entfernt (Zeilen 57-71)
|
||||||
|
- Neue `app.messaging.plugin.mqtt.*` Konfiguration verwendet
|
||||||
|
- Port auf 42099 aktualisiert
|
||||||
|
|
||||||
|
### Architektur-Änderungen
|
||||||
|
|
||||||
|
#### Neue Architektur-Schichten:
|
||||||
|
|
||||||
|
```
|
||||||
|
Application Layer (Services, Controllers)
|
||||||
|
↓
|
||||||
|
MessageDeliveryService (Envelope-Wrapping, Queue-Management)
|
||||||
|
↓
|
||||||
|
PluginManager (Plugin-Verwaltung)
|
||||||
|
↓
|
||||||
|
MessagingPlugin Interface (Protokoll-Abstraktion)
|
||||||
|
↓
|
||||||
|
MqttMessagingPlugin (MQTT-spezifische Implementierung)
|
||||||
|
↓
|
||||||
|
HiveMQ MQTT Client
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vorteile der neuen Architektur:
|
||||||
|
1. **Protokoll-Unabhängigkeit**: Einfacher Wechsel zwischen Messaging-Backends
|
||||||
|
2. **Testbarkeit**: Mock-Plugins für Tests
|
||||||
|
3. **Wartbarkeit**: Klare Trennung der Verantwortlichkeiten
|
||||||
|
4. **Erweiterbarkeit**: Neue Protokolle einfach hinzufügbar
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
|
||||||
|
#### Server-Migration (bereits durchgeführt):
|
||||||
|
1. ✅ Port auf 42099 geändert
|
||||||
|
2. ✅ Alte Konfiguration entfernt
|
||||||
|
3. ✅ Neue Plugin-Konfiguration aktiviert
|
||||||
|
4. ✅ Timeout erhöht
|
||||||
|
5. ✅ Fehlerbehandlung verbessert
|
||||||
|
|
||||||
|
#### Client-Migration (erforderlich):
|
||||||
|
Siehe `MQTT_MIGRATION_GUIDE.md` für detaillierte Anweisungen.
|
||||||
|
|
||||||
|
**Wichtigste Schritte:**
|
||||||
|
1. Port auf 42099 ändern
|
||||||
|
2. Authentifizierung hinzufügen (username: `app`, password: `apppwd`)
|
||||||
|
3. Message-Envelope-Format implementieren
|
||||||
|
4. ACK-Handling implementieren
|
||||||
|
5. Timeout auf 60s erhöhen
|
||||||
|
|
||||||
|
### Kompatibilität
|
||||||
|
|
||||||
|
#### Abwärtskompatibilität:
|
||||||
|
- ✅ Topic-Struktur unverändert
|
||||||
|
- ✅ Legacy-Nachrichten werden noch unterstützt
|
||||||
|
- ⚠️ Neue Clients sollten Envelope-Format verwenden
|
||||||
|
|
||||||
|
#### Vorwärtskompatibilität:
|
||||||
|
- ✅ Neue Features sind optional
|
||||||
|
- ✅ Schrittweise Migration möglich
|
||||||
|
- ✅ Alte und neue Clients können parallel laufen
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
#### Getestete Szenarien:
|
||||||
|
1. ✅ Verbindung zum neuen Broker (mqtt-2.assecutor.de:42099)
|
||||||
|
2. ✅ Authentifizierung mit username/password
|
||||||
|
3. ✅ Subscription zu allen Topics
|
||||||
|
4. ✅ Message-Envelope-Verarbeitung
|
||||||
|
5. ✅ ACK-Handling
|
||||||
|
6. ✅ Automatische Wiederverbindung
|
||||||
|
7. ✅ Timeout-Handling
|
||||||
|
|
||||||
|
#### Test-Logs:
|
||||||
|
```
|
||||||
|
11:29:26.251 [RxComputationThreadPool-1] INFO MqttMessagingPlugin - Connected successfully
|
||||||
|
11:29:26.414 [RxComputationThreadPool-3] INFO MqttMessagingPlugin - Successfully subscribed to: /ack/server/+
|
||||||
|
11:29:26.414 [RxComputationThreadPool-4] INFO MqttMessagingPlugin - Successfully subscribed to: /server/+/task_completed
|
||||||
|
11:29:26.414 [RxComputationThreadPool-5] INFO MqttMessagingPlugin - Successfully subscribed to: /server/+/jobs/assigned
|
||||||
|
11:29:26.415 [RxComputationThreadPool-6] INFO MqttMessagingPlugin - Successfully subscribed to: /server/+/message
|
||||||
|
11:29:26.417 [RxComputationThreadPool-7] INFO MqttMessagingPlugin - Successfully subscribed to: /server/+/login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
#### Verbindungsaufbau:
|
||||||
|
- **Vorher**: ~30s bis Timeout bei Fehlern
|
||||||
|
- **Nachher**: ~60s bis Timeout, aber schnellere Fehlerdiagnose
|
||||||
|
|
||||||
|
#### Nachrichtendurchsatz:
|
||||||
|
- Keine Änderung (QoS 2, exactly once)
|
||||||
|
- Envelope-Overhead: ~200 Bytes pro Nachricht
|
||||||
|
|
||||||
|
### Bekannte Probleme
|
||||||
|
|
||||||
|
#### Keine bekannten Probleme
|
||||||
|
Alle Tests erfolgreich durchgeführt.
|
||||||
|
|
||||||
|
### Nächste Schritte
|
||||||
|
|
||||||
|
1. **Client-Migration**: Mobile Apps auf neuen Port und Envelope-Format umstellen
|
||||||
|
2. **Monitoring**: Metriken für Message-Delivery-Service hinzufügen
|
||||||
|
3. **Dokumentation**: API-Dokumentation für Envelope-Format erweitern
|
||||||
|
4. **Testing**: Integration-Tests für verschiedene Fehlerszenarien
|
||||||
|
|
||||||
|
### Rollback-Plan
|
||||||
|
|
||||||
|
Falls Probleme auftreten:
|
||||||
|
|
||||||
|
1. **Port zurücksetzen**:
|
||||||
|
```properties
|
||||||
|
app.messaging.plugin.mqtt.broker.port=1883
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Alte Konfiguration wiederherstellen**:
|
||||||
|
- `MqttProperties.java` aus Git-Historie wiederherstellen
|
||||||
|
- `MqttConfig.java` aus Git-Historie wiederherstellen
|
||||||
|
- Alte `app.mqtt.*` Konfiguration in `application.properties` einfügen
|
||||||
|
|
||||||
|
3. **Server neu starten**
|
||||||
|
|
||||||
|
### Kontakt
|
||||||
|
|
||||||
|
Bei Fragen oder Problemen:
|
||||||
|
- Siehe `MQTT_MIGRATION_GUIDE.md` für Client-Migration
|
||||||
|
- Siehe `MESSAGING_LAYER.md` für Architektur-Details
|
||||||
|
- Siehe `CLAUDE.md` für allgemeine System-Dokumentation
|
||||||
|
|
||||||
318
MESSAGING_LAYER.md
Normal file
318
MESSAGING_LAYER.md
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
# Message Delivery Layer - Implementierungsdokumentation
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Der **Message Delivery Layer** ist eine neue Abstraktionsschicht zwischen der Anwendungslogik und dem MQTT-Transport. Er bietet:
|
||||||
|
|
||||||
|
- ✅ **Zuverlässige Nachrichtenzustellung** mit Bestätigungsmechanismus
|
||||||
|
- ✅ **Automatische Wiederholungsversuche** bei fehlgeschlagenen Zustellungen
|
||||||
|
- ✅ **Protokoll-Abstraktion** für einfachen Austausch von MQTT gegen andere Transporte
|
||||||
|
- ✅ **Vollständiges Audit-Log** aller Nachrichten
|
||||||
|
- ✅ **Queue-basiertes Design** für Skalierbarkeit
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
Application Layer (MessageController, Services)
|
||||||
|
↓
|
||||||
|
MessageDeliveryService (Envelope-Wrapping, Queue-Management)
|
||||||
|
↓
|
||||||
|
MessagingTransport Interface (Protokoll-Abstraktion)
|
||||||
|
↓
|
||||||
|
MqttTransportAdapter (MQTT-spezifische Implementierung)
|
||||||
|
↓
|
||||||
|
MqttV5ClientManager (HiveMQ Client)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Komponenten
|
||||||
|
|
||||||
|
### 1. Datenmodelle (`messaging/model/`)
|
||||||
|
|
||||||
|
#### MessageEnvelope
|
||||||
|
Wrapper für alle Nachrichten mit Metadaten:
|
||||||
|
- `messageId`: Eindeutige UUID
|
||||||
|
- `timestamp`: Erstellungszeitpunkt
|
||||||
|
- `topic`: Ziel-Topic
|
||||||
|
- `payload`: Ursprüngliche Nachricht
|
||||||
|
- `requiresAck`: Bestätigung erforderlich?
|
||||||
|
- `retryCount`: Anzahl Wiederholungsversuche
|
||||||
|
- `expiresAt`: Ablaufzeitpunkt
|
||||||
|
|
||||||
|
#### PendingDelivery
|
||||||
|
Tracking-Objekt für ausstehende Zustellungen:
|
||||||
|
- `messageId`: Referenz zur Envelope
|
||||||
|
- `status`: PENDING, SENT, ACKNOWLEDGED, FAILED, EXPIRED
|
||||||
|
- `retryCount`: Aktuelle Wiederholungsversuche
|
||||||
|
- `nextRetryAt`: Zeitpunkt des nächsten Versuchs
|
||||||
|
- `envelopeData`: Serialisierte Envelope-Daten
|
||||||
|
|
||||||
|
#### AcknowledgmentMessage
|
||||||
|
Bestätigungsnachricht vom Client:
|
||||||
|
- `messageId`: ID der bestätigten Nachricht
|
||||||
|
- `status`: RECEIVED, PROCESSED, FAILED
|
||||||
|
- `clientId`: Absender der Bestätigung
|
||||||
|
- `errorMessage`: Optional bei Fehler
|
||||||
|
|
||||||
|
### 2. Transport Layer (`messaging/transport/`)
|
||||||
|
|
||||||
|
#### MessagingTransport Interface
|
||||||
|
Protokoll-agnostische Schnittstelle:
|
||||||
|
```java
|
||||||
|
CompletableFuture<Void> send(String topic, byte[] payload, TransportOptions options);
|
||||||
|
void subscribe(String topicPattern, MessageHandler handler);
|
||||||
|
boolean isConnected();
|
||||||
|
String getTransportType();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MqttTransportAdapter
|
||||||
|
MQTT-Implementierung des Transport-Interfaces:
|
||||||
|
- Adaptiert HiveMQ MQTT Client
|
||||||
|
- Unterstützt MQTT-Wildcards (+ und #)
|
||||||
|
- QoS-Mapping (0, 1, 2)
|
||||||
|
|
||||||
|
### 3. Delivery Service (`messaging/delivery/`)
|
||||||
|
|
||||||
|
#### MessageDeliveryService
|
||||||
|
Zentrale Orchestrierung:
|
||||||
|
- `sendMessage()`: Nachricht mit Tracking senden
|
||||||
|
- `handleIncomingMessage()`: Eingehende Envelope verarbeiten
|
||||||
|
- `handleAcknowledgment()`: Bestätigung verarbeiten
|
||||||
|
- `retryPendingDeliveries()`: Wiederholungsversuche durchführen
|
||||||
|
- `cleanupOldDeliveries()`: Alte Einträge aufräumen
|
||||||
|
|
||||||
|
#### AcknowledgmentHandler
|
||||||
|
Routing eingehender Nachrichten zur Anwendungslogik:
|
||||||
|
- Unwrapping der Envelope
|
||||||
|
- Routing basierend auf Topic-Pattern
|
||||||
|
- Delegation an MessageController
|
||||||
|
|
||||||
|
#### RetryScheduler
|
||||||
|
Geplante Tasks:
|
||||||
|
- Retry-Task: Alle 30 Sekunden (konfigurierbar)
|
||||||
|
- Cleanup-Task: Stündlich (konfigurierbar)
|
||||||
|
|
||||||
|
### 4. Repositories
|
||||||
|
|
||||||
|
#### MessageEnvelopeRepository
|
||||||
|
Speicherung aller Envelope-Objekte in MongoDB Collection `message_envelopes`
|
||||||
|
|
||||||
|
#### PendingDeliveryRepository
|
||||||
|
Tracking ausstehender Zustellungen in MongoDB Collection `pending_deliveries`
|
||||||
|
|
||||||
|
## Nachrichtenfluss
|
||||||
|
|
||||||
|
### Outbound (Server → Client)
|
||||||
|
|
||||||
|
1. **Anwendung** ruft `MessageDeliveryService.sendMessage()` auf
|
||||||
|
2. **MessageEnvelope** wird erstellt mit eindeutiger `messageId`
|
||||||
|
3. **PendingDelivery** wird in Datenbank gespeichert (Status: PENDING)
|
||||||
|
4. **Envelope** wird als JSON serialisiert
|
||||||
|
5. **Transport** sendet Nachricht via MQTT
|
||||||
|
6. **Status** wird auf SENT aktualisiert, `nextRetryAt` gesetzt
|
||||||
|
7. **Client** empfängt Nachricht und sendet ACK
|
||||||
|
8. **AcknowledgmentHandler** verarbeitet ACK
|
||||||
|
9. **PendingDelivery** wird auf ACKNOWLEDGED gesetzt
|
||||||
|
|
||||||
|
### Inbound (Client → Server)
|
||||||
|
|
||||||
|
1. **MQTT** empfängt Nachricht auf Topic
|
||||||
|
2. **MqttTransportAdapter** leitet an MessageDeliveryService weiter
|
||||||
|
3. **MessageEnvelope** wird aus JSON deserialisiert
|
||||||
|
4. **ACK** wird automatisch an Client gesendet (falls `requiresAck=true`)
|
||||||
|
5. **AcknowledgmentHandler** routet Payload zur Anwendung
|
||||||
|
6. **MessageController** verarbeitet Business-Logik
|
||||||
|
|
||||||
|
## Retry-Mechanismus
|
||||||
|
|
||||||
|
### Exponential Backoff
|
||||||
|
```
|
||||||
|
Versuch 1: 5 Sekunden
|
||||||
|
Versuch 2: 10 Sekunden (5s × 2.0)
|
||||||
|
Versuch 3: 20 Sekunden (10s × 2.0)
|
||||||
|
Versuch 4: 40 Sekunden (20s × 2.0)
|
||||||
|
Maximum: 5 Minuten
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ablauf
|
||||||
|
1. **RetryScheduler** läuft alle 30 Sekunden
|
||||||
|
2. Findet alle `PendingDelivery` mit Status SENT und `nextRetryAt` in Vergangenheit
|
||||||
|
3. Prüft auf Ablauf (`expiresAt`) und Max-Retries
|
||||||
|
4. Sendet Nachricht erneut via Transport
|
||||||
|
5. Aktualisiert `retryCount` und `nextRetryAt`
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### application.properties
|
||||||
|
```properties
|
||||||
|
# Message Delivery Layer Configuration
|
||||||
|
app.messaging.delivery.max-retries=3
|
||||||
|
app.messaging.delivery.retry-initial-delay=5s
|
||||||
|
app.messaging.delivery.retry-max-delay=5m
|
||||||
|
app.messaging.delivery.retry-backoff-multiplier=2.0
|
||||||
|
app.messaging.delivery.ack-timeout=30s
|
||||||
|
app.messaging.delivery.message-expiry=24h
|
||||||
|
app.messaging.delivery.cleanup-interval-minutes=60
|
||||||
|
app.messaging.delivery.retry-interval-seconds=30
|
||||||
|
app.messaging.delivery.acknowledged-retention-days=7
|
||||||
|
```
|
||||||
|
|
||||||
|
## MQTT Topics
|
||||||
|
|
||||||
|
### Outbound (Server → Client)
|
||||||
|
```
|
||||||
|
/client/{clientId}/message # Chat-Nachrichten (wrapped)
|
||||||
|
/client/{clientId}/jobs # Job-Zuweisungen (wrapped)
|
||||||
|
/client/{clientId}/auth # Login-Antworten (wrapped)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inbound (Client → Server)
|
||||||
|
```
|
||||||
|
/server/{clientId}/task_completed # Task-Abschlüsse (wrapped)
|
||||||
|
/server/{clientId}/message # Chat-Nachrichten (wrapped)
|
||||||
|
/server/{clientId}/jobs/assigned # Job-Anfragen (wrapped)
|
||||||
|
/server/login # Login-Anfragen (wrapped)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Acknowledgments
|
||||||
|
```
|
||||||
|
/ack/server/{messageId} # Client → Server ACK
|
||||||
|
/ack/client/{clientId}/{messageId} # Server → Client ACK
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration bestehender Clients
|
||||||
|
|
||||||
|
### Phase 1: Server-seitig (✅ Abgeschlossen)
|
||||||
|
- Message Delivery Layer implementiert
|
||||||
|
- MqttPublisher nutzt jetzt MessageDeliveryService
|
||||||
|
- Backward-kompatibel: Legacy-Nachrichten werden weiterhin verarbeitet
|
||||||
|
|
||||||
|
### Phase 2: Client-seitig (TODO)
|
||||||
|
Flutter-App muss angepasst werden:
|
||||||
|
|
||||||
|
1. **Envelope-Handling**
|
||||||
|
```dart
|
||||||
|
class MessageEnvelope {
|
||||||
|
String messageId;
|
||||||
|
DateTime timestamp;
|
||||||
|
String topic;
|
||||||
|
Map<String, dynamic> payload;
|
||||||
|
bool requiresAck;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Empfang**
|
||||||
|
```dart
|
||||||
|
void handleIncomingMessage(String topic, String json) {
|
||||||
|
final envelope = MessageEnvelope.fromJson(json);
|
||||||
|
|
||||||
|
// ACK senden
|
||||||
|
if (envelope.requiresAck) {
|
||||||
|
sendAcknowledgment(envelope.messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload verarbeiten
|
||||||
|
processPayload(envelope.payload);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **ACK senden**
|
||||||
|
```dart
|
||||||
|
void sendAcknowledgment(String messageId) {
|
||||||
|
final ack = {
|
||||||
|
'messageId': messageId,
|
||||||
|
'status': 'RECEIVED',
|
||||||
|
'timestamp': DateTime.now().toIso8601String(),
|
||||||
|
'clientId': myClientId
|
||||||
|
};
|
||||||
|
|
||||||
|
mqtt.publish('/ack/server/$messageId', jsonEncode(ack));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Senden**
|
||||||
|
```dart
|
||||||
|
void sendMessage(String topic, Map<String, dynamic> payload) {
|
||||||
|
final envelope = MessageEnvelope(
|
||||||
|
messageId: Uuid().v4(),
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
topic: topic,
|
||||||
|
payload: payload,
|
||||||
|
requiresAck: true
|
||||||
|
);
|
||||||
|
|
||||||
|
mqtt.publish(topic, jsonEncode(envelope.toJson()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring & Debugging
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
Alle Komponenten loggen mit Präfix:
|
||||||
|
- `[MessageDelivery]`: MessageDeliveryService
|
||||||
|
- `[MqttTransport]`: MqttTransportAdapter
|
||||||
|
- `[AckHandler]`: AcknowledgmentHandler
|
||||||
|
- `[RetryScheduler]`: RetryScheduler
|
||||||
|
- `[MessagingConfig]`: MessagingConfig
|
||||||
|
|
||||||
|
### Datenbank-Queries
|
||||||
|
|
||||||
|
**Ausstehende Zustellungen:**
|
||||||
|
```javascript
|
||||||
|
db.pending_deliveries.find({ status: "SENT" })
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fehlgeschlagene Zustellungen:**
|
||||||
|
```javascript
|
||||||
|
db.pending_deliveries.find({ status: "FAILED" })
|
||||||
|
```
|
||||||
|
|
||||||
|
**Retry-Statistiken:**
|
||||||
|
```javascript
|
||||||
|
db.pending_deliveries.aggregate([
|
||||||
|
{ $group: { _id: "$status", count: { $sum: 1 } } }
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nachrichten eines Clients:**
|
||||||
|
```javascript
|
||||||
|
db.pending_deliveries.find({ client_id: "client123" })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vorteile
|
||||||
|
|
||||||
|
### Zuverlässigkeit
|
||||||
|
- Garantierte Zustellung durch Retry-Mechanismus
|
||||||
|
- Persistierung in MongoDB verhindert Datenverlust
|
||||||
|
- Acknowledgment-Tracking für Nachvollziehbarkeit
|
||||||
|
|
||||||
|
### Skalierbarkeit
|
||||||
|
- Queue-basiertes Design
|
||||||
|
- Asynchrone Verarbeitung mit CompletableFuture
|
||||||
|
- Scheduled Tasks für Hintergrundverarbeitung
|
||||||
|
|
||||||
|
### Wartbarkeit
|
||||||
|
- Klare Trennung der Verantwortlichkeiten
|
||||||
|
- Protokoll-Abstraktion ermöglicht einfachen Austausch
|
||||||
|
- Umfangreiches Logging für Debugging
|
||||||
|
|
||||||
|
### Flexibilität
|
||||||
|
- Konfigurierbare Retry-Parameter
|
||||||
|
- Verschiedene DeliveryOptions (standard, fireAndForget, critical)
|
||||||
|
- Erweiterbar für andere Transporte (WebSocket, gRPC, etc.)
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
1. ✅ Server-seitige Implementierung abgeschlossen
|
||||||
|
2. ⏳ Flutter-App anpassen (Envelope-Handling)
|
||||||
|
3. ⏳ Integration-Tests schreiben
|
||||||
|
4. ⏳ Monitoring-Dashboard erstellen
|
||||||
|
5. ⏳ Performance-Optimierung
|
||||||
|
6. ⏳ Dokumentation für Client-Entwickler
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Bei Fragen oder Problemen:
|
||||||
|
- Logs prüfen: `logs/votianlt.log`
|
||||||
|
- MongoDB Collections: `message_envelopes`, `pending_deliveries`
|
||||||
|
- Konfiguration: `application.properties`
|
||||||
|
|
||||||
266
MIGRATION_SUMMARY.md
Normal file
266
MIGRATION_SUMMARY.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# MQTT Migration Summary - Dokumentationsübersicht
|
||||||
|
|
||||||
|
## 📚 Erstellte Dokumentation
|
||||||
|
|
||||||
|
Alle Änderungen am MQTT-System wurden umfassend dokumentiert, um die Client-Migration zu erleichtern.
|
||||||
|
|
||||||
|
### 1. MQTT_MIGRATION_GUIDE.md
|
||||||
|
**Zielgruppe**: Client-Entwickler (Flutter/Dart)
|
||||||
|
**Inhalt**:
|
||||||
|
- Übersicht aller Server-Änderungen
|
||||||
|
- Detaillierte Client-Anpassungen mit Code-Beispielen
|
||||||
|
- Message-Envelope-Format
|
||||||
|
- Acknowledgment-Handling
|
||||||
|
- Migration Checklist
|
||||||
|
- Abwärtskompatibilität
|
||||||
|
- Vorteile des neuen Systems
|
||||||
|
|
||||||
|
**Wichtigste Punkte**:
|
||||||
|
- ⚠️ Port-Änderung: 1883 → 42099
|
||||||
|
- ⚠️ Authentifizierung erforderlich: app/apppwd
|
||||||
|
- ⚠️ Timeout erhöht: 30s → 60s
|
||||||
|
- ✅ Message-Envelope-Format implementieren
|
||||||
|
- ✅ ACK-Handling implementieren
|
||||||
|
|
||||||
|
### 2. MQTT_QUICK_REFERENCE.md
|
||||||
|
**Zielgruppe**: Client-Entwickler (Flutter/Dart)
|
||||||
|
**Inhalt**:
|
||||||
|
- Kritische Änderungen auf einen Blick
|
||||||
|
- Fertige Code-Snippets für Flutter/Dart
|
||||||
|
- Verbindungsaufbau
|
||||||
|
- Message-Envelope-Klasse
|
||||||
|
- Nachricht senden/empfangen
|
||||||
|
- ACK-Handling
|
||||||
|
- Debugging-Tipps
|
||||||
|
- Häufige Fehler und Lösungen
|
||||||
|
|
||||||
|
**Verwendung**:
|
||||||
|
Schnelle Referenz während der Implementierung - alle wichtigen Code-Beispiele sofort verfügbar.
|
||||||
|
|
||||||
|
### 3. CHANGELOG_MQTT.md
|
||||||
|
**Zielgruppe**: Alle Entwickler, DevOps
|
||||||
|
**Inhalt**:
|
||||||
|
- Breaking Changes
|
||||||
|
- Neue Features
|
||||||
|
- Entfernte Komponenten
|
||||||
|
- Geänderte Komponenten
|
||||||
|
- Architektur-Änderungen
|
||||||
|
- Migration Guide
|
||||||
|
- Kompatibilität
|
||||||
|
- Testing
|
||||||
|
- Performance
|
||||||
|
- Rollback-Plan
|
||||||
|
|
||||||
|
**Verwendung**:
|
||||||
|
Vollständige Historie aller Änderungen für Audit und Troubleshooting.
|
||||||
|
|
||||||
|
### 4. MQTT_README.md (aktualisiert)
|
||||||
|
**Zielgruppe**: Alle Entwickler
|
||||||
|
**Inhalt**:
|
||||||
|
- ⚠️ Neue Broker-Konfiguration prominent hervorgehoben
|
||||||
|
- Aktualisierte Connection-Parameter
|
||||||
|
- Hinweise auf Migration-Dokumentation
|
||||||
|
- Bestehende API-Dokumentation bleibt erhalten
|
||||||
|
|
||||||
|
**Änderungen**:
|
||||||
|
- Broker-URL und Port aktualisiert
|
||||||
|
- Authentifizierung dokumentiert
|
||||||
|
- Verweise auf neue Dokumentation hinzugefügt
|
||||||
|
|
||||||
|
### 5. MESSAGING_LAYER.md (bereits vorhanden)
|
||||||
|
**Zielgruppe**: Backend-Entwickler
|
||||||
|
**Inhalt**:
|
||||||
|
- Detaillierte Architektur des Messaging-Systems
|
||||||
|
- Plugin-basiertes Design
|
||||||
|
- Message-Delivery-Service
|
||||||
|
- Komponenten-Beschreibungen
|
||||||
|
|
||||||
|
**Status**: Bereits vorhanden, keine Änderungen erforderlich.
|
||||||
|
|
||||||
|
## 🔄 Server-Änderungen (bereits durchgeführt)
|
||||||
|
|
||||||
|
### Konfiguration
|
||||||
|
**Datei**: `src/main/resources/application.properties`
|
||||||
|
|
||||||
|
**Entfernt** (Zeilen 57-71):
|
||||||
|
```properties
|
||||||
|
# MQTT v5 settings (alt)
|
||||||
|
app.mqtt.enabled=true
|
||||||
|
app.mqtt.broker-uri=mqtt://mqtt-2.assecutor.de
|
||||||
|
app.mqtt.client-id=server-${random.uuid}
|
||||||
|
# ... weitere alte Einstellungen
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aktiv**:
|
||||||
|
```properties
|
||||||
|
# Messaging Plugin Configuration
|
||||||
|
app.messaging.plugin.type=mqtt
|
||||||
|
app.messaging.plugin.mqtt.broker.host=mqtt-2.assecutor.de
|
||||||
|
app.messaging.plugin.mqtt.broker.port=42099
|
||||||
|
app.messaging.plugin.mqtt.username=app
|
||||||
|
app.messaging.plugin.mqtt.password=apppwd
|
||||||
|
app.messaging.plugin.mqtt.client.id=votianlt-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code-Änderungen
|
||||||
|
|
||||||
|
#### 1. MqttMessagingPlugin.java
|
||||||
|
**Datei**: `src/main/java/de/assecutor/votianlt/messaging/plugin/mqtt/MqttMessagingPlugin.java`
|
||||||
|
|
||||||
|
**Änderungen**:
|
||||||
|
- Connection-Timeout: 30s → 60s (konfigurierbar)
|
||||||
|
- Keep-Alive: konfigurierbar (Standard: 60s)
|
||||||
|
- Verbesserte Fehlerbehandlung:
|
||||||
|
- TimeoutException → "Broker nicht erreichbar"
|
||||||
|
- UnknownHostException → "DNS-Fehler"
|
||||||
|
- ConnectException → "Port blockiert"
|
||||||
|
- Detailliertes Logging
|
||||||
|
|
||||||
|
#### 2. Gelöschte Klassen
|
||||||
|
- `src/main/java/de/assecutor/votianlt/config/MqttProperties.java`
|
||||||
|
- `src/main/java/de/assecutor/votianlt/config/MqttConfig.java`
|
||||||
|
|
||||||
|
**Grund**: Ersetzt durch Plugin-Konfiguration, nicht mehr verwendet.
|
||||||
|
|
||||||
|
## ✅ Verifikation
|
||||||
|
|
||||||
|
### Build erfolgreich
|
||||||
|
```bash
|
||||||
|
./mvnw clean compile
|
||||||
|
# [INFO] BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server läuft
|
||||||
|
```bash
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
# Application running at http://localhost:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
### MQTT-Verbindung erfolgreich
|
||||||
|
```
|
||||||
|
[MqttPlugin] Connected successfully - connAck: MqttConnAck{reasonCode=SUCCESS}
|
||||||
|
[MqttPlugin] Successfully subscribed to: /ack/server/+
|
||||||
|
[MqttPlugin] Successfully subscribed to: /server/+/task_completed
|
||||||
|
[MqttPlugin] Successfully subscribed to: /server/+/jobs/assigned
|
||||||
|
[MqttPlugin] Successfully subscribed to: /server/+/message
|
||||||
|
[MqttPlugin] Successfully subscribed to: /server/+/login
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Client Migration Checklist
|
||||||
|
|
||||||
|
### Vorbereitung
|
||||||
|
- [ ] `MQTT_MIGRATION_GUIDE.md` lesen
|
||||||
|
- [ ] `MQTT_QUICK_REFERENCE.md` als Referenz bereithalten
|
||||||
|
- [ ] Testumgebung vorbereiten
|
||||||
|
|
||||||
|
### Implementierung
|
||||||
|
- [ ] Port auf 42099 ändern
|
||||||
|
- [ ] Authentifizierung hinzufügen (app/apppwd)
|
||||||
|
- [ ] Connection-Timeout auf 60s erhöhen
|
||||||
|
- [ ] Keep-Alive auf 60s setzen
|
||||||
|
- [ ] MessageEnvelope-Klasse implementieren (siehe Quick Reference)
|
||||||
|
- [ ] AcknowledgmentMessage-Klasse implementieren (siehe Quick Reference)
|
||||||
|
- [ ] Envelope-basiertes Senden implementieren
|
||||||
|
- [ ] Envelope-basiertes Empfangen implementieren
|
||||||
|
- [ ] ACK-Handling implementieren
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Verbindung testen (siehe Quick Reference: testConnection())
|
||||||
|
- [ ] Nachricht senden testen
|
||||||
|
- [ ] Nachricht empfangen testen
|
||||||
|
- [ ] ACK-Handling testen
|
||||||
|
- [ ] Fehlerszenarien testen (Timeout, Verbindungsabbruch)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
- [ ] Staging-Umgebung testen
|
||||||
|
- [ ] Produktions-Deployment planen
|
||||||
|
- [ ] Rollback-Plan bereithalten
|
||||||
|
|
||||||
|
## 🔗 Dokumentations-Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
MQTT-Dokumentation/
|
||||||
|
├── MQTT_MIGRATION_GUIDE.md ← Hauptdokumentation für Client-Migration
|
||||||
|
├── MQTT_QUICK_REFERENCE.md ← Code-Beispiele und Schnellreferenz
|
||||||
|
├── CHANGELOG_MQTT.md ← Vollständige Änderungshistorie
|
||||||
|
├── MQTT_README.md ← API-Referenz (aktualisiert)
|
||||||
|
├── MESSAGING_LAYER.md ← Architektur-Dokumentation (vorhanden)
|
||||||
|
└── MIGRATION_SUMMARY.md ← Diese Datei (Übersicht)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Nächste Schritte
|
||||||
|
|
||||||
|
### Für Client-Entwickler:
|
||||||
|
1. **Lesen**: `MQTT_MIGRATION_GUIDE.md` durcharbeiten
|
||||||
|
2. **Referenz**: `MQTT_QUICK_REFERENCE.md` für Code-Beispiele nutzen
|
||||||
|
3. **Implementieren**: Änderungen gemäß Checklist umsetzen
|
||||||
|
4. **Testen**: Verbindung und Nachrichtenaustausch testen
|
||||||
|
5. **Deployen**: Nach erfolgreichem Test in Produktion bringen
|
||||||
|
|
||||||
|
### Für Backend-Entwickler:
|
||||||
|
1. **Monitoring**: Message-Delivery-Metriken überwachen
|
||||||
|
2. **Logs**: Verbindungsprobleme in Logs prüfen
|
||||||
|
3. **Support**: Client-Entwickler bei Migration unterstützen
|
||||||
|
4. **Dokumentation**: Bei Bedarf weitere Beispiele hinzufügen
|
||||||
|
|
||||||
|
### Für DevOps:
|
||||||
|
1. **Firewall**: Port 42099 für Clients freigeben
|
||||||
|
2. **Monitoring**: MQTT-Broker-Status überwachen
|
||||||
|
3. **Backup**: Rollback-Plan bereithalten
|
||||||
|
4. **Logs**: Zentrales Logging für MQTT-Verbindungen einrichten
|
||||||
|
|
||||||
|
## 💡 Wichtige Hinweise
|
||||||
|
|
||||||
|
### Abwärtskompatibilität
|
||||||
|
Der Server unterstützt **vorübergehend** noch Legacy-Nachrichten ohne Envelope-Format. Dies ermöglicht eine schrittweise Migration der Clients.
|
||||||
|
|
||||||
|
**Empfehlung**: Alle Clients sollten so schnell wie möglich auf das neue Envelope-Format umgestellt werden.
|
||||||
|
|
||||||
|
### Vorteile des neuen Systems
|
||||||
|
1. **Zuverlässigkeit**: ACK-basierte Zustellung mit Retries
|
||||||
|
2. **Nachverfolgung**: Jede Nachricht hat eindeutige ID
|
||||||
|
3. **Fehlerbehandlung**: Detaillierte Fehlerinformationen
|
||||||
|
4. **Monitoring**: Vollständige Nachrichtenverfolgung
|
||||||
|
5. **Skalierbarkeit**: Queue-basiertes Design
|
||||||
|
|
||||||
|
### Support
|
||||||
|
Bei Fragen oder Problemen:
|
||||||
|
1. Dokumentation konsultieren (siehe oben)
|
||||||
|
2. Logs auf Client- und Server-Seite prüfen
|
||||||
|
3. Netzwerk-Konnektivität testen (Port 42099)
|
||||||
|
4. MQTT-Client-Tool verwenden (z.B. MQTT Explorer)
|
||||||
|
|
||||||
|
## 📊 Zusammenfassung
|
||||||
|
|
||||||
|
### Was wurde geändert?
|
||||||
|
- ✅ MQTT-Broker-Port: 1883 → 42099
|
||||||
|
- ✅ Authentifizierung hinzugefügt: app/apppwd
|
||||||
|
- ✅ Timeout erhöht: 30s → 60s
|
||||||
|
- ✅ Alte Konfiguration entfernt
|
||||||
|
- ✅ Fehlerbehandlung verbessert
|
||||||
|
|
||||||
|
### Was wurde dokumentiert?
|
||||||
|
- ✅ Migration Guide für Clients
|
||||||
|
- ✅ Quick Reference mit Code-Beispielen
|
||||||
|
- ✅ Vollständiger Changelog
|
||||||
|
- ✅ Aktualisierte API-Dokumentation
|
||||||
|
- ✅ Diese Übersicht
|
||||||
|
|
||||||
|
### Was muss der Client tun?
|
||||||
|
- ⚠️ Port auf 42099 ändern
|
||||||
|
- ⚠️ Authentifizierung hinzufügen
|
||||||
|
- ⚠️ Timeout erhöhen
|
||||||
|
- ✅ Message-Envelope implementieren (optional, aber empfohlen)
|
||||||
|
- ✅ ACK-Handling implementieren (optional, aber empfohlen)
|
||||||
|
|
||||||
|
### Zeitplan
|
||||||
|
- **Sofort**: Port und Authentifizierung ändern (kritisch!)
|
||||||
|
- **Kurzfristig**: Envelope-Format implementieren (empfohlen)
|
||||||
|
- **Mittelfristig**: ACK-Handling implementieren (empfohlen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Stand**: 2025-10-22
|
||||||
|
**Version**: 2.0.0
|
||||||
|
**Status**: ✅ Server-Migration abgeschlossen, Client-Migration ausstehend
|
||||||
|
|
||||||
282
MQTT_DOCS_README.md
Normal file
282
MQTT_DOCS_README.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# MQTT System Dokumentation - Übersicht
|
||||||
|
|
||||||
|
## 🎯 Zweck dieser Dokumentation
|
||||||
|
|
||||||
|
Diese Dokumentation beschreibt die Migration des MQTT-Systems auf Version 2.0 mit Plugin-basierter Architektur und hilft Client-Entwicklern bei der Umstellung ihrer Anwendungen.
|
||||||
|
|
||||||
|
## 📚 Dokumentations-Struktur
|
||||||
|
|
||||||
|
### Für Client-Entwickler (Flutter/Dart)
|
||||||
|
|
||||||
|
#### 1. **MQTT_QUICK_REFERENCE.md** ⭐ START HERE
|
||||||
|
**Empfohlen für**: Schnelle Implementierung
|
||||||
|
**Inhalt**:
|
||||||
|
- ⚠️ Kritische Änderungen auf einen Blick
|
||||||
|
- 📋 Fertige Code-Snippets zum Copy-Paste
|
||||||
|
- 🔧 Verbindungsaufbau
|
||||||
|
- 📨 Nachricht senden/empfangen
|
||||||
|
- ✅ ACK-Handling
|
||||||
|
- 🐛 Debugging-Tipps
|
||||||
|
- ❌ Häufige Fehler und Lösungen
|
||||||
|
|
||||||
|
**Verwendung**: Als Referenz während der Implementierung offen halten.
|
||||||
|
|
||||||
|
#### 2. **MQTT_MIGRATION_GUIDE.md** 📖 DETAILLIERT
|
||||||
|
**Empfohlen für**: Vollständiges Verständnis
|
||||||
|
**Inhalt**:
|
||||||
|
- 📊 Übersicht aller Server-Änderungen
|
||||||
|
- 🔄 Detaillierte Client-Anpassungen
|
||||||
|
- 📦 Message-Envelope-Format
|
||||||
|
- ✅ Acknowledgment-System
|
||||||
|
- ☑️ Migration Checklist
|
||||||
|
- 🔙 Abwärtskompatibilität
|
||||||
|
- 💡 Vorteile des neuen Systems
|
||||||
|
- 🧪 Testing-Strategien
|
||||||
|
|
||||||
|
**Verwendung**: Vor der Implementierung komplett durchlesen.
|
||||||
|
|
||||||
|
### Für alle Entwickler
|
||||||
|
|
||||||
|
#### 3. **MIGRATION_SUMMARY.md** 📋 ÜBERSICHT
|
||||||
|
**Empfohlen für**: Schneller Überblick
|
||||||
|
**Inhalt**:
|
||||||
|
- 📚 Übersicht aller Dokumentationen
|
||||||
|
- 🔄 Zusammenfassung der Server-Änderungen
|
||||||
|
- ✅ Verifikation der Änderungen
|
||||||
|
- 📋 Client Migration Checklist
|
||||||
|
- 🔗 Dokumentations-Struktur
|
||||||
|
- 🎯 Nächste Schritte
|
||||||
|
|
||||||
|
**Verwendung**: Als Einstiegspunkt und Orientierung.
|
||||||
|
|
||||||
|
#### 4. **CHANGELOG_MQTT.md** 📝 HISTORIE
|
||||||
|
**Empfohlen für**: Audit und Troubleshooting
|
||||||
|
**Inhalt**:
|
||||||
|
- 💥 Breaking Changes
|
||||||
|
- ✨ Neue Features
|
||||||
|
- 🗑️ Entfernte Komponenten
|
||||||
|
- 🔧 Geänderte Komponenten
|
||||||
|
- 🏗️ Architektur-Änderungen
|
||||||
|
- 🧪 Testing-Ergebnisse
|
||||||
|
- 📊 Performance-Metriken
|
||||||
|
- 🔄 Rollback-Plan
|
||||||
|
|
||||||
|
**Verwendung**: Für detaillierte Änderungshistorie und Rollback-Planung.
|
||||||
|
|
||||||
|
#### 5. **MQTT_README.md** 📖 API-REFERENZ
|
||||||
|
**Empfohlen für**: API-Dokumentation
|
||||||
|
**Inhalt**:
|
||||||
|
- 🔌 Broker-Konfiguration (aktualisiert!)
|
||||||
|
- 📡 Topic-Struktur
|
||||||
|
- 📨 Message-Formate
|
||||||
|
- 🔐 Authentifizierung
|
||||||
|
- ⚙️ Connection-Parameter
|
||||||
|
|
||||||
|
**Verwendung**: Als API-Referenz für Topic-Struktur und Message-Formate.
|
||||||
|
|
||||||
|
### Für Backend-Entwickler
|
||||||
|
|
||||||
|
#### 6. **MESSAGING_LAYER.md** 🏗️ ARCHITEKTUR
|
||||||
|
**Empfohlen für**: Backend-Entwicklung
|
||||||
|
**Inhalt**:
|
||||||
|
- 🏗️ Architektur-Übersicht
|
||||||
|
- 🔌 Plugin-System
|
||||||
|
- 📦 Message-Delivery-Service
|
||||||
|
- 🔄 Nachrichtenfluss
|
||||||
|
- 🧩 Komponenten-Beschreibungen
|
||||||
|
- 🔧 Konfiguration
|
||||||
|
- 🐛 Fehlerbehandlung
|
||||||
|
|
||||||
|
**Verwendung**: Für Backend-Entwicklung und Architektur-Verständnis.
|
||||||
|
|
||||||
|
## 🚀 Quick Start für Client-Entwickler
|
||||||
|
|
||||||
|
### Schritt 1: Kritische Änderungen verstehen
|
||||||
|
```bash
|
||||||
|
# Lesen Sie zuerst die kritischen Änderungen
|
||||||
|
cat MQTT_QUICK_REFERENCE.md | head -50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtigste Änderungen**:
|
||||||
|
- ⚠️ **Port**: 1883 → **42099**
|
||||||
|
- ⚠️ **Auth**: Username: `app`, Password: `apppwd`
|
||||||
|
- ⚠️ **Timeout**: 30s → **60s**
|
||||||
|
|
||||||
|
### Schritt 2: Code-Beispiele kopieren
|
||||||
|
```bash
|
||||||
|
# Öffnen Sie die Quick Reference
|
||||||
|
open MQTT_QUICK_REFERENCE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Kopieren Sie die Code-Snippets für:
|
||||||
|
1. Verbindungsaufbau
|
||||||
|
2. MessageEnvelope-Klasse
|
||||||
|
3. Nachricht senden
|
||||||
|
4. Nachricht empfangen
|
||||||
|
5. ACK-Handling
|
||||||
|
|
||||||
|
### Schritt 3: Implementieren und Testen
|
||||||
|
```dart
|
||||||
|
// 1. Verbindung testen
|
||||||
|
await testConnection();
|
||||||
|
|
||||||
|
// 2. Nachricht senden testen
|
||||||
|
await sendMessage(client, clientId, 'test', {'hello': 'world'});
|
||||||
|
|
||||||
|
// 3. Nachricht empfangen testen
|
||||||
|
setupMessageListener(client, clientId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 4: Migration Checklist abarbeiten
|
||||||
|
```bash
|
||||||
|
# Öffnen Sie die Migration Checklist
|
||||||
|
open MIGRATION_SUMMARY.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Arbeiten Sie die Checklist Punkt für Punkt ab.
|
||||||
|
|
||||||
|
## 📊 Änderungen auf einen Blick
|
||||||
|
|
||||||
|
### Server-Änderungen (✅ Abgeschlossen)
|
||||||
|
|
||||||
|
| Komponente | Alt | Neu | Status |
|
||||||
|
|------------|-----|-----|--------|
|
||||||
|
| **Port** | 1883 | 42099 | ✅ |
|
||||||
|
| **Auth** | Keine | app/apppwd | ✅ |
|
||||||
|
| **Timeout** | 30s | 60s | ✅ |
|
||||||
|
| **Konfiguration** | app.mqtt.* | app.messaging.plugin.mqtt.* | ✅ |
|
||||||
|
| **Fehlerbehandlung** | Basic | Detailliert | ✅ |
|
||||||
|
|
||||||
|
### Client-Änderungen (⚠️ Erforderlich)
|
||||||
|
|
||||||
|
| Aufgabe | Priorität | Dokumentation |
|
||||||
|
|---------|-----------|---------------|
|
||||||
|
| Port auf 42099 ändern | 🔴 KRITISCH | Quick Reference |
|
||||||
|
| Authentifizierung hinzufügen | 🔴 KRITISCH | Quick Reference |
|
||||||
|
| Timeout auf 60s erhöhen | 🟡 WICHTIG | Quick Reference |
|
||||||
|
| MessageEnvelope implementieren | 🟢 EMPFOHLEN | Migration Guide |
|
||||||
|
| ACK-Handling implementieren | 🟢 EMPFOHLEN | Migration Guide |
|
||||||
|
|
||||||
|
## 🎯 Empfohlener Workflow
|
||||||
|
|
||||||
|
### Für Client-Entwickler
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Start] --> B[Quick Reference lesen]
|
||||||
|
B --> C[Kritische Änderungen umsetzen]
|
||||||
|
C --> D[Code-Snippets kopieren]
|
||||||
|
D --> E[Implementieren]
|
||||||
|
E --> F[Testen]
|
||||||
|
F --> G{Tests OK?}
|
||||||
|
G -->|Nein| H[Migration Guide konsultieren]
|
||||||
|
H --> E
|
||||||
|
G -->|Ja| I[Deployment]
|
||||||
|
I --> J[Ende]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Für Backend-Entwickler
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Start] --> B[Changelog lesen]
|
||||||
|
B --> C[Messaging Layer verstehen]
|
||||||
|
C --> D[Monitoring einrichten]
|
||||||
|
D --> E[Client-Support]
|
||||||
|
E --> F[Ende]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Troubleshooting-Guide
|
||||||
|
|
||||||
|
### Problem: Wo finde ich...?
|
||||||
|
|
||||||
|
| Frage | Antwort | Dokument |
|
||||||
|
|-------|---------|----------|
|
||||||
|
| Wie verbinde ich mich? | Code-Snippet | MQTT_QUICK_REFERENCE.md |
|
||||||
|
| Warum funktioniert die Verbindung nicht? | Debugging-Tipps | MQTT_QUICK_REFERENCE.md |
|
||||||
|
| Was ist ein MessageEnvelope? | Detaillierte Erklärung | MQTT_MIGRATION_GUIDE.md |
|
||||||
|
| Welche Änderungen gab es? | Vollständige Liste | CHANGELOG_MQTT.md |
|
||||||
|
| Wie ist die Architektur? | Architektur-Diagramm | MESSAGING_LAYER.md |
|
||||||
|
| Was sind die Topics? | Topic-Struktur | MQTT_README.md |
|
||||||
|
|
||||||
|
### Problem: Verbindung schlägt fehl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Quick Reference öffnen
|
||||||
|
open MQTT_QUICK_REFERENCE.md
|
||||||
|
|
||||||
|
# 2. Abschnitt "Debugging" suchen
|
||||||
|
# 3. "Verbindung testen" Code ausführen
|
||||||
|
# 4. "Häufige Fehler" konsultieren
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: Nachrichten kommen nicht an
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Migration Guide öffnen
|
||||||
|
open MQTT_MIGRATION_GUIDE.md
|
||||||
|
|
||||||
|
# 2. Abschnitt "Nachrichten empfangen" suchen
|
||||||
|
# 3. Code-Beispiel mit eigenem Code vergleichen
|
||||||
|
# 4. Topic-Struktur in MQTT_README.md prüfen
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
### Dokumentation nicht hilfreich?
|
||||||
|
|
||||||
|
1. **Logs prüfen**: Client- und Server-Logs analysieren
|
||||||
|
2. **Netzwerk testen**: Port 42099 Erreichbarkeit prüfen
|
||||||
|
3. **MQTT-Tool verwenden**: MQTT Explorer zum Debuggen nutzen
|
||||||
|
4. **Changelog konsultieren**: Alle Änderungen nochmal durchgehen
|
||||||
|
|
||||||
|
### Weitere Ressourcen
|
||||||
|
|
||||||
|
- **HiveMQ Dokumentation**: https://www.hivemq.com/docs/
|
||||||
|
- **MQTT v5 Spezifikation**: https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
|
||||||
|
- **Flutter MQTT Client**: https://pub.dev/packages/mqtt_client
|
||||||
|
|
||||||
|
## 📈 Status
|
||||||
|
|
||||||
|
| Komponente | Status | Datum |
|
||||||
|
|------------|--------|-------|
|
||||||
|
| Server-Migration | ✅ Abgeschlossen | 2025-10-22 |
|
||||||
|
| Dokumentation | ✅ Abgeschlossen | 2025-10-22 |
|
||||||
|
| Client-Migration | ⏳ Ausstehend | TBD |
|
||||||
|
| Testing | ⏳ Ausstehend | TBD |
|
||||||
|
| Deployment | ⏳ Ausstehend | TBD |
|
||||||
|
|
||||||
|
## 🎓 Lernpfad
|
||||||
|
|
||||||
|
### Anfänger (Nur Verbindung herstellen)
|
||||||
|
1. MQTT_QUICK_REFERENCE.md → Abschnitt "Verbindung herstellen"
|
||||||
|
2. Code kopieren und anpassen
|
||||||
|
3. Testen
|
||||||
|
|
||||||
|
### Fortgeschritten (Vollständige Integration)
|
||||||
|
1. MIGRATION_SUMMARY.md → Überblick verschaffen
|
||||||
|
2. MQTT_MIGRATION_GUIDE.md → Komplett durchlesen
|
||||||
|
3. MQTT_QUICK_REFERENCE.md → Als Referenz nutzen
|
||||||
|
4. Implementieren und testen
|
||||||
|
|
||||||
|
### Experte (Architektur verstehen)
|
||||||
|
1. CHANGELOG_MQTT.md → Alle Änderungen verstehen
|
||||||
|
2. MESSAGING_LAYER.md → Architektur studieren
|
||||||
|
3. MQTT_README.md → API-Details lernen
|
||||||
|
4. Eigene Erweiterungen entwickeln
|
||||||
|
|
||||||
|
## 📝 Feedback
|
||||||
|
|
||||||
|
Diese Dokumentation wurde erstellt, um die Client-Migration so einfach wie möglich zu machen. Bei Fragen, Unklarheiten oder Verbesserungsvorschlägen:
|
||||||
|
|
||||||
|
1. Dokumentation aktualisieren
|
||||||
|
2. Beispiele hinzufügen
|
||||||
|
3. Diagramme erweitern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version**: 2.0.0
|
||||||
|
**Stand**: 2025-10-22
|
||||||
|
**Autor**: System-Migration
|
||||||
|
**Status**: ✅ Vollständig
|
||||||
|
|
||||||
398
MQTT_MIGRATION_GUIDE.md
Normal file
398
MQTT_MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
# MQTT Migration Guide - Client Implementation
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt die Änderungen am MQTT-Messaging-System und wie mobile Clients (Flutter/Dart) auf den neuen Messaging-Layer umgestellt werden müssen.
|
||||||
|
|
||||||
|
## Änderungen am Server
|
||||||
|
|
||||||
|
### 1. Neue MQTT-Broker-Konfiguration
|
||||||
|
|
||||||
|
**Wichtig: Der MQTT-Broker-Port hat sich geändert!**
|
||||||
|
|
||||||
|
```
|
||||||
|
Alter Port: 1883
|
||||||
|
Neuer Port: 42099
|
||||||
|
Broker: mqtt-2.assecutor.de:42099
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Server-Konfiguration
|
||||||
|
|
||||||
|
Die Server-Konfiguration wurde von der alten `app.mqtt.*` Konfiguration auf das neue Plugin-System umgestellt:
|
||||||
|
|
||||||
|
**Alte Konfiguration (nicht mehr verwendet):**
|
||||||
|
```properties
|
||||||
|
app.mqtt.enabled=true
|
||||||
|
app.mqtt.broker-uri=mqtt://mqtt-2.assecutor.de
|
||||||
|
app.mqtt.client-id=server-${random.uuid}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Neue Konfiguration:**
|
||||||
|
```properties
|
||||||
|
# Messaging Plugin Configuration
|
||||||
|
app.messaging.plugin.type=mqtt
|
||||||
|
app.messaging.plugin.mqtt.broker.host=mqtt-2.assecutor.de
|
||||||
|
app.messaging.plugin.mqtt.broker.port=42099
|
||||||
|
app.messaging.plugin.mqtt.username=app
|
||||||
|
app.messaging.plugin.mqtt.password=apppwd
|
||||||
|
app.messaging.plugin.mqtt.client.id=votianlt-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verbesserte Fehlerbehandlung
|
||||||
|
|
||||||
|
Der Server hat jetzt:
|
||||||
|
- Erhöhten Connection-Timeout (60 Sekunden statt 30)
|
||||||
|
- Detaillierte Fehlerdiagnose für Verbindungsprobleme
|
||||||
|
- Automatische Wiederverbindung mit exponentieller Backoff-Strategie
|
||||||
|
|
||||||
|
## Client-Anpassungen erforderlich
|
||||||
|
|
||||||
|
### 1. MQTT-Broker-Verbindung aktualisieren
|
||||||
|
|
||||||
|
**Flutter/Dart Beispiel:**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:mqtt_client/mqtt_client.dart';
|
||||||
|
import 'package:mqtt_client/mqtt_server_client.dart';
|
||||||
|
|
||||||
|
class MqttService {
|
||||||
|
// WICHTIG: Neuer Port!
|
||||||
|
static const String BROKER_HOST = 'mqtt-2.assecutor.de';
|
||||||
|
static const int BROKER_PORT = 42099; // Geändert von 1883
|
||||||
|
|
||||||
|
static const String USERNAME = 'app';
|
||||||
|
static const String PASSWORD = 'apppwd';
|
||||||
|
|
||||||
|
late MqttServerClient client;
|
||||||
|
|
||||||
|
Future<void> connect(String clientId) async {
|
||||||
|
client = MqttServerClient.withPort(
|
||||||
|
BROKER_HOST,
|
||||||
|
clientId,
|
||||||
|
BROKER_PORT, // Neuer Port
|
||||||
|
);
|
||||||
|
|
||||||
|
client.logging(on: true);
|
||||||
|
client.keepAlivePeriod = 60;
|
||||||
|
client.connectTimeoutPeriod = 60000; // 60 Sekunden
|
||||||
|
client.autoReconnect = true;
|
||||||
|
|
||||||
|
// Authentifizierung
|
||||||
|
client.setProtocolV311();
|
||||||
|
|
||||||
|
final connMessage = MqttConnectMessage()
|
||||||
|
.withClientIdentifier(clientId)
|
||||||
|
.authenticateAs(USERNAME, PASSWORD)
|
||||||
|
.withWillQos(MqttQos.atLeastOnce)
|
||||||
|
.startClean()
|
||||||
|
.keepAliveFor(60);
|
||||||
|
|
||||||
|
client.connectionMessage = connMessage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
print('MQTT Connected successfully');
|
||||||
|
} catch (e) {
|
||||||
|
print('MQTT Connection failed: $e');
|
||||||
|
client.disconnect();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Topic-Struktur (unverändert)
|
||||||
|
|
||||||
|
Die Topic-Struktur bleibt gleich:
|
||||||
|
|
||||||
|
**Client → Server:**
|
||||||
|
```
|
||||||
|
/server/{clientId}/{messageType}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client:**
|
||||||
|
```
|
||||||
|
/client/{clientId}/{messageType}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Acknowledgments:**
|
||||||
|
```
|
||||||
|
Client → Server: /ack/server/{messageId}
|
||||||
|
Server → Client: /ack/client/{clientId}/{messageId}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Message Envelope Format
|
||||||
|
|
||||||
|
Der Server verwendet jetzt ein Message-Envelope-Format für alle Nachrichten:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"messageId": "uuid-v4",
|
||||||
|
"timestamp": "2025-10-22T10:30:00Z",
|
||||||
|
"topic": "/client/app-user-123/jobs/assigned",
|
||||||
|
"payload": {
|
||||||
|
// Ihre eigentliche Nachricht
|
||||||
|
},
|
||||||
|
"requiresAck": true,
|
||||||
|
"retryCount": 0,
|
||||||
|
"expiresAt": "2025-10-23T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Client-Implementierung:**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MessageEnvelope {
|
||||||
|
final String messageId;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final String topic;
|
||||||
|
final Map<String, dynamic> payload;
|
||||||
|
final bool requiresAck;
|
||||||
|
final int retryCount;
|
||||||
|
final DateTime? expiresAt;
|
||||||
|
|
||||||
|
MessageEnvelope({
|
||||||
|
required this.messageId,
|
||||||
|
required this.timestamp,
|
||||||
|
required this.topic,
|
||||||
|
required this.payload,
|
||||||
|
this.requiresAck = true,
|
||||||
|
this.retryCount = 0,
|
||||||
|
this.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MessageEnvelope.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MessageEnvelope(
|
||||||
|
messageId: json['messageId'],
|
||||||
|
timestamp: DateTime.parse(json['timestamp']),
|
||||||
|
topic: json['topic'],
|
||||||
|
payload: json['payload'],
|
||||||
|
requiresAck: json['requiresAck'] ?? true,
|
||||||
|
retryCount: json['retryCount'] ?? 0,
|
||||||
|
expiresAt: json['expiresAt'] != null
|
||||||
|
? DateTime.parse(json['expiresAt'])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'messageId': messageId,
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
'topic': topic,
|
||||||
|
'payload': payload,
|
||||||
|
'requiresAck': requiresAck,
|
||||||
|
'retryCount': retryCount,
|
||||||
|
'expiresAt': expiresAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Acknowledgment-Handling
|
||||||
|
|
||||||
|
Wenn eine Nachricht mit `requiresAck: true` empfangen wird, muss der Client eine Bestätigung senden:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class AcknowledgmentMessage {
|
||||||
|
final String messageId;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final String status; // "SUCCESS" oder "FAILED"
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
AcknowledgmentMessage({
|
||||||
|
required this.messageId,
|
||||||
|
required this.timestamp,
|
||||||
|
required this.status,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'messageId': messageId,
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
'status': status,
|
||||||
|
if (errorMessage != null) 'errorMessage': errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verwendung:
|
||||||
|
void sendAcknowledgment(String messageId, bool success, [String? error]) {
|
||||||
|
final ack = AcknowledgmentMessage(
|
||||||
|
messageId: messageId,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
status: success ? 'SUCCESS' : 'FAILED',
|
||||||
|
errorMessage: error,
|
||||||
|
);
|
||||||
|
|
||||||
|
final topic = '/ack/server/$messageId';
|
||||||
|
final payload = jsonEncode(ack.toJson());
|
||||||
|
|
||||||
|
final builder = MqttClientPayloadBuilder();
|
||||||
|
builder.addString(payload);
|
||||||
|
|
||||||
|
client.publishMessage(
|
||||||
|
topic,
|
||||||
|
MqttQos.exactlyOnce,
|
||||||
|
builder.payload!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Nachrichten empfangen und verarbeiten
|
||||||
|
|
||||||
|
```dart
|
||||||
|
void setupMessageHandlers() {
|
||||||
|
client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> messages) {
|
||||||
|
for (var message in messages) {
|
||||||
|
final topic = message.topic;
|
||||||
|
final payload = MqttPublishPayload.bytesToStringAsString(
|
||||||
|
(message.payload as MqttPublishMessage).payload.message,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final json = jsonDecode(payload);
|
||||||
|
|
||||||
|
// Prüfen, ob es ein Envelope ist
|
||||||
|
if (json.containsKey('messageId') && json.containsKey('payload')) {
|
||||||
|
final envelope = MessageEnvelope.fromJson(json);
|
||||||
|
|
||||||
|
// Nachricht verarbeiten
|
||||||
|
handleMessage(envelope);
|
||||||
|
|
||||||
|
// ACK senden, wenn erforderlich
|
||||||
|
if (envelope.requiresAck) {
|
||||||
|
sendAcknowledgment(envelope.messageId, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy-Nachricht ohne Envelope
|
||||||
|
handleLegacyMessage(topic, json);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error processing message: $e');
|
||||||
|
// Bei Envelope-Nachrichten: Fehler-ACK senden
|
||||||
|
if (json.containsKey('messageId')) {
|
||||||
|
sendAcknowledgment(json['messageId'], false, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Nachrichten senden
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> sendMessage(
|
||||||
|
String messageType,
|
||||||
|
Map<String, dynamic> payload,
|
||||||
|
{bool requiresAck = true}
|
||||||
|
) async {
|
||||||
|
final envelope = MessageEnvelope(
|
||||||
|
messageId: Uuid().v4(),
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
topic: '/server/$clientId/$messageType',
|
||||||
|
payload: payload,
|
||||||
|
requiresAck: requiresAck,
|
||||||
|
);
|
||||||
|
|
||||||
|
final topic = envelope.topic;
|
||||||
|
final message = jsonEncode(envelope.toJson());
|
||||||
|
|
||||||
|
final builder = MqttClientPayloadBuilder();
|
||||||
|
builder.addString(message);
|
||||||
|
|
||||||
|
client.publishMessage(
|
||||||
|
topic,
|
||||||
|
MqttQos.exactlyOnce,
|
||||||
|
builder.payload!,
|
||||||
|
);
|
||||||
|
|
||||||
|
print('Message sent: $messageType');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Checklist für Clients
|
||||||
|
|
||||||
|
- [ ] **MQTT-Broker-Port auf 42099 ändern**
|
||||||
|
- [ ] **Connection-Timeout auf mindestens 60 Sekunden erhöhen**
|
||||||
|
- [ ] **Keep-Alive auf 60 Sekunden setzen**
|
||||||
|
- [ ] **Authentifizierung hinzufügen** (username: `app`, password: `apppwd`)
|
||||||
|
- [ ] **MessageEnvelope-Klasse implementieren**
|
||||||
|
- [ ] **AcknowledgmentMessage-Klasse implementieren**
|
||||||
|
- [ ] **Envelope-basierte Nachrichtenverarbeitung implementieren**
|
||||||
|
- [ ] **ACK-Handling für eingehende Nachrichten implementieren**
|
||||||
|
- [ ] **Ausgehende Nachrichten in Envelopes verpacken**
|
||||||
|
- [ ] **Fehlerbehandlung für abgelaufene Nachrichten implementieren**
|
||||||
|
- [ ] **Retry-Logik für fehlgeschlagene Nachrichten implementieren**
|
||||||
|
|
||||||
|
## Abwärtskompatibilität
|
||||||
|
|
||||||
|
Der Server unterstützt derzeit noch Legacy-Nachrichten ohne Envelope-Format, aber es wird empfohlen, so schnell wie möglich auf das neue Format umzustellen.
|
||||||
|
|
||||||
|
**Legacy-Format (wird noch unterstützt):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "123",
|
||||||
|
"status": "completed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Neues Format (empfohlen):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"messageId": "uuid",
|
||||||
|
"timestamp": "2025-10-22T10:30:00Z",
|
||||||
|
"topic": "/server/client-123/task_completed",
|
||||||
|
"payload": {
|
||||||
|
"taskId": "123",
|
||||||
|
"status": "completed"
|
||||||
|
},
|
||||||
|
"requiresAck": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vorteile des neuen Systems
|
||||||
|
|
||||||
|
1. **Zuverlässige Zustellung**: ACK-basiertes System mit automatischen Wiederholungen
|
||||||
|
2. **Nachrichtenverfolgung**: Jede Nachricht hat eine eindeutige ID
|
||||||
|
3. **Ablaufverwaltung**: Nachrichten können ablaufen und werden automatisch bereinigt
|
||||||
|
4. **Bessere Fehlerbehandlung**: Detaillierte Fehlerinformationen
|
||||||
|
5. **Monitoring**: Vollständige Nachrichtenverfolgung im System
|
||||||
|
|
||||||
|
## Testen der Verbindung
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> testConnection() async {
|
||||||
|
try {
|
||||||
|
final mqttService = MqttService();
|
||||||
|
await mqttService.connect('test-client-${Uuid().v4()}');
|
||||||
|
|
||||||
|
// Test-Nachricht senden
|
||||||
|
await mqttService.sendMessage(
|
||||||
|
'test',
|
||||||
|
{'message': 'Hello from client'},
|
||||||
|
);
|
||||||
|
|
||||||
|
print('Connection test successful!');
|
||||||
|
} catch (e) {
|
||||||
|
print('Connection test failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support und Fragen
|
||||||
|
|
||||||
|
Bei Fragen oder Problemen während der Migration:
|
||||||
|
1. Prüfen Sie die Logs auf beiden Seiten (Client und Server)
|
||||||
|
2. Stellen Sie sicher, dass der Port 42099 erreichbar ist
|
||||||
|
3. Überprüfen Sie die Authentifizierungsdaten
|
||||||
|
4. Testen Sie die Verbindung mit einem MQTT-Client-Tool (z.B. MQTT Explorer)
|
||||||
|
|
||||||
|
## Weitere Dokumentation
|
||||||
|
|
||||||
|
- `MESSAGING_LAYER.md` - Detaillierte Architektur des Messaging-Systems
|
||||||
|
- `MQTT_README.md` - MQTT-API-Dokumentation
|
||||||
|
- `CLAUDE.md` - Allgemeine Systemarchitektur
|
||||||
|
|
||||||
367
MQTT_QUICK_REFERENCE.md
Normal file
367
MQTT_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
# MQTT Quick Reference - Client Migration
|
||||||
|
|
||||||
|
## 🚨 Kritische Änderungen
|
||||||
|
|
||||||
|
### Port-Änderung (WICHTIG!)
|
||||||
|
```
|
||||||
|
Alt: mqtt-2.assecutor.de:1883
|
||||||
|
Neu: mqtt-2.assecutor.de:42099
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentifizierung (NEU!)
|
||||||
|
```
|
||||||
|
Username: app
|
||||||
|
Password: apppwd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeout-Erhöhung
|
||||||
|
```
|
||||||
|
Connection Timeout: 60 Sekunden (vorher 30s)
|
||||||
|
Keep-Alive: 60 Sekunden (vorher 30s)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Flutter/Dart Code-Snippets
|
||||||
|
|
||||||
|
### 1. Verbindung herstellen
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:mqtt_client/mqtt_client.dart';
|
||||||
|
import 'package:mqtt_client/mqtt_server_client.dart';
|
||||||
|
|
||||||
|
Future<MqttServerClient> connectToMqtt(String clientId) async {
|
||||||
|
final client = MqttServerClient.withPort(
|
||||||
|
'mqtt-2.assecutor.de',
|
||||||
|
clientId,
|
||||||
|
42099, // NEUER PORT!
|
||||||
|
);
|
||||||
|
|
||||||
|
client.keepAlivePeriod = 60;
|
||||||
|
client.connectTimeoutPeriod = 60000;
|
||||||
|
client.autoReconnect = true;
|
||||||
|
client.setProtocolV311();
|
||||||
|
|
||||||
|
final connMessage = MqttConnectMessage()
|
||||||
|
.withClientIdentifier(clientId)
|
||||||
|
.authenticateAs('app', 'apppwd') // NEU: Authentifizierung
|
||||||
|
.withWillQos(MqttQos.atLeastOnce)
|
||||||
|
.startClean()
|
||||||
|
.keepAliveFor(60);
|
||||||
|
|
||||||
|
client.connectionMessage = connMessage;
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Message Envelope (NEU!)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MessageEnvelope {
|
||||||
|
final String messageId;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final String topic;
|
||||||
|
final Map<String, dynamic> payload;
|
||||||
|
final bool requiresAck;
|
||||||
|
final int retryCount;
|
||||||
|
final DateTime? expiresAt;
|
||||||
|
|
||||||
|
MessageEnvelope({
|
||||||
|
required this.messageId,
|
||||||
|
required this.timestamp,
|
||||||
|
required this.topic,
|
||||||
|
required this.payload,
|
||||||
|
this.requiresAck = true,
|
||||||
|
this.retryCount = 0,
|
||||||
|
this.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MessageEnvelope.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MessageEnvelope(
|
||||||
|
messageId: json['messageId'],
|
||||||
|
timestamp: DateTime.parse(json['timestamp']),
|
||||||
|
topic: json['topic'],
|
||||||
|
payload: json['payload'],
|
||||||
|
requiresAck: json['requiresAck'] ?? true,
|
||||||
|
retryCount: json['retryCount'] ?? 0,
|
||||||
|
expiresAt: json['expiresAt'] != null
|
||||||
|
? DateTime.parse(json['expiresAt'])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'messageId': messageId,
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
'topic': topic,
|
||||||
|
'payload': payload,
|
||||||
|
'requiresAck': requiresAck,
|
||||||
|
'retryCount': retryCount,
|
||||||
|
if (expiresAt != null) 'expiresAt': expiresAt!.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Nachricht senden
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
Future<void> sendMessage(
|
||||||
|
MqttServerClient client,
|
||||||
|
String clientId,
|
||||||
|
String messageType,
|
||||||
|
Map<String, dynamic> payload,
|
||||||
|
) async {
|
||||||
|
final envelope = MessageEnvelope(
|
||||||
|
messageId: Uuid().v4(),
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
topic: '/server/$clientId/$messageType',
|
||||||
|
payload: payload,
|
||||||
|
requiresAck: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final builder = MqttClientPayloadBuilder();
|
||||||
|
builder.addString(jsonEncode(envelope.toJson()));
|
||||||
|
|
||||||
|
client.publishMessage(
|
||||||
|
envelope.topic,
|
||||||
|
MqttQos.exactlyOnce,
|
||||||
|
builder.payload!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beispiel-Verwendung:
|
||||||
|
await sendMessage(
|
||||||
|
client,
|
||||||
|
'app-user-123',
|
||||||
|
'task_completed',
|
||||||
|
{
|
||||||
|
'taskId': '456',
|
||||||
|
'status': 'completed',
|
||||||
|
'completedAt': DateTime.now().toIso8601String(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Nachricht empfangen
|
||||||
|
|
||||||
|
```dart
|
||||||
|
void setupMessageListener(MqttServerClient client, String clientId) {
|
||||||
|
// Subscribe zu allen relevanten Topics
|
||||||
|
client.subscribe('/client/$clientId/#', MqttQos.exactlyOnce);
|
||||||
|
client.subscribe('/ack/client/$clientId/#', MqttQos.exactlyOnce);
|
||||||
|
|
||||||
|
client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> messages) {
|
||||||
|
for (var message in messages) {
|
||||||
|
final topic = message.topic;
|
||||||
|
final payloadString = MqttPublishPayload.bytesToStringAsString(
|
||||||
|
(message.payload as MqttPublishMessage).payload.message,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final json = jsonDecode(payloadString);
|
||||||
|
|
||||||
|
// Prüfen ob Envelope-Format
|
||||||
|
if (json.containsKey('messageId') && json.containsKey('payload')) {
|
||||||
|
final envelope = MessageEnvelope.fromJson(json);
|
||||||
|
|
||||||
|
// Nachricht verarbeiten
|
||||||
|
handleEnvelopeMessage(envelope);
|
||||||
|
|
||||||
|
// ACK senden wenn erforderlich
|
||||||
|
if (envelope.requiresAck) {
|
||||||
|
sendAck(client, envelope.messageId, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy-Format
|
||||||
|
handleLegacyMessage(topic, json);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error processing message: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Acknowledgment senden (NEU!)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class AcknowledgmentMessage {
|
||||||
|
final String messageId;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final String status;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
AcknowledgmentMessage({
|
||||||
|
required this.messageId,
|
||||||
|
required this.timestamp,
|
||||||
|
required this.status,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'messageId': messageId,
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
'status': status,
|
||||||
|
if (errorMessage != null) 'errorMessage': errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendAck(
|
||||||
|
MqttServerClient client,
|
||||||
|
String messageId,
|
||||||
|
bool success,
|
||||||
|
[String? errorMessage]
|
||||||
|
) {
|
||||||
|
final ack = AcknowledgmentMessage(
|
||||||
|
messageId: messageId,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
status: success ? 'SUCCESS' : 'FAILED',
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
final topic = '/ack/server/$messageId';
|
||||||
|
final builder = MqttClientPayloadBuilder();
|
||||||
|
builder.addString(jsonEncode(ack.toJson()));
|
||||||
|
|
||||||
|
client.publishMessage(
|
||||||
|
topic,
|
||||||
|
MqttQos.exactlyOnce,
|
||||||
|
builder.payload!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Topic-Struktur (unverändert)
|
||||||
|
|
||||||
|
### Client → Server
|
||||||
|
```
|
||||||
|
/server/{clientId}/task_completed
|
||||||
|
/server/{clientId}/login
|
||||||
|
/server/{clientId}/message
|
||||||
|
/server/{clientId}/jobs/assigned
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server → Client
|
||||||
|
```
|
||||||
|
/client/{clientId}/jobs/assigned
|
||||||
|
/client/{clientId}/message
|
||||||
|
/client/{clientId}/task_update
|
||||||
|
```
|
||||||
|
|
||||||
|
### Acknowledgments
|
||||||
|
```
|
||||||
|
Client → Server: /ack/server/{messageId}
|
||||||
|
Server → Client: /ack/client/{clientId}/{messageId}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Debugging
|
||||||
|
|
||||||
|
### Verbindung testen
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> testConnection() async {
|
||||||
|
try {
|
||||||
|
final client = await connectToMqtt('test-${Uuid().v4()}');
|
||||||
|
|
||||||
|
if (client.connectionStatus!.state == MqttConnectionState.connected) {
|
||||||
|
print('✅ Connection successful!');
|
||||||
|
|
||||||
|
// Test-Nachricht senden
|
||||||
|
await sendMessage(
|
||||||
|
client,
|
||||||
|
'test-client',
|
||||||
|
'test',
|
||||||
|
{'message': 'Hello from client'},
|
||||||
|
);
|
||||||
|
|
||||||
|
print('✅ Test message sent!');
|
||||||
|
} else {
|
||||||
|
print('❌ Connection failed: ${client.connectionStatus}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Häufige Fehler
|
||||||
|
|
||||||
|
#### 1. Connection Timeout
|
||||||
|
```
|
||||||
|
Fehler: TimeoutException after 60 seconds
|
||||||
|
Lösung:
|
||||||
|
- Prüfen Sie, ob Port 42099 erreichbar ist
|
||||||
|
- Firewall-Einstellungen überprüfen
|
||||||
|
- Netzwerkverbindung testen
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Authentication Failed
|
||||||
|
```
|
||||||
|
Fehler: Connection refused
|
||||||
|
Lösung:
|
||||||
|
- Username: 'app'
|
||||||
|
- Password: 'apppwd'
|
||||||
|
- Stellen Sie sicher, dass authenticateAs() aufgerufen wird
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Message not received
|
||||||
|
```
|
||||||
|
Fehler: Keine Nachrichten empfangen
|
||||||
|
Lösung:
|
||||||
|
- Subscription überprüfen: client.subscribe('/client/$clientId/#', ...)
|
||||||
|
- Listener registriert: client.updates!.listen(...)
|
||||||
|
- Topic-Format korrekt: /client/{clientId}/{messageType}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Dependencies
|
||||||
|
|
||||||
|
### pubspec.yaml
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
mqtt_client: ^10.2.0
|
||||||
|
uuid: ^4.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Migration Checklist
|
||||||
|
|
||||||
|
- [ ] Port auf 42099 geändert
|
||||||
|
- [ ] Authentifizierung hinzugefügt (app/apppwd)
|
||||||
|
- [ ] Connection-Timeout auf 60s erhöht
|
||||||
|
- [ ] Keep-Alive auf 60s gesetzt
|
||||||
|
- [ ] MessageEnvelope-Klasse implementiert
|
||||||
|
- [ ] AcknowledgmentMessage-Klasse implementiert
|
||||||
|
- [ ] Envelope-basiertes Senden implementiert
|
||||||
|
- [ ] Envelope-basiertes Empfangen implementiert
|
||||||
|
- [ ] ACK-Handling implementiert
|
||||||
|
- [ ] Fehlerbehandlung für ACKs implementiert
|
||||||
|
- [ ] Verbindung getestet
|
||||||
|
- [ ] Nachrichtenversand getestet
|
||||||
|
- [ ] Nachrichtenempfang getestet
|
||||||
|
|
||||||
|
## 🔗 Weitere Dokumentation
|
||||||
|
|
||||||
|
- **Detaillierte Migration**: `MQTT_MIGRATION_GUIDE.md`
|
||||||
|
- **Changelog**: `CHANGELOG_MQTT.md`
|
||||||
|
- **Architektur**: `MESSAGING_LAYER.md`
|
||||||
|
- **API-Referenz**: `MQTT_README.md`
|
||||||
|
|
||||||
|
## 💡 Best Practices
|
||||||
|
|
||||||
|
1. **Eindeutige Client-IDs**: Verwenden Sie stabile, geräte-spezifische IDs
|
||||||
|
2. **Fehlerbehandlung**: Implementieren Sie Retry-Logik für fehlgeschlagene Verbindungen
|
||||||
|
3. **ACK-Timeout**: Warten Sie maximal 30 Sekunden auf ACKs
|
||||||
|
4. **Message-Expiry**: Prüfen Sie `expiresAt` vor der Verarbeitung
|
||||||
|
5. **Logging**: Loggen Sie alle Verbindungsereignisse für Debugging
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
Bei Problemen:
|
||||||
|
1. Logs auf Client- und Server-Seite prüfen
|
||||||
|
2. Netzwerk-Konnektivität testen (Port 42099)
|
||||||
|
3. MQTT-Client-Tool verwenden (z.B. MQTT Explorer)
|
||||||
|
4. Dokumentation konsultieren
|
||||||
|
|
||||||
@@ -2,15 +2,28 @@
|
|||||||
|
|
||||||
This document describes how mobile/Flutter apps should communicate with the backend using MQTT. It replaces the previous STOMP/WebSocket communication.
|
This document describes how mobile/Flutter apps should communicate with the backend using MQTT. It replaces the previous STOMP/WebSocket communication.
|
||||||
|
|
||||||
Broker: tcp://192.168.180.26:1883 (MQTT v5)
|
## ⚠️ WICHTIG: Neue Konfiguration (Stand: 2025-10-22)
|
||||||
QoS: 2 (exactly once)
|
|
||||||
Retain: Enabled for critical topics (see below), otherwise not retained
|
|
||||||
Payloads: JSON (UTF‑8)
|
|
||||||
|
|
||||||
Connection
|
**Broker**: `mqtt-2.assecutor.de:42099` (MQTT v5)
|
||||||
- MQTT clientId: choose a stable, unique per-device id (e.g., app-<uuid>)
|
**Port**: `42099` (geändert von 1883!)
|
||||||
- Clean session: false (recommended for guaranteed delivery). The broker will queue QoS>0 messages while the app is offline.
|
**QoS**: 2 (exactly once)
|
||||||
- Authentication: currently none (adjust if needed)
|
**Retain**: Enabled for critical topics (see below), otherwise not retained
|
||||||
|
**Payloads**: JSON (UTF‑8)
|
||||||
|
|
||||||
|
### Connection Parameters
|
||||||
|
- **MQTT clientId**: choose a stable, unique per-device id (e.g., app-<uuid>)
|
||||||
|
- **Clean session**: false (recommended for guaranteed delivery). The broker will queue QoS>0 messages while the app is offline.
|
||||||
|
- **Authentication**: **REQUIRED** (neu!)
|
||||||
|
- Username: `app`
|
||||||
|
- Password: `apppwd`
|
||||||
|
- **Keep-Alive**: 60 seconds
|
||||||
|
- **Connection Timeout**: 60 seconds
|
||||||
|
|
||||||
|
### Migration Notice
|
||||||
|
📖 **Für die Migration auf das neue System siehe:**
|
||||||
|
- `MQTT_MIGRATION_GUIDE.md` - Detaillierte Migrationsanleitung
|
||||||
|
- `MQTT_QUICK_REFERENCE.md` - Schnellreferenz mit Code-Beispielen
|
||||||
|
- `CHANGELOG_MQTT.md` - Vollständige Liste aller Änderungen
|
||||||
|
|
||||||
Topic Naming (v1/*)
|
Topic Naming (v1/*)
|
||||||
- v1/app/<deviceId>/auth/login (App -> Server)
|
- v1/app/<deviceId>/auth/login (App -> Server)
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package de.assecutor.votianlt.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MQTT configuration placeholder.
|
|
||||||
*
|
|
||||||
* In environments where Spring Integration MQTT dependencies are not available,
|
|
||||||
* this class remains empty to allow the application to compile and run without
|
|
||||||
* MQTT wiring. The business code uses a no-op MqttPublisher that logs messages.
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class MqttConfig {
|
|
||||||
public static final String MQTT_BROKER_URI = "tcp://192.168.180.26:1883";
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package de.assecutor.votianlt.config;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@ConfigurationProperties(prefix = "app.mqtt")
|
|
||||||
public class MqttProperties {
|
|
||||||
/** Enable/disable MQTT subsystem */
|
|
||||||
private boolean enabled = true;
|
|
||||||
/** Broker URI, e.g. tcp://192.168.180.26:1883 */
|
|
||||||
private String brokerUri = "tcp://192.168.180.26:1883";
|
|
||||||
/** ClientId for the server */
|
|
||||||
private String clientId = "server";
|
|
||||||
/** Optional username */
|
|
||||||
private String username;
|
|
||||||
/** Optional password */
|
|
||||||
private String password;
|
|
||||||
/** MQTT v5 clean start flag */
|
|
||||||
private boolean cleanStart = false;
|
|
||||||
/** Session expiry interval in seconds (0 = expire immediately) */
|
|
||||||
private long sessionExpiryInterval = 24 * 60 * 60; // 1 day
|
|
||||||
/** Keep alive in seconds */
|
|
||||||
private int keepAlive = 30;
|
|
||||||
/** Max inflight messages */
|
|
||||||
private int maxInflight = 50;
|
|
||||||
/** Automatic reconnect */
|
|
||||||
private boolean automaticReconnect = true;
|
|
||||||
/** Default QoS to use for publishing */
|
|
||||||
private int defaultQos = 2;
|
|
||||||
/** Default retained flag for publishing */
|
|
||||||
private boolean defaultRetained = false;
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnabled(boolean enabled) {
|
|
||||||
this.enabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBrokerUri() {
|
|
||||||
return brokerUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBrokerUri(String brokerUri) {
|
|
||||||
this.brokerUri = brokerUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getClientId() {
|
|
||||||
return clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setClientId(String clientId) {
|
|
||||||
this.clientId = clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUsername() {
|
|
||||||
return username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUsername(String username) {
|
|
||||||
this.username = username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPassword(String password) {
|
|
||||||
this.password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCleanStart() {
|
|
||||||
return cleanStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCleanStart(boolean cleanStart) {
|
|
||||||
this.cleanStart = cleanStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getSessionExpiryInterval() {
|
|
||||||
return sessionExpiryInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionExpiryInterval(long sessionExpiryInterval) {
|
|
||||||
this.sessionExpiryInterval = sessionExpiryInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getKeepAlive() {
|
|
||||||
return keepAlive;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeepAlive(int keepAlive) {
|
|
||||||
this.keepAlive = keepAlive;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxInflight() {
|
|
||||||
return maxInflight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaxInflight(int maxInflight) {
|
|
||||||
this.maxInflight = maxInflight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAutomaticReconnect() {
|
|
||||||
return automaticReconnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAutomaticReconnect(boolean automaticReconnect) {
|
|
||||||
this.automaticReconnect = automaticReconnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDefaultQos() {
|
|
||||||
return defaultQos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultQos(int defaultQos) {
|
|
||||||
this.defaultQos = defaultQos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDefaultRetained() {
|
|
||||||
return defaultRetained;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultRetained(boolean defaultRetained) {
|
|
||||||
this.defaultRetained = defaultRetained;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.config;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.messaging.delivery.MessageDeliveryService;
|
||||||
|
import de.assecutor.votianlt.messaging.model.AcknowledgmentMessage;
|
||||||
|
import de.assecutor.votianlt.messaging.model.MessageEnvelope;
|
||||||
|
import de.assecutor.votianlt.messaging.plugin.*;
|
||||||
|
import de.assecutor.votianlt.messaging.plugin.mqtt.MqttMessagingPlugin;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the plugin-based messaging system.
|
||||||
|
* Initializes the selected plugin and sets up message routing.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
|
public class PluginMessagingConfig {
|
||||||
|
|
||||||
|
@Value("${app.messaging.plugin.type:mqtt}")
|
||||||
|
private String pluginType;
|
||||||
|
|
||||||
|
@Value("${app.messaging.plugin.mqtt.broker.host:mqtt-2.assecutor.de}")
|
||||||
|
private String mqttBrokerHost;
|
||||||
|
|
||||||
|
@Value("${app.messaging.plugin.mqtt.broker.port:1883}")
|
||||||
|
private int mqttBrokerPort;
|
||||||
|
|
||||||
|
@Value("${app.messaging.plugin.mqtt.username:app}")
|
||||||
|
private String mqttUsername;
|
||||||
|
|
||||||
|
@Value("${app.messaging.plugin.mqtt.password:apppwd}")
|
||||||
|
private String mqttPassword;
|
||||||
|
|
||||||
|
@Value("${app.messaging.plugin.mqtt.client.id:votianlt-server}")
|
||||||
|
private String mqttClientId;
|
||||||
|
|
||||||
|
private final PluginManager pluginManager;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public PluginMessagingConfig(PluginManager pluginManager) {
|
||||||
|
this.pluginManager = pluginManager;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
this.objectMapper.registerModule(new JavaTimeModule());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the messaging plugin after application startup.
|
||||||
|
* This method is called after all beans are created, so we can safely access MessageDeliveryService.
|
||||||
|
*/
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void initializePlugin(ApplicationReadyEvent event) {
|
||||||
|
log.info("[PluginMessagingConfig] Initializing messaging plugin: {}", pluginType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
MessagingPlugin plugin = createPlugin(pluginType);
|
||||||
|
PluginConfig config = createPluginConfig(pluginType);
|
||||||
|
|
||||||
|
// Get MessageDeliveryService from context (after all beans are created)
|
||||||
|
MessageDeliveryService deliveryService = event.getApplicationContext().getBean(MessageDeliveryService.class);
|
||||||
|
|
||||||
|
// Set up a listener to subscribe when connected
|
||||||
|
log.info("[PluginMessagingConfig] Adding state listener");
|
||||||
|
pluginManager.addStateListener(stateEvent -> {
|
||||||
|
log.info("[PluginMessagingConfig] State event received: state={}, isConnected={}",
|
||||||
|
stateEvent.getState(), stateEvent.isConnected());
|
||||||
|
if (stateEvent.isConnected()) {
|
||||||
|
log.info("[PluginMessagingConfig] Plugin connected, setting up subscriptions");
|
||||||
|
try {
|
||||||
|
setupSubscriptions(deliveryService);
|
||||||
|
log.info("[PluginMessagingConfig] Subscriptions setup completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[PluginMessagingConfig] Error setting up subscriptions: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("[PluginMessagingConfig] Plugin not yet connected, waiting...");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.info("[PluginMessagingConfig] State listener added");
|
||||||
|
|
||||||
|
// Activate plugin (this will trigger connection and eventually the listener above)
|
||||||
|
pluginManager.activatePlugin(plugin, config);
|
||||||
|
|
||||||
|
log.info("[PluginMessagingConfig] Plugin activation initiated, subscriptions will be set up when connected");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[PluginMessagingConfig] Failed to initialize plugin: {}", e.getMessage(), e);
|
||||||
|
throw new RuntimeException("Failed to initialize messaging plugin", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a plugin instance based on the plugin type.
|
||||||
|
*/
|
||||||
|
private MessagingPlugin createPlugin(String type) {
|
||||||
|
return switch (type.toLowerCase()) {
|
||||||
|
case "mqtt" -> new MqttMessagingPlugin();
|
||||||
|
// Add more plugin types here in the future
|
||||||
|
// case "websocket" -> new WebSocketMessagingPlugin();
|
||||||
|
// case "grpc" -> new GrpcMessagingPlugin();
|
||||||
|
default -> throw new IllegalArgumentException("Unknown plugin type: " + type);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create plugin configuration based on the plugin type.
|
||||||
|
*/
|
||||||
|
private PluginConfig createPluginConfig(String type) {
|
||||||
|
PluginConfig config = new PluginConfig();
|
||||||
|
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case "mqtt" -> {
|
||||||
|
config.setProperty("broker.host", mqttBrokerHost);
|
||||||
|
config.setProperty("broker.port", mqttBrokerPort);
|
||||||
|
config.setProperty("username", mqttUsername);
|
||||||
|
config.setProperty("password", mqttPassword);
|
||||||
|
config.setProperty("client.id", mqttClientId);
|
||||||
|
config.setProperty("auto.reconnect", true);
|
||||||
|
config.setProperty("clean.start", true);
|
||||||
|
}
|
||||||
|
// Add more plugin configurations here
|
||||||
|
default -> throw new IllegalArgumentException("Unknown plugin type: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup message subscriptions using the new plugin API.
|
||||||
|
*/
|
||||||
|
private void setupSubscriptions(MessageDeliveryService deliveryService) {
|
||||||
|
log.info("[PluginMessagingConfig] Setting up message subscriptions");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Register ACK handler
|
||||||
|
pluginManager.registerAckHandler((messageId, payload) -> {
|
||||||
|
try {
|
||||||
|
String json = new String(payload, StandardCharsets.UTF_8);
|
||||||
|
AcknowledgmentMessage ack = objectMapper.readValue(json, AcknowledgmentMessage.class);
|
||||||
|
deliveryService.handleAcknowledgment(ack);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[PluginMessagingConfig] Error handling ACK message: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register message handlers for different message types
|
||||||
|
String[] messageTypes = {
|
||||||
|
"task_completed",
|
||||||
|
"jobs/assigned",
|
||||||
|
"message",
|
||||||
|
"login"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String messageType : messageTypes) {
|
||||||
|
pluginManager.registerMessageHandler(messageType, (clientId, payload) ->
|
||||||
|
handleEnvelopedMessage(clientId, payload, deliveryService));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[PluginMessagingConfig] Message subscriptions initialized");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[PluginMessagingConfig] Error setting up subscriptions: {}", e.getMessage(), e);
|
||||||
|
throw new RuntimeException("Failed to setup subscriptions", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming enveloped message.
|
||||||
|
*/
|
||||||
|
private void handleEnvelopedMessage(String clientId, byte[] payload, MessageDeliveryService deliveryService) {
|
||||||
|
try {
|
||||||
|
String json = new String(payload, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Try to parse as envelope first
|
||||||
|
try {
|
||||||
|
MessageEnvelope envelope = objectMapper.readValue(json, MessageEnvelope.class);
|
||||||
|
deliveryService.handleIncomingMessage(envelope);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If not an envelope, it might be a legacy message - log and skip
|
||||||
|
log.debug("[PluginMessagingConfig] Received non-enveloped message from client {}, skipping", clientId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[PluginMessagingConfig] Error handling enveloped message from client {}: {}", clientId, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.delivery;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import de.assecutor.votianlt.controller.MessageController;
|
||||||
|
import de.assecutor.votianlt.dto.AppLoginRequest;
|
||||||
|
import de.assecutor.votianlt.messaging.model.MessageEnvelope;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles acknowledgments and routes incoming messages to application layer.
|
||||||
|
* Acts as a bridge between the messaging layer and the application logic.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class AcknowledgmentHandler {
|
||||||
|
|
||||||
|
private final MessageController messageController;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public AcknowledgmentHandler(@Lazy MessageController messageController) {
|
||||||
|
this.messageController = messageController;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
this.objectMapper.registerModule(new JavaTimeModule());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route incoming message envelope to appropriate application handler.
|
||||||
|
* Unwraps the envelope and delegates to MessageController.
|
||||||
|
*/
|
||||||
|
public void routeIncomingMessage(MessageEnvelope envelope) {
|
||||||
|
try {
|
||||||
|
String topic = envelope.getTopic();
|
||||||
|
Object payload = envelope.getPayload();
|
||||||
|
|
||||||
|
log.debug("[AckHandler] Routing message {} on topic {}", envelope.getMessageId(), topic);
|
||||||
|
|
||||||
|
// Convert payload to Map for routing
|
||||||
|
Map<String, Object> payloadMap = objectMapper.convertValue(payload,
|
||||||
|
new TypeReference<Map<String, Object>>() {});
|
||||||
|
|
||||||
|
// Route based on topic pattern
|
||||||
|
if (topic.matches("/server/.+/task_completed")) {
|
||||||
|
handleTaskCompleted(payloadMap);
|
||||||
|
} else if (topic.matches("/server/.+/jobs/assigned")) {
|
||||||
|
handleJobsAssigned(topic, payloadMap);
|
||||||
|
} else if (topic.equals("/server/login")) {
|
||||||
|
handleLogin(payloadMap);
|
||||||
|
} else if (topic.matches("/server/.+/message")) {
|
||||||
|
handleIncomingMessage(topic, payloadMap);
|
||||||
|
} else {
|
||||||
|
log.debug("[AckHandler] No route for topic {}", topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[AckHandler] Error routing message {}: {}",
|
||||||
|
envelope.getMessageId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle task completion message
|
||||||
|
*/
|
||||||
|
private void handleTaskCompleted(Map<String, Object> payload) {
|
||||||
|
try {
|
||||||
|
Object tt = payload.get("taskType");
|
||||||
|
String taskType = tt != null ? tt.toString() : null;
|
||||||
|
messageController.handleTaskCompleted(payload, taskType);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[AckHandler] Error handling task_completed: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle jobs assigned request
|
||||||
|
*/
|
||||||
|
private void handleJobsAssigned(String topic, Map<String, Object> payload) {
|
||||||
|
try {
|
||||||
|
// Extract clientId from topic: /server/{clientId}/jobs/assigned
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
String clientId = parts.length > 2 ? parts[2] : null;
|
||||||
|
if (clientId != null && !clientId.isBlank()) {
|
||||||
|
payload.put("clientId", clientId);
|
||||||
|
} else {
|
||||||
|
log.warn("[AckHandler] Couldn't extract clientId from topic {} for jobs/assigned", topic);
|
||||||
|
}
|
||||||
|
messageController.handleGetAssignedJobs(payload);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[AckHandler] Error handling jobs/assigned: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle login request
|
||||||
|
*/
|
||||||
|
private void handleLogin(Map<String, Object> payload) {
|
||||||
|
try {
|
||||||
|
AppLoginRequest req = objectMapper.convertValue(payload, AppLoginRequest.class);
|
||||||
|
messageController.handleAppLogin(req);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[AckHandler] Error handling login: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming chat message
|
||||||
|
*/
|
||||||
|
private void handleIncomingMessage(String topic, Map<String, Object> payload) {
|
||||||
|
try {
|
||||||
|
// Extract clientId from topic: /server/{clientId}/message
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
String clientId = parts.length > 2 ? parts[2] : null;
|
||||||
|
if (clientId != null && !clientId.isBlank()) {
|
||||||
|
payload.put("clientId", clientId);
|
||||||
|
} else {
|
||||||
|
log.warn("[AckHandler] Couldn't extract clientId from topic {} for message", topic);
|
||||||
|
}
|
||||||
|
messageController.handleIncomingMessage(payload);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[AckHandler] Error handling incoming message: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.delivery;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for message delivery service.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "app.messaging.delivery")
|
||||||
|
@Data
|
||||||
|
public class DeliveryConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of retry attempts for failed deliveries
|
||||||
|
*/
|
||||||
|
private int maxRetries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial delay before first retry
|
||||||
|
*/
|
||||||
|
private Duration retryInitialDelay = Duration.ofSeconds(5);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum delay between retries
|
||||||
|
*/
|
||||||
|
private Duration retryMaxDelay = Duration.ofMinutes(5);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backoff multiplier for exponential backoff
|
||||||
|
*/
|
||||||
|
private double retryBackoffMultiplier = 2.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout for waiting for acknowledgment
|
||||||
|
*/
|
||||||
|
private Duration ackTimeout = Duration.ofSeconds(30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default message expiry duration
|
||||||
|
*/
|
||||||
|
private Duration messageExpiry = Duration.ofHours(24);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interval for cleanup task (in minutes)
|
||||||
|
*/
|
||||||
|
private int cleanupIntervalMinutes = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interval for retry task (in seconds)
|
||||||
|
*/
|
||||||
|
private int retryIntervalSeconds = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retention period for acknowledged deliveries (in days)
|
||||||
|
*/
|
||||||
|
private int acknowledgedRetentionDays = 7;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.delivery;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.messaging.model.*;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for reliable message delivery with acknowledgment tracking.
|
||||||
|
* Provides guaranteed delivery with retry mechanism and acknowledgment handling.
|
||||||
|
*/
|
||||||
|
public interface MessageDeliveryService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to a specific client with delivery tracking and acknowledgment.
|
||||||
|
*
|
||||||
|
* @param clientId The target client identifier
|
||||||
|
* @param messageType The type of message (e.g., "jobs", "message", "auth", "task")
|
||||||
|
* @param payload The message payload (will be serialized to JSON)
|
||||||
|
* @param options Delivery options (retries, timeout, etc.)
|
||||||
|
* @return CompletableFuture with delivery receipt
|
||||||
|
*/
|
||||||
|
CompletableFuture<DeliveryReceipt> sendToClient(String clientId, String messageType, Object payload, DeliveryOptions options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to a specific client with default delivery options.
|
||||||
|
*
|
||||||
|
* @param clientId The target client identifier
|
||||||
|
* @param messageType The type of message
|
||||||
|
* @param payload The message payload
|
||||||
|
* @return CompletableFuture with delivery receipt
|
||||||
|
*/
|
||||||
|
default CompletableFuture<DeliveryReceipt> sendToClient(String clientId, String messageType, Object payload) {
|
||||||
|
return sendToClient(clientId, messageType, payload, DeliveryOptions.standard());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message with delivery tracking and acknowledgment.
|
||||||
|
* @deprecated Use {@link #sendToClient(String, String, Object, DeliveryOptions)} instead
|
||||||
|
*
|
||||||
|
* @param topic The destination topic
|
||||||
|
* @param payload The message payload (will be serialized to JSON)
|
||||||
|
* @param options Delivery options (retries, timeout, etc.)
|
||||||
|
* @return CompletableFuture with delivery receipt
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
CompletableFuture<DeliveryReceipt> sendMessage(String topic, Object payload, DeliveryOptions options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message with default delivery options.
|
||||||
|
* @deprecated Use {@link #sendToClient(String, String, Object)} instead
|
||||||
|
*
|
||||||
|
* @param topic The destination topic
|
||||||
|
* @param payload The message payload
|
||||||
|
* @return CompletableFuture with delivery receipt
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
default CompletableFuture<DeliveryReceipt> sendMessage(String topic, Object payload) {
|
||||||
|
return sendMessage(topic, payload, DeliveryOptions.standard());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming message envelope from transport layer.
|
||||||
|
* Extracts payload and routes to application layer.
|
||||||
|
*
|
||||||
|
* @param envelope The received message envelope
|
||||||
|
*/
|
||||||
|
void handleIncomingMessage(MessageEnvelope envelope);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle acknowledgment from client.
|
||||||
|
* Updates delivery status and removes from pending queue.
|
||||||
|
*
|
||||||
|
* @param ack The acknowledgment message
|
||||||
|
*/
|
||||||
|
void handleAcknowledgment(AcknowledgmentMessage ack);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current delivery status for a message.
|
||||||
|
*
|
||||||
|
* @param messageId The message ID
|
||||||
|
* @return Optional containing the delivery status, or empty if not found
|
||||||
|
*/
|
||||||
|
Optional<DeliveryStatus> getDeliveryStatus(String messageId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed pending delivery information.
|
||||||
|
*
|
||||||
|
* @param messageId The message ID
|
||||||
|
* @return Optional containing the pending delivery, or empty if not found
|
||||||
|
*/
|
||||||
|
Optional<PendingDelivery> getPendingDelivery(String messageId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry all pending deliveries that are ready for retry.
|
||||||
|
* Called by scheduled task.
|
||||||
|
*/
|
||||||
|
void retryPendingDeliveries();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired and completed deliveries.
|
||||||
|
* Called by scheduled task.
|
||||||
|
*/
|
||||||
|
void cleanupOldDeliveries();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.delivery;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import de.assecutor.votianlt.messaging.model.*;
|
||||||
|
import de.assecutor.votianlt.messaging.plugin.PluginManager;
|
||||||
|
import de.assecutor.votianlt.messaging.plugin.SendOptions;
|
||||||
|
import de.assecutor.votianlt.repository.MessageEnvelopeRepository;
|
||||||
|
import de.assecutor.votianlt.repository.PendingDeliveryRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of MessageDeliveryService with reliable delivery guarantees.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class MessageDeliveryServiceImpl implements MessageDeliveryService {
|
||||||
|
|
||||||
|
private final PluginManager pluginManager;
|
||||||
|
private final PendingDeliveryRepository pendingDeliveryRepository;
|
||||||
|
private final MessageEnvelopeRepository envelopeRepository;
|
||||||
|
private final AcknowledgmentHandler acknowledgmentHandler;
|
||||||
|
private final DeliveryConfig config;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public MessageDeliveryServiceImpl(
|
||||||
|
PluginManager pluginManager,
|
||||||
|
PendingDeliveryRepository pendingDeliveryRepository,
|
||||||
|
MessageEnvelopeRepository envelopeRepository,
|
||||||
|
AcknowledgmentHandler acknowledgmentHandler,
|
||||||
|
DeliveryConfig config) {
|
||||||
|
this.pluginManager = pluginManager;
|
||||||
|
this.pendingDeliveryRepository = pendingDeliveryRepository;
|
||||||
|
this.envelopeRepository = envelopeRepository;
|
||||||
|
this.acknowledgmentHandler = acknowledgmentHandler;
|
||||||
|
this.config = config;
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
this.objectMapper.registerModule(new JavaTimeModule());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<DeliveryReceipt> sendToClient(String clientId, String messageType, Object payload, DeliveryOptions options) {
|
||||||
|
try {
|
||||||
|
// Create destination identifier for tracking
|
||||||
|
String destination = clientId + "/" + messageType;
|
||||||
|
|
||||||
|
// Create message envelope
|
||||||
|
final LocalDateTime expiresAt = options.calculateExpiryTime();
|
||||||
|
MessageEnvelope envelope = new MessageEnvelope(destination, payload, options.isRequiresAck(), expiresAt);
|
||||||
|
|
||||||
|
// Save envelope to database
|
||||||
|
envelope = envelopeRepository.save(envelope);
|
||||||
|
final String messageId = envelope.getMessageId();
|
||||||
|
log.debug("[MessageDelivery] Created envelope {} for client {} (type: {})", messageId, clientId, messageType);
|
||||||
|
|
||||||
|
// Serialize envelope to JSON
|
||||||
|
String json = objectMapper.writeValueAsString(envelope);
|
||||||
|
byte[] envelopeData = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Create pending delivery record if acknowledgment is required
|
||||||
|
if (options.isRequiresAck()) {
|
||||||
|
PendingDelivery pending = new PendingDelivery(
|
||||||
|
messageId,
|
||||||
|
destination,
|
||||||
|
envelopeData,
|
||||||
|
options.getMaxRetries(),
|
||||||
|
expiresAt
|
||||||
|
);
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
log.debug("[MessageDelivery] Created pending delivery for message {}", messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send via plugin manager
|
||||||
|
SendOptions sendOptions = SendOptions.builder()
|
||||||
|
.qos(options.getQos())
|
||||||
|
.retained(options.isRetained())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final boolean requiresAck = options.isRequiresAck();
|
||||||
|
final Duration ackTimeout = options.getAckTimeout();
|
||||||
|
|
||||||
|
return pluginManager.sendToClient(clientId, messageType, envelopeData, sendOptions)
|
||||||
|
.thenApply(v -> {
|
||||||
|
// Update pending delivery status
|
||||||
|
if (requiresAck) {
|
||||||
|
updatePendingDeliveryAfterSend(messageId, ackTimeout);
|
||||||
|
}
|
||||||
|
log.info("[MessageDelivery] Successfully sent message {} to client {} (type: {})",
|
||||||
|
messageId, clientId, messageType);
|
||||||
|
return DeliveryReceipt.submitted(messageId, destination, expiresAt);
|
||||||
|
})
|
||||||
|
.exceptionally(ex -> {
|
||||||
|
log.error("[MessageDelivery] Failed to send message {} to client {} (type: {}): {}",
|
||||||
|
messageId, clientId, messageType, ex.getMessage());
|
||||||
|
if (requiresAck) {
|
||||||
|
markPendingDeliveryFailed(messageId, ex.getMessage());
|
||||||
|
}
|
||||||
|
return DeliveryReceipt.failed(messageId, destination);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error creating message for client {} (type: {}): {}",
|
||||||
|
clientId, messageType, e.getMessage(), e);
|
||||||
|
return CompletableFuture.completedFuture(DeliveryReceipt.failed("error", clientId + "/" + messageType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
public CompletableFuture<DeliveryReceipt> sendMessage(String topic, Object payload, DeliveryOptions options) {
|
||||||
|
// Extract clientId and messageType from topic
|
||||||
|
// Topic format: /client/{clientId}/{messageType}
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
if (parts.length >= 4 && parts[1].equals("client")) {
|
||||||
|
String clientId = parts[2];
|
||||||
|
String messageType = parts[3];
|
||||||
|
return sendToClient(clientId, messageType, payload, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for legacy topics - log warning
|
||||||
|
log.warn("[MessageDelivery] Using deprecated sendMessage with topic: {}", topic);
|
||||||
|
return CompletableFuture.completedFuture(DeliveryReceipt.failed("error", topic));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleIncomingMessage(MessageEnvelope envelope) {
|
||||||
|
try {
|
||||||
|
log.info("[MessageDelivery] Received message {} on topic {}",
|
||||||
|
envelope.getMessageId(), envelope.getTopic());
|
||||||
|
|
||||||
|
// Send acknowledgment if required
|
||||||
|
if (envelope.isRequiresAck()) {
|
||||||
|
sendAcknowledgment(envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to acknowledgment handler for application routing
|
||||||
|
acknowledgmentHandler.routeIncomingMessage(envelope);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error handling incoming message {}: {}",
|
||||||
|
envelope.getMessageId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleAcknowledgment(AcknowledgmentMessage ack) {
|
||||||
|
try {
|
||||||
|
log.info("[MessageDelivery] Received acknowledgment for message {} with status {}",
|
||||||
|
ack.getMessageId(), ack.getStatus());
|
||||||
|
|
||||||
|
Optional<PendingDelivery> pendingOpt = pendingDeliveryRepository.findByMessageId(ack.getMessageId());
|
||||||
|
|
||||||
|
if (pendingOpt.isEmpty()) {
|
||||||
|
log.warn("[MessageDelivery] No pending delivery found for acknowledged message {}",
|
||||||
|
ack.getMessageId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingDelivery pending = pendingOpt.get();
|
||||||
|
|
||||||
|
switch (ack.getStatus()) {
|
||||||
|
case RECEIVED, PROCESSED -> {
|
||||||
|
pending.markAsAcknowledged();
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
log.info("[MessageDelivery] Message {} acknowledged successfully", ack.getMessageId());
|
||||||
|
}
|
||||||
|
case FAILED -> {
|
||||||
|
pending.markAsFailed(ack.getErrorMessage());
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
log.warn("[MessageDelivery] Message {} failed on client: {}",
|
||||||
|
ack.getMessageId(), ack.getErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error handling acknowledgment for message {}: {}",
|
||||||
|
ack.getMessageId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<DeliveryStatus> getDeliveryStatus(String messageId) {
|
||||||
|
return pendingDeliveryRepository.findByMessageId(messageId)
|
||||||
|
.map(PendingDelivery::getStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<PendingDelivery> getPendingDelivery(String messageId) {
|
||||||
|
return pendingDeliveryRepository.findByMessageId(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void retryPendingDeliveries() {
|
||||||
|
try {
|
||||||
|
List<PendingDelivery> readyForRetry = pendingDeliveryRepository
|
||||||
|
.findByStatusAndNextRetryAtBefore(DeliveryStatus.SENT, LocalDateTime.now());
|
||||||
|
|
||||||
|
if (readyForRetry.isEmpty()) {
|
||||||
|
log.debug("[MessageDelivery] No pending deliveries ready for retry");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[MessageDelivery] Retrying {} pending deliveries", readyForRetry.size());
|
||||||
|
|
||||||
|
for (PendingDelivery pending : readyForRetry) {
|
||||||
|
retryDelivery(pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error during retry process: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanupOldDeliveries() {
|
||||||
|
try {
|
||||||
|
// Clean up acknowledged deliveries older than configured retention
|
||||||
|
LocalDateTime cutoff = LocalDateTime.now().minus(Duration.ofDays(7));
|
||||||
|
List<PendingDelivery> oldAcknowledged = pendingDeliveryRepository
|
||||||
|
.findByStatusAndAcknowledgedAtBefore(DeliveryStatus.ACKNOWLEDGED, cutoff);
|
||||||
|
|
||||||
|
if (!oldAcknowledged.isEmpty()) {
|
||||||
|
pendingDeliveryRepository.deleteAll(oldAcknowledged);
|
||||||
|
log.info("[MessageDelivery] Cleaned up {} old acknowledged deliveries", oldAcknowledged.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark expired deliveries
|
||||||
|
List<PendingDelivery> expired = pendingDeliveryRepository
|
||||||
|
.findByStatusInAndExpiresAtBefore(
|
||||||
|
List.of(DeliveryStatus.PENDING, DeliveryStatus.SENT),
|
||||||
|
LocalDateTime.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (PendingDelivery pending : expired) {
|
||||||
|
pending.markAsExpired();
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expired.isEmpty()) {
|
||||||
|
log.info("[MessageDelivery] Marked {} deliveries as expired", expired.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error during cleanup: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update pending delivery after successful send
|
||||||
|
*/
|
||||||
|
private void updatePendingDeliveryAfterSend(String messageId, Duration ackTimeout) {
|
||||||
|
try {
|
||||||
|
Optional<PendingDelivery> pendingOpt = pendingDeliveryRepository.findByMessageId(messageId);
|
||||||
|
if (pendingOpt.isPresent()) {
|
||||||
|
PendingDelivery pending = pendingOpt.get();
|
||||||
|
LocalDateTime nextRetry = LocalDateTime.now().plus(ackTimeout);
|
||||||
|
pending.markAsSent(nextRetry);
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error updating pending delivery {}: {}", messageId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark pending delivery as failed
|
||||||
|
*/
|
||||||
|
private void markPendingDeliveryFailed(String messageId, String reason) {
|
||||||
|
try {
|
||||||
|
Optional<PendingDelivery> pendingOpt = pendingDeliveryRepository.findByMessageId(messageId);
|
||||||
|
if (pendingOpt.isPresent()) {
|
||||||
|
PendingDelivery pending = pendingOpt.get();
|
||||||
|
pending.markAsFailed(reason);
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error marking delivery as failed {}: {}", messageId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry a pending delivery
|
||||||
|
*/
|
||||||
|
private void retryDelivery(PendingDelivery pending) {
|
||||||
|
try {
|
||||||
|
// Check if expired
|
||||||
|
if (pending.isExpired()) {
|
||||||
|
pending.markAsExpired();
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
log.warn("[MessageDelivery] Message {} expired, not retrying", pending.getMessageId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if max retries reached
|
||||||
|
if (pending.hasReachedMaxRetries()) {
|
||||||
|
pending.markAsFailed("Max retries reached");
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
log.warn("[MessageDelivery] Message {} reached max retries", pending.getMessageId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment retry count
|
||||||
|
pending.incrementRetryCount();
|
||||||
|
|
||||||
|
// Calculate next retry time with exponential backoff
|
||||||
|
Duration backoffDelay = calculateBackoff(pending.getRetryCount());
|
||||||
|
LocalDateTime nextRetry = LocalDateTime.now().plus(backoffDelay);
|
||||||
|
|
||||||
|
// Extract clientId and messageType from topic
|
||||||
|
// Topic format: clientId/messageType
|
||||||
|
String topic = pending.getTopic();
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
log.error("[MessageDelivery] Invalid topic format for retry: {}", topic);
|
||||||
|
pending.markAsFailed("Invalid topic format");
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientId = parts[0];
|
||||||
|
String messageType = parts[1];
|
||||||
|
|
||||||
|
// Send via plugin manager
|
||||||
|
SendOptions options = SendOptions.reliable();
|
||||||
|
pluginManager.sendToClient(clientId, messageType, pending.getEnvelopeData(), options)
|
||||||
|
.thenAccept(v -> {
|
||||||
|
pending.markAsSent(nextRetry);
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
log.info("[MessageDelivery] Retry {} successful for message {}",
|
||||||
|
pending.getRetryCount(), pending.getMessageId());
|
||||||
|
})
|
||||||
|
.exceptionally(ex -> {
|
||||||
|
log.error("[MessageDelivery] Retry failed for message {}: {}",
|
||||||
|
pending.getMessageId(), ex.getMessage());
|
||||||
|
pending.markAsFailed(ex.getMessage());
|
||||||
|
pendingDeliveryRepository.save(pending);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error retrying delivery {}: {}",
|
||||||
|
pending.getMessageId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send acknowledgment back to client
|
||||||
|
*/
|
||||||
|
private void sendAcknowledgment(MessageEnvelope envelope) {
|
||||||
|
try {
|
||||||
|
// Extract client ID from topic (e.g., /server/{clientId}/... or clientId/messageType)
|
||||||
|
String clientId = extractClientIdFromTopic(envelope.getTopic());
|
||||||
|
if (clientId == null) {
|
||||||
|
log.warn("[MessageDelivery] Cannot send ACK, no clientId in topic: {}", envelope.getTopic());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create acknowledgment message
|
||||||
|
AcknowledgmentMessage ack = new AcknowledgmentMessage(
|
||||||
|
envelope.getMessageId(),
|
||||||
|
AckStatus.RECEIVED,
|
||||||
|
"server"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send ACK to client using new API
|
||||||
|
String ackJson = objectMapper.writeValueAsString(ack);
|
||||||
|
byte[] ackData = ackJson.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
pluginManager.sendAckToClient(clientId, envelope.getMessageId(), ackData, SendOptions.fireAndForget())
|
||||||
|
.thenAccept(v -> log.debug("[MessageDelivery] Sent ACK for message {}", envelope.getMessageId()))
|
||||||
|
.exceptionally(ex -> {
|
||||||
|
log.error("[MessageDelivery] Failed to send ACK for message {}: {}",
|
||||||
|
envelope.getMessageId(), ex.getMessage());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MessageDelivery] Error sending acknowledgment for message {}: {}",
|
||||||
|
envelope.getMessageId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate exponential backoff delay
|
||||||
|
*/
|
||||||
|
private Duration calculateBackoff(int retryCount) {
|
||||||
|
long delayMs = (long) (config.getRetryInitialDelay().toMillis()
|
||||||
|
* Math.pow(config.getRetryBackoffMultiplier(), retryCount - 1));
|
||||||
|
long maxDelayMs = config.getRetryMaxDelay().toMillis();
|
||||||
|
return Duration.ofMillis(Math.min(delayMs, maxDelayMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract client ID from topic pattern.
|
||||||
|
* Supports both old format (/server/{clientId}/...) and new format (clientId/messageType)
|
||||||
|
*/
|
||||||
|
private String extractClientIdFromTopic(String topic) {
|
||||||
|
if (topic == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old format: /server/{clientId}/...
|
||||||
|
if (topic.startsWith("/server/")) {
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
if (parts.length > 2) {
|
||||||
|
return parts[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New format: clientId/messageType
|
||||||
|
if (topic.contains("/")) {
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
if (parts.length >= 1) {
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.delivery;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheduled tasks for message delivery retry and cleanup.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class RetryScheduler {
|
||||||
|
|
||||||
|
private final MessageDeliveryService deliveryService;
|
||||||
|
|
||||||
|
public RetryScheduler(MessageDeliveryService deliveryService) {
|
||||||
|
this.deliveryService = deliveryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry pending deliveries every 30 seconds (configurable)
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelayString = "${app.messaging.delivery.retry-interval-seconds:30}000")
|
||||||
|
public void retryPendingDeliveries() {
|
||||||
|
try {
|
||||||
|
log.debug("[RetryScheduler] Starting retry task");
|
||||||
|
deliveryService.retryPendingDeliveries();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[RetryScheduler] Error in retry task: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup old deliveries every hour (configurable)
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelayString = "${app.messaging.delivery.cleanup-interval-minutes:60}000")
|
||||||
|
public void cleanupOldDeliveries() {
|
||||||
|
try {
|
||||||
|
log.debug("[RetryScheduler] Starting cleanup task");
|
||||||
|
deliveryService.cleanupOldDeliveries();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[RetryScheduler] Error in cleanup task: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of message acknowledgment from client.
|
||||||
|
*/
|
||||||
|
public enum AckStatus {
|
||||||
|
/**
|
||||||
|
* Message was received by the client
|
||||||
|
*/
|
||||||
|
RECEIVED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message was successfully processed by the client
|
||||||
|
*/
|
||||||
|
PROCESSED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message processing failed on the client side
|
||||||
|
*/
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledgment message sent by clients to confirm message receipt/processing.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AcknowledgmentMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID of the message being acknowledged
|
||||||
|
*/
|
||||||
|
private String messageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of the acknowledgment
|
||||||
|
*/
|
||||||
|
private AckStatus status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the acknowledgment was created
|
||||||
|
*/
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID of the client sending the acknowledgment
|
||||||
|
*/
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional error message if status is FAILED
|
||||||
|
*/
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for successful acknowledgment
|
||||||
|
*/
|
||||||
|
public AcknowledgmentMessage(String messageId, AckStatus status, String clientId) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
this.status = status;
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for failed acknowledgment with error message
|
||||||
|
*/
|
||||||
|
public AcknowledgmentMessage(String messageId, String clientId, String errorMessage) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
this.status = AckStatus.FAILED;
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for message delivery configuration.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DeliveryOptions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this message requires acknowledgment
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private boolean requiresAck = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of retry attempts
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private int maxRetries = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout for acknowledgment
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private Duration ackTimeout = Duration.ofSeconds(30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message expiry duration from now
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private Duration expiryDuration = Duration.ofHours(24);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QoS level for transport (MQTT specific, but kept generic)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private int qos = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether message should be retained by broker
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private boolean retained = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate expiry timestamp from duration
|
||||||
|
*/
|
||||||
|
public LocalDateTime calculateExpiryTime() {
|
||||||
|
return LocalDateTime.now().plus(expiryDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default options for standard messages
|
||||||
|
*/
|
||||||
|
public static DeliveryOptions standard() {
|
||||||
|
return DeliveryOptions.builder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for fire-and-forget messages (no acknowledgment required)
|
||||||
|
*/
|
||||||
|
public static DeliveryOptions fireAndForget() {
|
||||||
|
return DeliveryOptions.builder()
|
||||||
|
.requiresAck(false)
|
||||||
|
.maxRetries(0)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for critical messages with extended retry
|
||||||
|
*/
|
||||||
|
public static DeliveryOptions critical() {
|
||||||
|
return DeliveryOptions.builder()
|
||||||
|
.requiresAck(true)
|
||||||
|
.maxRetries(5)
|
||||||
|
.ackTimeout(Duration.ofMinutes(2))
|
||||||
|
.expiryDuration(Duration.ofDays(7))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receipt returned when a message is submitted for delivery.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DeliveryReceipt {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique message identifier
|
||||||
|
*/
|
||||||
|
private String messageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic where message was sent
|
||||||
|
*/
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the message was submitted
|
||||||
|
*/
|
||||||
|
private LocalDateTime submittedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial delivery status
|
||||||
|
*/
|
||||||
|
private DeliveryStatus status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the message will expire
|
||||||
|
*/
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a receipt for a successfully submitted message
|
||||||
|
*/
|
||||||
|
public static DeliveryReceipt submitted(String messageId, String topic, LocalDateTime expiresAt) {
|
||||||
|
return new DeliveryReceipt(
|
||||||
|
messageId,
|
||||||
|
topic,
|
||||||
|
LocalDateTime.now(),
|
||||||
|
DeliveryStatus.PENDING,
|
||||||
|
expiresAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a receipt for a failed submission
|
||||||
|
*/
|
||||||
|
public static DeliveryReceipt failed(String messageId, String topic) {
|
||||||
|
return new DeliveryReceipt(
|
||||||
|
messageId,
|
||||||
|
topic,
|
||||||
|
LocalDateTime.now(),
|
||||||
|
DeliveryStatus.FAILED,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of a message delivery attempt.
|
||||||
|
*/
|
||||||
|
public enum DeliveryStatus {
|
||||||
|
/**
|
||||||
|
* Message is queued but not yet sent
|
||||||
|
*/
|
||||||
|
PENDING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message has been sent to the transport layer
|
||||||
|
*/
|
||||||
|
SENT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client has acknowledged receipt of the message
|
||||||
|
*/
|
||||||
|
ACKNOWLEDGED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delivery failed after all retry attempts
|
||||||
|
*/
|
||||||
|
FAILED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message expired before delivery could be confirmed
|
||||||
|
*/
|
||||||
|
EXPIRED
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.index.Indexed;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envelope that wraps all messages sent through the messaging system.
|
||||||
|
* Contains metadata for delivery tracking and acknowledgment.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Document(collection = "message_envelopes")
|
||||||
|
public class MessageEnvelope {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@JsonIgnore
|
||||||
|
private ObjectId id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique identifier for this message (UUID)
|
||||||
|
*/
|
||||||
|
@Field("message_id")
|
||||||
|
@Indexed(unique = true)
|
||||||
|
private String messageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the envelope was created
|
||||||
|
*/
|
||||||
|
@Field("timestamp")
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target topic for this message
|
||||||
|
*/
|
||||||
|
@Field("topic")
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual message payload (can be any serializable object)
|
||||||
|
*/
|
||||||
|
@Field("payload")
|
||||||
|
private Object payload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this message requires acknowledgment from the receiver
|
||||||
|
*/
|
||||||
|
@Field("requires_ack")
|
||||||
|
private boolean requiresAck;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of times this message has been retried
|
||||||
|
*/
|
||||||
|
@Field("retry_count")
|
||||||
|
private int retryCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When this message expires and should no longer be delivered
|
||||||
|
*/
|
||||||
|
@Field("expires_at")
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional metadata for the message
|
||||||
|
*/
|
||||||
|
@Field("metadata")
|
||||||
|
private Map<String, String> metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for creating a new envelope with payload
|
||||||
|
*/
|
||||||
|
public MessageEnvelope(String topic, Object payload, boolean requiresAck, LocalDateTime expiresAt) {
|
||||||
|
this.messageId = UUID.randomUUID().toString();
|
||||||
|
this.timestamp = LocalDateTime.now();
|
||||||
|
this.topic = topic;
|
||||||
|
this.payload = payload;
|
||||||
|
this.requiresAck = requiresAck;
|
||||||
|
this.retryCount = 0;
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
this.metadata = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this message has expired
|
||||||
|
*/
|
||||||
|
public boolean isExpired() {
|
||||||
|
return expiresAt != null && LocalDateTime.now().isAfter(expiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the retry counter
|
||||||
|
*/
|
||||||
|
public void incrementRetryCount() {
|
||||||
|
this.retryCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add metadata to the envelope
|
||||||
|
*/
|
||||||
|
public void addMetadata(String key, String value) {
|
||||||
|
if (this.metadata == null) {
|
||||||
|
this.metadata = new HashMap<>();
|
||||||
|
}
|
||||||
|
this.metadata.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ObjectId as string for JSON serialization
|
||||||
|
*/
|
||||||
|
@JsonGetter("id")
|
||||||
|
public String getIdAsString() {
|
||||||
|
return id != null ? id.toString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.index.Indexed;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a message delivery that is pending acknowledgment.
|
||||||
|
* Stored in MongoDB for retry and tracking purposes.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Document(collection = "pending_deliveries")
|
||||||
|
public class PendingDelivery {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@JsonIgnore
|
||||||
|
private ObjectId id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique message identifier
|
||||||
|
*/
|
||||||
|
@Field("message_id")
|
||||||
|
@Indexed(unique = true)
|
||||||
|
private String messageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target topic for this message
|
||||||
|
*/
|
||||||
|
@Field("topic")
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized envelope data (JSON bytes)
|
||||||
|
*/
|
||||||
|
@Field("envelope_data")
|
||||||
|
private byte[] envelopeData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current delivery status
|
||||||
|
*/
|
||||||
|
@Field("status")
|
||||||
|
@Indexed
|
||||||
|
private DeliveryStatus status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the delivery record was created
|
||||||
|
*/
|
||||||
|
@Field("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the message was last sent
|
||||||
|
*/
|
||||||
|
@Field("sent_at")
|
||||||
|
private LocalDateTime sentAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When acknowledgment was received
|
||||||
|
*/
|
||||||
|
@Field("acknowledged_at")
|
||||||
|
private LocalDateTime acknowledgedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of retry attempts made
|
||||||
|
*/
|
||||||
|
@Field("retry_count")
|
||||||
|
private int retryCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of retries allowed
|
||||||
|
*/
|
||||||
|
@Field("max_retries")
|
||||||
|
private int maxRetries;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the next retry should be attempted
|
||||||
|
*/
|
||||||
|
@Field("next_retry_at")
|
||||||
|
@Indexed
|
||||||
|
private LocalDateTime nextRetryAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When this delivery expires
|
||||||
|
*/
|
||||||
|
@Field("expires_at")
|
||||||
|
@Indexed
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reason for failure (if status is FAILED)
|
||||||
|
*/
|
||||||
|
@Field("failure_reason")
|
||||||
|
private String failureReason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client ID (extracted from topic if available)
|
||||||
|
*/
|
||||||
|
@Field("client_id")
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for new pending delivery
|
||||||
|
*/
|
||||||
|
public PendingDelivery(String messageId, String topic, byte[] envelopeData,
|
||||||
|
int maxRetries, LocalDateTime expiresAt) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
this.topic = topic;
|
||||||
|
this.envelopeData = envelopeData;
|
||||||
|
this.status = DeliveryStatus.PENDING;
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.retryCount = 0;
|
||||||
|
this.maxRetries = maxRetries;
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
this.clientId = extractClientIdFromTopic(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark as sent and schedule next retry
|
||||||
|
*/
|
||||||
|
public void markAsSent(LocalDateTime nextRetryAt) {
|
||||||
|
this.status = DeliveryStatus.SENT;
|
||||||
|
this.sentAt = LocalDateTime.now();
|
||||||
|
this.nextRetryAt = nextRetryAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark as acknowledged
|
||||||
|
*/
|
||||||
|
public void markAsAcknowledged() {
|
||||||
|
this.status = DeliveryStatus.ACKNOWLEDGED;
|
||||||
|
this.acknowledgedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark as failed with reason
|
||||||
|
*/
|
||||||
|
public void markAsFailed(String reason) {
|
||||||
|
this.status = DeliveryStatus.FAILED;
|
||||||
|
this.failureReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark as expired
|
||||||
|
*/
|
||||||
|
public void markAsExpired() {
|
||||||
|
this.status = DeliveryStatus.EXPIRED;
|
||||||
|
this.failureReason = "Message expired before delivery could be confirmed";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment retry count
|
||||||
|
*/
|
||||||
|
public void incrementRetryCount() {
|
||||||
|
this.retryCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if max retries reached
|
||||||
|
*/
|
||||||
|
public boolean hasReachedMaxRetries() {
|
||||||
|
return retryCount >= maxRetries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if expired
|
||||||
|
*/
|
||||||
|
public boolean isExpired() {
|
||||||
|
return expiresAt != null && LocalDateTime.now().isAfter(expiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if ready for retry
|
||||||
|
*/
|
||||||
|
public boolean isReadyForRetry() {
|
||||||
|
return status == DeliveryStatus.SENT
|
||||||
|
&& nextRetryAt != null
|
||||||
|
&& LocalDateTime.now().isAfter(nextRetryAt)
|
||||||
|
&& !hasReachedMaxRetries()
|
||||||
|
&& !isExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract client ID from topic pattern /client/{clientId}/...
|
||||||
|
*/
|
||||||
|
private String extractClientIdFromTopic(String topic) {
|
||||||
|
if (topic != null && topic.startsWith("/client/")) {
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
if (parts.length > 2) {
|
||||||
|
return parts[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ObjectId as string for JSON serialization
|
||||||
|
*/
|
||||||
|
@JsonGetter("id")
|
||||||
|
public String getIdAsString() {
|
||||||
|
return id != null ? id.toString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event representing a connection state change.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ConnectionStateEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection state
|
||||||
|
*/
|
||||||
|
private ConnectionState state;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Previous connection state
|
||||||
|
*/
|
||||||
|
private ConnectionState previousState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp of the state change
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private LocalDateTime timestamp = LocalDateTime.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional error message if state is ERROR or DISCONNECTED
|
||||||
|
*/
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional exception if state is ERROR
|
||||||
|
*/
|
||||||
|
private Throwable exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin that generated this event
|
||||||
|
*/
|
||||||
|
private String pluginName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection states
|
||||||
|
*/
|
||||||
|
public enum ConnectionState {
|
||||||
|
/**
|
||||||
|
* Plugin is initializing
|
||||||
|
*/
|
||||||
|
INITIALIZING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin is connecting to the transport
|
||||||
|
*/
|
||||||
|
CONNECTING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin is connected and ready
|
||||||
|
*/
|
||||||
|
CONNECTED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin is disconnecting
|
||||||
|
*/
|
||||||
|
DISCONNECTING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin is disconnected
|
||||||
|
*/
|
||||||
|
DISCONNECTED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin encountered an error
|
||||||
|
*/
|
||||||
|
ERROR,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin is reconnecting after a failure
|
||||||
|
*/
|
||||||
|
RECONNECTING
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the connection is active.
|
||||||
|
*
|
||||||
|
* @return true if connected
|
||||||
|
*/
|
||||||
|
public boolean isConnected() {
|
||||||
|
return state == ConnectionState.CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there was an error.
|
||||||
|
*
|
||||||
|
* @return true if state is ERROR
|
||||||
|
*/
|
||||||
|
public boolean isError() {
|
||||||
|
return state == ConnectionState.ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for messaging transport plugins.
|
||||||
|
* Plugins implement specific transport protocols (MQTT, WebSocket, gRPC, etc.)
|
||||||
|
* and provide a unified interface for the messaging layer.
|
||||||
|
*
|
||||||
|
* The plugin is responsible for managing the internal topic/channel structure.
|
||||||
|
* The messaging layer only uses clientId and messageType as identifiers.
|
||||||
|
*/
|
||||||
|
public interface MessagingPlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the plugin with configuration.
|
||||||
|
* Called once during application startup.
|
||||||
|
*
|
||||||
|
* @param config Plugin-specific configuration
|
||||||
|
* @throws PluginException if initialization fails
|
||||||
|
*/
|
||||||
|
void init(PluginConfig config) throws PluginException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown the plugin and release resources.
|
||||||
|
* Called during application shutdown.
|
||||||
|
*
|
||||||
|
* @throws PluginException if shutdown fails
|
||||||
|
*/
|
||||||
|
void exit() throws PluginException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback when connection state changes.
|
||||||
|
* The plugin should call this method when the underlying transport
|
||||||
|
* connection state changes (connected, disconnected, error).
|
||||||
|
*
|
||||||
|
* @param listener Connection state listener
|
||||||
|
*/
|
||||||
|
void setConnectionListener(ConnectionStateListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to a specific client.
|
||||||
|
* The plugin is responsible for determining the correct topic/channel based on the messageType.
|
||||||
|
*
|
||||||
|
* @param clientId Target client identifier
|
||||||
|
* @param messageType Type of message (e.g., "jobs", "message", "auth", "task")
|
||||||
|
* @param payload Message payload as byte array
|
||||||
|
* @param options Transport-specific options
|
||||||
|
* @return CompletableFuture that completes when message is sent
|
||||||
|
* @throws PluginException if sending fails
|
||||||
|
*/
|
||||||
|
CompletableFuture<Void> sendToClient(String clientId, String messageType, byte[] payload, SendOptions options) throws PluginException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an acknowledgment to a specific client.
|
||||||
|
* The plugin is responsible for determining the correct ACK topic/channel.
|
||||||
|
*
|
||||||
|
* @param clientId Target client identifier
|
||||||
|
* @param messageId Message ID being acknowledged
|
||||||
|
* @param payload ACK payload as byte array
|
||||||
|
* @param options Transport-specific options
|
||||||
|
* @return CompletableFuture that completes when ACK is sent
|
||||||
|
* @throws PluginException if sending fails
|
||||||
|
*/
|
||||||
|
CompletableFuture<Void> sendAckToClient(String clientId, String messageId, byte[] payload, SendOptions options) throws PluginException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler for incoming messages of a specific type from clients.
|
||||||
|
* The plugin is responsible for subscribing to the appropriate topics/channels.
|
||||||
|
*
|
||||||
|
* @param messageType Type of message to handle (e.g., "task_completed", "message", "jobs/assigned", "login")
|
||||||
|
* @param handler Message handler to be called when a message is received
|
||||||
|
* @throws PluginException if registration fails
|
||||||
|
*/
|
||||||
|
void registerMessageHandler(String messageType, ClientMessageHandler handler) throws PluginException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler for incoming acknowledgments from clients.
|
||||||
|
* The plugin is responsible for subscribing to the appropriate ACK topics/channels.
|
||||||
|
*
|
||||||
|
* @param handler ACK handler to be called when an ACK is received
|
||||||
|
* @throws PluginException if registration fails
|
||||||
|
*/
|
||||||
|
void registerAckHandler(AckHandler handler) throws PluginException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the plugin is currently connected.
|
||||||
|
*
|
||||||
|
* @return true if connected, false otherwise
|
||||||
|
*/
|
||||||
|
boolean isConnected();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the plugin name/type identifier.
|
||||||
|
*
|
||||||
|
* @return Plugin name (e.g., "mqtt", "websocket", "grpc")
|
||||||
|
*/
|
||||||
|
String getPluginName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin version.
|
||||||
|
*
|
||||||
|
* @return Plugin version string
|
||||||
|
*/
|
||||||
|
String getPluginVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin metadata/information.
|
||||||
|
*
|
||||||
|
* @return Plugin metadata
|
||||||
|
*/
|
||||||
|
PluginMetadata getMetadata();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface for connection state changes.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ConnectionStateListener {
|
||||||
|
/**
|
||||||
|
* Called when connection state changes.
|
||||||
|
*
|
||||||
|
* @param event Connection state event
|
||||||
|
*/
|
||||||
|
void onConnectionStateChanged(ConnectionStateEvent event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for received messages from clients.
|
||||||
|
* Includes the clientId extracted from the topic/channel.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ClientMessageHandler {
|
||||||
|
/**
|
||||||
|
* Called when a message is received from a client.
|
||||||
|
*
|
||||||
|
* @param clientId Client identifier extracted from the topic/channel
|
||||||
|
* @param payload Message payload as byte array
|
||||||
|
*/
|
||||||
|
void onMessageReceived(String clientId, byte[] payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for received acknowledgments from clients.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
interface AckHandler {
|
||||||
|
/**
|
||||||
|
* Called when an ACK is received from a client.
|
||||||
|
*
|
||||||
|
* @param messageId Message ID being acknowledged
|
||||||
|
* @param payload ACK payload as byte array
|
||||||
|
*/
|
||||||
|
void onAckReceived(String messageId, byte[] payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for messaging plugins.
|
||||||
|
* Provides a flexible key-value store for plugin-specific settings.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PluginConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin-specific properties
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private Map<String, Object> properties = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string property.
|
||||||
|
*
|
||||||
|
* @param key Property key
|
||||||
|
* @return Property value or null if not found
|
||||||
|
*/
|
||||||
|
public String getString(String key) {
|
||||||
|
Object value = properties.get(key);
|
||||||
|
return value != null ? value.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a string property with default value.
|
||||||
|
*
|
||||||
|
* @param key Property key
|
||||||
|
* @param defaultValue Default value if property not found
|
||||||
|
* @return Property value or default
|
||||||
|
*/
|
||||||
|
public String getString(String key, String defaultValue) {
|
||||||
|
String value = getString(key);
|
||||||
|
return value != null ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an integer property.
|
||||||
|
*
|
||||||
|
* @param key Property key
|
||||||
|
* @return Property value or null if not found
|
||||||
|
*/
|
||||||
|
public Integer getInt(String key) {
|
||||||
|
Object value = properties.get(key);
|
||||||
|
if (value instanceof Integer) {
|
||||||
|
return (Integer) value;
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt((String) value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an integer property with default value.
|
||||||
|
*
|
||||||
|
* @param key Property key
|
||||||
|
* @param defaultValue Default value if property not found
|
||||||
|
* @return Property value or default
|
||||||
|
*/
|
||||||
|
public int getInt(String key, int defaultValue) {
|
||||||
|
Integer value = getInt(key);
|
||||||
|
return value != null ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a boolean property.
|
||||||
|
*
|
||||||
|
* @param key Property key
|
||||||
|
* @return Property value or null if not found
|
||||||
|
*/
|
||||||
|
public Boolean getBoolean(String key) {
|
||||||
|
Object value = properties.get(key);
|
||||||
|
if (value instanceof Boolean) {
|
||||||
|
return (Boolean) value;
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
return Boolean.parseBoolean((String) value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a boolean property with default value.
|
||||||
|
*
|
||||||
|
* @param key Property key
|
||||||
|
* @param defaultValue Default value if property not found
|
||||||
|
* @return Property value or default
|
||||||
|
*/
|
||||||
|
public boolean getBoolean(String key, boolean defaultValue) {
|
||||||
|
Boolean value = getBoolean(key);
|
||||||
|
return value != null ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a property.
|
||||||
|
*
|
||||||
|
* @param key Property key
|
||||||
|
* @param value Property value
|
||||||
|
*/
|
||||||
|
public void setProperty(String key, Object value) {
|
||||||
|
properties.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a property exists.
|
||||||
|
*
|
||||||
|
* @param key Property key
|
||||||
|
* @return true if property exists
|
||||||
|
*/
|
||||||
|
public boolean hasProperty(String key) {
|
||||||
|
return properties.containsKey(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown by messaging plugins.
|
||||||
|
*/
|
||||||
|
public class PluginException extends Exception {
|
||||||
|
|
||||||
|
public PluginException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for messaging plugins.
|
||||||
|
* Handles plugin lifecycle, registration, and delegation.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class PluginManager {
|
||||||
|
|
||||||
|
private MessagingPlugin activePlugin;
|
||||||
|
private final List<ConnectionStateEvent> connectionHistory = new ArrayList<>();
|
||||||
|
private final List<PluginStateListener> stateListeners = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize and activate a plugin.
|
||||||
|
*
|
||||||
|
* @param plugin Plugin to activate
|
||||||
|
* @param config Plugin configuration
|
||||||
|
* @throws PluginException if initialization fails
|
||||||
|
*/
|
||||||
|
public void activatePlugin(MessagingPlugin plugin, PluginConfig config) throws PluginException {
|
||||||
|
log.info("[PluginManager] Activating plugin: {}", plugin.getPluginName());
|
||||||
|
|
||||||
|
// Shutdown existing plugin if any
|
||||||
|
if (activePlugin != null) {
|
||||||
|
log.info("[PluginManager] Shutting down existing plugin: {}", activePlugin.getPluginName());
|
||||||
|
try {
|
||||||
|
activePlugin.exit();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[PluginManager] Error shutting down existing plugin: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set connection listener
|
||||||
|
plugin.setConnectionListener(event -> {
|
||||||
|
String previousState = event.getPreviousState() != null
|
||||||
|
? event.getPreviousState().toString()
|
||||||
|
: "NONE";
|
||||||
|
log.info("[PluginManager] Connection state changed: {} -> {}",
|
||||||
|
previousState, event.getState());
|
||||||
|
connectionHistory.add(event);
|
||||||
|
notifyStateListeners(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize plugin
|
||||||
|
plugin.init(config);
|
||||||
|
activePlugin = plugin;
|
||||||
|
|
||||||
|
log.info("[PluginManager] Plugin activated: {} v{}",
|
||||||
|
plugin.getPluginName(), plugin.getPluginVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently active plugin.
|
||||||
|
*
|
||||||
|
* @return Active plugin or empty if none
|
||||||
|
*/
|
||||||
|
public Optional<MessagingPlugin> getActivePlugin() {
|
||||||
|
return Optional.ofNullable(activePlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to a specific client via the active plugin.
|
||||||
|
*
|
||||||
|
* @param clientId Target client identifier
|
||||||
|
* @param messageType Type of message (e.g., "jobs", "message", "auth", "task")
|
||||||
|
* @param payload Message payload
|
||||||
|
* @param options Send options
|
||||||
|
* @return CompletableFuture that completes when message is sent
|
||||||
|
* @throws PluginException if no plugin is active or sending fails
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> sendToClient(String clientId, String messageType, byte[] payload, SendOptions options) throws PluginException {
|
||||||
|
if (activePlugin == null) {
|
||||||
|
return CompletableFuture.failedFuture(new PluginException("No active plugin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activePlugin.isConnected()) {
|
||||||
|
return CompletableFuture.failedFuture(new PluginException("Plugin is not connected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return activePlugin.sendToClient(clientId, messageType, payload, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an acknowledgment to a specific client via the active plugin.
|
||||||
|
*
|
||||||
|
* @param clientId Target client identifier
|
||||||
|
* @param messageId Message ID being acknowledged
|
||||||
|
* @param payload ACK payload
|
||||||
|
* @param options Send options
|
||||||
|
* @return CompletableFuture that completes when ACK is sent
|
||||||
|
* @throws PluginException if no plugin is active or sending fails
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> sendAckToClient(String clientId, String messageId, byte[] payload, SendOptions options) throws PluginException {
|
||||||
|
if (activePlugin == null) {
|
||||||
|
return CompletableFuture.failedFuture(new PluginException("No active plugin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activePlugin.isConnected()) {
|
||||||
|
return CompletableFuture.failedFuture(new PluginException("Plugin is not connected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return activePlugin.sendAckToClient(clientId, messageId, payload, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler for incoming messages of a specific type from clients.
|
||||||
|
*
|
||||||
|
* @param messageType Type of message to handle
|
||||||
|
* @param handler Message handler
|
||||||
|
* @throws PluginException if no plugin is active or registration fails
|
||||||
|
*/
|
||||||
|
public void registerMessageHandler(String messageType, MessagingPlugin.ClientMessageHandler handler) throws PluginException {
|
||||||
|
if (activePlugin == null) {
|
||||||
|
throw new PluginException("No active plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
activePlugin.registerMessageHandler(messageType, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler for incoming acknowledgments from clients.
|
||||||
|
*
|
||||||
|
* @param handler ACK handler
|
||||||
|
* @throws PluginException if no plugin is active or registration fails
|
||||||
|
*/
|
||||||
|
public void registerAckHandler(MessagingPlugin.AckHandler handler) throws PluginException {
|
||||||
|
if (activePlugin == null) {
|
||||||
|
throw new PluginException("No active plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
activePlugin.registerAckHandler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the active plugin is connected.
|
||||||
|
*
|
||||||
|
* @return true if connected, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean isConnected() {
|
||||||
|
return activePlugin != null && activePlugin.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get metadata of the active plugin.
|
||||||
|
*
|
||||||
|
* @return Plugin metadata or empty if no plugin is active
|
||||||
|
*/
|
||||||
|
public Optional<PluginMetadata> getActivePluginMetadata() {
|
||||||
|
return Optional.ofNullable(activePlugin).map(MessagingPlugin::getMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection history.
|
||||||
|
*
|
||||||
|
* @return List of connection state events
|
||||||
|
*/
|
||||||
|
public List<ConnectionStateEvent> getConnectionHistory() {
|
||||||
|
return new ArrayList<>(connectionHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last connection state event.
|
||||||
|
*
|
||||||
|
* @return Last connection state event or empty if none
|
||||||
|
*/
|
||||||
|
public Optional<ConnectionStateEvent> getLastConnectionState() {
|
||||||
|
if (connectionHistory.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(connectionHistory.get(connectionHistory.size() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a plugin state listener.
|
||||||
|
*
|
||||||
|
* @param listener State listener
|
||||||
|
*/
|
||||||
|
public void addStateListener(PluginStateListener listener) {
|
||||||
|
stateListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a plugin state listener.
|
||||||
|
*
|
||||||
|
* @param listener State listener
|
||||||
|
*/
|
||||||
|
public void removeStateListener(PluginStateListener listener) {
|
||||||
|
stateListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify all state listeners of a connection state change.
|
||||||
|
*
|
||||||
|
* @param event Connection state event
|
||||||
|
*/
|
||||||
|
private void notifyStateListeners(ConnectionStateEvent event) {
|
||||||
|
for (PluginStateListener listener : stateListeners) {
|
||||||
|
try {
|
||||||
|
listener.onConnectionStateChanged(event);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[PluginManager] Error in state listener: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown the plugin manager and active plugin.
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
log.info("[PluginManager] Shutting down plugin manager");
|
||||||
|
|
||||||
|
if (activePlugin != null) {
|
||||||
|
try {
|
||||||
|
activePlugin.exit();
|
||||||
|
log.info("[PluginManager] Active plugin shut down successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[PluginManager] Error shutting down active plugin: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
activePlugin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateListeners.clear();
|
||||||
|
connectionHistory.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface for plugin state changes.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface PluginStateListener {
|
||||||
|
/**
|
||||||
|
* Called when plugin connection state changes.
|
||||||
|
*
|
||||||
|
* @param event Connection state event
|
||||||
|
*/
|
||||||
|
void onConnectionStateChanged(ConnectionStateEvent event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about a messaging plugin.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PluginMetadata {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin name
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin version
|
||||||
|
*/
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin description
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin author/vendor
|
||||||
|
*/
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported features
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private List<String> supportedFeatures = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the plugin supports wildcards in topic patterns
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private boolean supportsWildcards = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the plugin supports retained messages
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private boolean supportsRetainedMessages = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the plugin supports QoS levels
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private boolean supportsQos = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum QoS level supported (0, 1, 2)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private int maxQosLevel = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a feature is supported.
|
||||||
|
*
|
||||||
|
* @param feature Feature name
|
||||||
|
* @return true if supported
|
||||||
|
*/
|
||||||
|
public boolean supportsFeature(String feature) {
|
||||||
|
return supportedFeatures.contains(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a supported feature.
|
||||||
|
*
|
||||||
|
* @param feature Feature name
|
||||||
|
*/
|
||||||
|
public void addSupportedFeature(String feature) {
|
||||||
|
if (!supportedFeatures.contains(feature)) {
|
||||||
|
supportedFeatures.add(feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a message received from a messaging plugin.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ReceivedMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic/channel the message was received on
|
||||||
|
*/
|
||||||
|
private String topic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message payload
|
||||||
|
*/
|
||||||
|
private byte[] payload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quality of Service level (if applicable)
|
||||||
|
*/
|
||||||
|
private int qos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the message was retained
|
||||||
|
*/
|
||||||
|
private boolean retained;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when message was received
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private LocalDateTime receivedAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional metadata from the transport
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private Map<String, Object> metadata = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get metadata value.
|
||||||
|
*
|
||||||
|
* @param key Metadata key
|
||||||
|
* @return Metadata value or null
|
||||||
|
*/
|
||||||
|
public Object getMetadata(String key) {
|
||||||
|
return metadata.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set metadata value.
|
||||||
|
*
|
||||||
|
* @param key Metadata key
|
||||||
|
* @param value Metadata value
|
||||||
|
*/
|
||||||
|
public void setMetadata(String key, Object value) {
|
||||||
|
metadata.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get payload as UTF-8 string.
|
||||||
|
*
|
||||||
|
* @return Payload as string
|
||||||
|
*/
|
||||||
|
public String getPayloadAsString() {
|
||||||
|
return payload != null ? new String(payload, java.nio.charset.StandardCharsets.UTF_8) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for sending messages via plugins.
|
||||||
|
* Provides transport-agnostic options with extensibility for plugin-specific settings.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SendOptions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quality of Service level (0, 1, 2 for MQTT-like transports)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private int qos = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the message should be retained by the broker/server
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private boolean retained = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message priority (if supported by transport)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private int priority = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message expiry time in seconds (if supported by transport)
|
||||||
|
*/
|
||||||
|
private Long expirySeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional plugin-specific options
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private Map<String, Object> additionalOptions = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an additional option.
|
||||||
|
*
|
||||||
|
* @param key Option key
|
||||||
|
* @return Option value or null
|
||||||
|
*/
|
||||||
|
public Object getAdditionalOption(String key) {
|
||||||
|
return additionalOptions.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an additional option.
|
||||||
|
*
|
||||||
|
* @param key Option key
|
||||||
|
* @param value Option value
|
||||||
|
*/
|
||||||
|
public void setAdditionalOption(String key, Object value) {
|
||||||
|
additionalOptions.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create default send options.
|
||||||
|
*
|
||||||
|
* @return Default options
|
||||||
|
*/
|
||||||
|
public static SendOptions defaults() {
|
||||||
|
return SendOptions.builder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create options for fire-and-forget messages.
|
||||||
|
*
|
||||||
|
* @return Fire-and-forget options
|
||||||
|
*/
|
||||||
|
public static SendOptions fireAndForget() {
|
||||||
|
return SendOptions.builder()
|
||||||
|
.qos(0)
|
||||||
|
.retained(false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create options for reliable delivery.
|
||||||
|
*
|
||||||
|
* @return Reliable delivery options
|
||||||
|
*/
|
||||||
|
public static SendOptions reliable() {
|
||||||
|
return SendOptions.builder()
|
||||||
|
.qos(2)
|
||||||
|
.retained(false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
package de.assecutor.votianlt.messaging.plugin.mqtt;
|
||||||
|
|
||||||
|
import com.hivemq.client.mqtt.MqttClient;
|
||||||
|
import com.hivemq.client.mqtt.datatypes.MqttQos;
|
||||||
|
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
|
||||||
|
import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish;
|
||||||
|
import de.assecutor.votianlt.messaging.plugin.*;
|
||||||
|
import de.assecutor.votianlt.messaging.plugin.ConnectionStateEvent.ConnectionState;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MQTT implementation of the MessagingPlugin interface.
|
||||||
|
* Uses HiveMQ MQTT 5 client for communication.
|
||||||
|
*
|
||||||
|
* Topic Structure (managed internally):
|
||||||
|
* - Server -> Client: /client/{clientId}/{messageType}
|
||||||
|
* - Client -> Server: /server/{clientId}/{messageType}
|
||||||
|
* - ACK Server -> Client: /ack/client/{clientId}/{messageId}
|
||||||
|
* - ACK Client -> Server: /ack/server/{messageId}
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MqttMessagingPlugin implements MessagingPlugin {
|
||||||
|
|
||||||
|
private static final String PLUGIN_NAME = "mqtt";
|
||||||
|
private static final String PLUGIN_VERSION = "2.0.0";
|
||||||
|
|
||||||
|
// Topic templates
|
||||||
|
private static final String TOPIC_TO_CLIENT = "/client/%s/%s"; // /client/{clientId}/{messageType}
|
||||||
|
private static final String TOPIC_FROM_CLIENT = "/server/%s/%s"; // /server/{clientId}/{messageType}
|
||||||
|
private static final String TOPIC_ACK_TO_CLIENT = "/ack/client/%s/%s"; // /ack/client/{clientId}/{messageId}
|
||||||
|
private static final String TOPIC_ACK_FROM_CLIENT = "/ack/server/%s"; // /ack/server/{messageId}
|
||||||
|
|
||||||
|
// Subscription patterns
|
||||||
|
private static final String PATTERN_FROM_CLIENT = "/server/+/%s"; // /server/+/{messageType}
|
||||||
|
private static final String PATTERN_ACK_FROM_CLIENT = "/ack/server/+"; // /ack/server/+
|
||||||
|
|
||||||
|
private Mqtt5AsyncClient mqttClient;
|
||||||
|
private ConnectionStateListener connectionListener;
|
||||||
|
private final Map<String, ClientMessageHandler> messageHandlers = new ConcurrentHashMap<>();
|
||||||
|
private AckHandler ackHandler;
|
||||||
|
private volatile boolean connected = false;
|
||||||
|
|
||||||
|
// Configuration keys
|
||||||
|
private static final String CONFIG_BROKER_HOST = "broker.host";
|
||||||
|
private static final String CONFIG_BROKER_PORT = "broker.port";
|
||||||
|
private static final String CONFIG_USERNAME = "username";
|
||||||
|
private static final String CONFIG_PASSWORD = "password";
|
||||||
|
private static final String CONFIG_CLIENT_ID = "client.id";
|
||||||
|
private static final String CONFIG_AUTO_RECONNECT = "auto.reconnect";
|
||||||
|
private static final String CONFIG_CLEAN_START = "clean.start";
|
||||||
|
private static final String CONFIG_CONNECTION_TIMEOUT = "connection.timeout.seconds";
|
||||||
|
private static final String CONFIG_KEEP_ALIVE = "keep.alive.seconds";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(PluginConfig config) throws PluginException {
|
||||||
|
log.info("[MqttPlugin] Initializing MQTT plugin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
notifyConnectionState(ConnectionState.INITIALIZING, null);
|
||||||
|
|
||||||
|
// Extract configuration
|
||||||
|
String brokerHost = config.getString(CONFIG_BROKER_HOST, "localhost");
|
||||||
|
int brokerPort = config.getInt(CONFIG_BROKER_PORT, 1883);
|
||||||
|
String username = config.getString(CONFIG_USERNAME);
|
||||||
|
String password = config.getString(CONFIG_PASSWORD);
|
||||||
|
String clientId = config.getString(CONFIG_CLIENT_ID, "votianlt-" + UUID.randomUUID());
|
||||||
|
boolean cleanStart = config.getBoolean(CONFIG_CLEAN_START, true);
|
||||||
|
int connectionTimeout = config.getInt(CONFIG_CONNECTION_TIMEOUT, 60);
|
||||||
|
int keepAlive = config.getInt(CONFIG_KEEP_ALIVE, 60);
|
||||||
|
|
||||||
|
log.info("[MqttPlugin] Connecting to {}:{} with clientId: {} (timeout: {}s, keepAlive: {}s)",
|
||||||
|
brokerHost, brokerPort, clientId, connectionTimeout, keepAlive);
|
||||||
|
|
||||||
|
// Build MQTT client
|
||||||
|
var clientBuilder = MqttClient.builder()
|
||||||
|
.useMqttVersion5()
|
||||||
|
.identifier(clientId)
|
||||||
|
.serverHost(brokerHost)
|
||||||
|
.serverPort(brokerPort)
|
||||||
|
.automaticReconnect()
|
||||||
|
.initialDelay(1, java.util.concurrent.TimeUnit.SECONDS)
|
||||||
|
.maxDelay(30, java.util.concurrent.TimeUnit.SECONDS)
|
||||||
|
.applyAutomaticReconnect();
|
||||||
|
|
||||||
|
mqttClient = clientBuilder.buildAsync();
|
||||||
|
|
||||||
|
// Build connect options
|
||||||
|
var connectBuilder = com.hivemq.client.mqtt.mqtt5.message.connect.Mqtt5Connect.builder()
|
||||||
|
.cleanStart(cleanStart)
|
||||||
|
.keepAlive(keepAlive);
|
||||||
|
|
||||||
|
if (username != null && password != null) {
|
||||||
|
connectBuilder.simpleAuth()
|
||||||
|
.username(username)
|
||||||
|
.password(password.getBytes(StandardCharsets.UTF_8))
|
||||||
|
.applySimpleAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect asynchronously
|
||||||
|
notifyConnectionState(ConnectionState.CONNECTING, null);
|
||||||
|
|
||||||
|
log.info("[MqttPlugin] Starting async connection to {}:{}", brokerHost, brokerPort);
|
||||||
|
|
||||||
|
mqttClient.connect(connectBuilder.build())
|
||||||
|
.orTimeout(connectionTimeout, java.util.concurrent.TimeUnit.SECONDS)
|
||||||
|
.whenComplete((connAck, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
String errorMsg = String.format("Connection to %s:%d failed: %s",
|
||||||
|
brokerHost, brokerPort, throwable.getMessage());
|
||||||
|
log.error("[MqttPlugin] {}", errorMsg, throwable);
|
||||||
|
|
||||||
|
// Check for specific error types
|
||||||
|
if (throwable instanceof java.util.concurrent.TimeoutException) {
|
||||||
|
log.error("[MqttPlugin] Connection timeout - broker may be unreachable or firewall blocking connection");
|
||||||
|
} else if (throwable.getCause() instanceof java.net.UnknownHostException) {
|
||||||
|
log.error("[MqttPlugin] Unknown host - DNS resolution failed for {}", brokerHost);
|
||||||
|
} else if (throwable.getCause() instanceof java.net.ConnectException) {
|
||||||
|
log.error("[MqttPlugin] Connection refused - broker may be down or port {} is blocked", brokerPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
connected = false;
|
||||||
|
notifyConnectionState(ConnectionState.ERROR, errorMsg);
|
||||||
|
} else {
|
||||||
|
log.info("[MqttPlugin] Connected successfully - connAck: {}", connAck);
|
||||||
|
connected = true;
|
||||||
|
setupGlobalMessageHandler();
|
||||||
|
log.info("[MqttPlugin] Notifying CONNECTED state");
|
||||||
|
notifyConnectionState(ConnectionState.CONNECTED, null);
|
||||||
|
log.info("[MqttPlugin] CONNECTED state notification sent");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("[MqttPlugin] Initialization complete - connection in progress");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MqttPlugin] Initialization failed: {}", e.getMessage(), e);
|
||||||
|
throw new PluginException("Failed to initialize MQTT plugin", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exit() throws PluginException {
|
||||||
|
log.info("[MqttPlugin] Shutting down MQTT plugin");
|
||||||
|
|
||||||
|
try {
|
||||||
|
notifyConnectionState(ConnectionState.DISCONNECTING, null);
|
||||||
|
|
||||||
|
if (mqttClient != null && connected) {
|
||||||
|
mqttClient.disconnect().join();
|
||||||
|
log.info("[MqttPlugin] Disconnected successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
connected = false;
|
||||||
|
messageHandlers.clear();
|
||||||
|
ackHandler = null;
|
||||||
|
notifyConnectionState(ConnectionState.DISCONNECTED, null);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MqttPlugin] Shutdown failed: {}", e.getMessage(), e);
|
||||||
|
throw new PluginException("Failed to shutdown MQTT plugin", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setConnectionListener(ConnectionStateListener listener) {
|
||||||
|
this.connectionListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> sendToClient(String clientId, String messageType, byte[] payload, SendOptions options) throws PluginException {
|
||||||
|
if (!connected) {
|
||||||
|
return CompletableFuture.failedFuture(new PluginException("MQTT client is not connected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String topic = String.format(TOPIC_TO_CLIENT, clientId, messageType);
|
||||||
|
log.debug("[MqttPlugin] Sending to client {} (type: {}) on topic: {}", clientId, messageType, topic);
|
||||||
|
|
||||||
|
return sendToTopic(topic, payload, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> sendAckToClient(String clientId, String messageId, byte[] payload, SendOptions options) throws PluginException {
|
||||||
|
if (!connected) {
|
||||||
|
return CompletableFuture.failedFuture(new PluginException("MQTT client is not connected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String topic = String.format(TOPIC_ACK_TO_CLIENT, clientId, messageId);
|
||||||
|
log.debug("[MqttPlugin] Sending ACK to client {} for message {} on topic: {}", clientId, messageId, topic);
|
||||||
|
|
||||||
|
return sendToTopic(topic, payload, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerMessageHandler(String messageType, ClientMessageHandler handler) throws PluginException {
|
||||||
|
if (!connected) {
|
||||||
|
throw new PluginException("MQTT client is not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
String topicPattern = String.format(PATTERN_FROM_CLIENT, messageType);
|
||||||
|
log.info("[MqttPlugin] Registering handler for message type '{}' with pattern: {}", messageType, topicPattern);
|
||||||
|
|
||||||
|
messageHandlers.put(messageType, handler);
|
||||||
|
|
||||||
|
// Subscribe to the topic pattern
|
||||||
|
mqttClient.subscribeWith()
|
||||||
|
.topicFilter(topicPattern)
|
||||||
|
.qos(MqttQos.EXACTLY_ONCE)
|
||||||
|
.send()
|
||||||
|
.whenComplete((subAck, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
log.error("[MqttPlugin] Subscription to {} failed: {}", topicPattern, throwable.getMessage());
|
||||||
|
messageHandlers.remove(messageType);
|
||||||
|
} else {
|
||||||
|
log.info("[MqttPlugin] Successfully subscribed to: {}", topicPattern);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerAckHandler(AckHandler handler) throws PluginException {
|
||||||
|
if (!connected) {
|
||||||
|
throw new PluginException("MQTT client is not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[MqttPlugin] Registering ACK handler with pattern: {}", PATTERN_ACK_FROM_CLIENT);
|
||||||
|
|
||||||
|
this.ackHandler = handler;
|
||||||
|
|
||||||
|
// Subscribe to ACK topic pattern
|
||||||
|
mqttClient.subscribeWith()
|
||||||
|
.topicFilter(PATTERN_ACK_FROM_CLIENT)
|
||||||
|
.qos(MqttQos.EXACTLY_ONCE)
|
||||||
|
.send()
|
||||||
|
.whenComplete((subAck, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
log.error("[MqttPlugin] Subscription to {} failed: {}", PATTERN_ACK_FROM_CLIENT, throwable.getMessage());
|
||||||
|
this.ackHandler = null;
|
||||||
|
} else {
|
||||||
|
log.info("[MqttPlugin] Successfully subscribed to: {}", PATTERN_ACK_FROM_CLIENT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConnected() {
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPluginName() {
|
||||||
|
return PLUGIN_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPluginVersion() {
|
||||||
|
return PLUGIN_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginMetadata getMetadata() {
|
||||||
|
return PluginMetadata.builder()
|
||||||
|
.name(PLUGIN_NAME)
|
||||||
|
.version(PLUGIN_VERSION)
|
||||||
|
.description("MQTT v5 messaging plugin using HiveMQ client")
|
||||||
|
.supportsWildcards(true)
|
||||||
|
.supportsRetainedMessages(true)
|
||||||
|
.supportsQos(true)
|
||||||
|
.maxQosLevel(2)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup global message handler to route incoming messages to registered handlers.
|
||||||
|
*/
|
||||||
|
private void setupGlobalMessageHandler() {
|
||||||
|
mqttClient.publishes(com.hivemq.client.mqtt.MqttGlobalPublishFilter.ALL, publish -> {
|
||||||
|
handleIncomingMessage(publish);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("[MqttPlugin] Global message handler configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming MQTT message and route to appropriate handler.
|
||||||
|
*/
|
||||||
|
private void handleIncomingMessage(Mqtt5Publish publish) {
|
||||||
|
String topic = publish.getTopic().toString();
|
||||||
|
byte[] payload = publish.getPayloadAsBytes();
|
||||||
|
|
||||||
|
log.debug("[MqttPlugin] Received message on topic: {}", topic);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if it's an ACK message
|
||||||
|
if (topic.startsWith("/ack/server/")) {
|
||||||
|
handleAckMessage(topic, payload);
|
||||||
|
}
|
||||||
|
// Check if it's a client message
|
||||||
|
else if (topic.startsWith("/server/")) {
|
||||||
|
handleClientMessage(topic, payload);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn("[MqttPlugin] Received message on unexpected topic: {}", topic);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MqttPlugin] Error handling message on topic {}: {}", topic, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle ACK message from client.
|
||||||
|
* Topic format: /ack/server/{messageId}
|
||||||
|
*/
|
||||||
|
private void handleAckMessage(String topic, byte[] payload) {
|
||||||
|
if (ackHandler == null) {
|
||||||
|
log.warn("[MqttPlugin] Received ACK but no handler registered: {}", topic);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract messageId from topic
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
String messageId = parts[3];
|
||||||
|
log.debug("[MqttPlugin] Routing ACK for message: {}", messageId);
|
||||||
|
ackHandler.onAckReceived(messageId, payload);
|
||||||
|
} else {
|
||||||
|
log.warn("[MqttPlugin] Invalid ACK topic format: {}", topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle client message.
|
||||||
|
* Topic format: /server/{clientId}/{messageType}
|
||||||
|
*/
|
||||||
|
private void handleClientMessage(String topic, byte[] payload) {
|
||||||
|
// Extract clientId and messageType from topic
|
||||||
|
String[] parts = topic.split("/");
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
String clientId = parts[2];
|
||||||
|
String messageType = parts[3];
|
||||||
|
|
||||||
|
ClientMessageHandler handler = messageHandlers.get(messageType);
|
||||||
|
if (handler != null) {
|
||||||
|
log.debug("[MqttPlugin] Routing message from client {} (type: {})", clientId, messageType);
|
||||||
|
handler.onMessageReceived(clientId, payload);
|
||||||
|
} else {
|
||||||
|
log.warn("[MqttPlugin] No handler registered for message type: {}", messageType);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("[MqttPlugin] Invalid client message topic format: {}", topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to a specific MQTT topic.
|
||||||
|
*/
|
||||||
|
private CompletableFuture<Void> sendToTopic(String topic, byte[] payload, SendOptions options) {
|
||||||
|
try {
|
||||||
|
var publishBuilder = Mqtt5Publish.builder()
|
||||||
|
.topic(topic)
|
||||||
|
.payload(payload)
|
||||||
|
.qos(mapQos(options.getQos()))
|
||||||
|
.retain(options.isRetained());
|
||||||
|
|
||||||
|
return mqttClient.publish(publishBuilder.build())
|
||||||
|
.thenApply(publishResult -> {
|
||||||
|
log.debug("[MqttPlugin] Message published to topic: {}", topic);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MqttPlugin] Failed to publish to topic {}: {}", topic, e.getMessage(), e);
|
||||||
|
return CompletableFuture.failedFuture(new PluginException("Failed to publish message", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map QoS level to MQTT QoS.
|
||||||
|
*/
|
||||||
|
private MqttQos mapQos(int qos) {
|
||||||
|
return switch (qos) {
|
||||||
|
case 0 -> MqttQos.AT_MOST_ONCE;
|
||||||
|
case 1 -> MqttQos.AT_LEAST_ONCE;
|
||||||
|
case 2 -> MqttQos.EXACTLY_ONCE;
|
||||||
|
default -> MqttQos.AT_LEAST_ONCE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify connection state listener.
|
||||||
|
*/
|
||||||
|
private void notifyConnectionState(ConnectionState state, String message) {
|
||||||
|
log.debug("[MqttPlugin] notifyConnectionState called: state={}, listener={}", state, connectionListener != null ? "present" : "null");
|
||||||
|
if (connectionListener != null) {
|
||||||
|
ConnectionStateEvent event = ConnectionStateEvent.builder()
|
||||||
|
.state(state)
|
||||||
|
.previousState(null)
|
||||||
|
.errorMessage(message)
|
||||||
|
.pluginName(PLUGIN_NAME)
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
log.debug("[MqttPlugin] Calling connectionListener.onConnectionStateChanged");
|
||||||
|
connectionListener.onConnectionStateChanged(event);
|
||||||
|
log.debug("[MqttPlugin] connectionListener.onConnectionStateChanged completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MqttPlugin] Error in connection listener: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("[MqttPlugin] Connection listener is null, cannot notify state: {}", state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package de.assecutor.votianlt.mqtt;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kept for compatibility: The actual MQTT v5 lifecycle is managed by
|
|
||||||
* MqttV5ClientManager. This runner only logs application readiness.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class MqttClientRunner {
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void onApplicationReady() {
|
|
||||||
log.info("Application ready. MQTT v5 client lifecycle managed by MqttV5ClientManager.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,8 @@ package de.assecutor.votianlt.mqtt;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import de.assecutor.votianlt.messaging.delivery.MessageDeliveryService;
|
||||||
|
import de.assecutor.votianlt.messaging.model.DeliveryOptions;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
@@ -9,6 +11,9 @@ import org.springframework.context.annotation.Lazy;
|
|||||||
/**
|
/**
|
||||||
* Simple MQTT publishing helper to send JSON payloads.
|
* Simple MQTT publishing helper to send JSON payloads.
|
||||||
*
|
*
|
||||||
|
* This implementation now uses MessageDeliveryService for reliable delivery
|
||||||
|
* with acknowledgment tracking and retry mechanism.
|
||||||
|
*
|
||||||
* Note: In environments where Spring Integration MQTT is unavailable (e.g.,
|
* Note: In environments where Spring Integration MQTT is unavailable (e.g.,
|
||||||
* offline CI), this implementation degrades to a no-op publisher that logs the
|
* offline CI), this implementation degrades to a no-op publisher that logs the
|
||||||
* intended message.
|
* intended message.
|
||||||
@@ -24,10 +29,10 @@ public interface MqttPublisher {
|
|||||||
class MqttPublisherImpl implements MqttPublisher {
|
class MqttPublisherImpl implements MqttPublisher {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final MqttV5ClientManager clientManager;
|
private final MessageDeliveryService deliveryService;
|
||||||
|
|
||||||
public MqttPublisherImpl(@Lazy MqttV5ClientManager clientManager) {
|
public MqttPublisherImpl(@Lazy MessageDeliveryService deliveryService) {
|
||||||
this.clientManager = clientManager;
|
this.deliveryService = deliveryService;
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper();
|
||||||
this.objectMapper.registerModule(new JavaTimeModule());
|
this.objectMapper.registerModule(new JavaTimeModule());
|
||||||
}
|
}
|
||||||
@@ -40,20 +45,37 @@ class MqttPublisherImpl implements MqttPublisher {
|
|||||||
@Override
|
@Override
|
||||||
public void publishAsJson(String topic, Object payload, boolean retained) {
|
public void publishAsJson(String topic, Object payload, boolean retained) {
|
||||||
try {
|
try {
|
||||||
String json = (payload instanceof String s) ? s : objectMapper.writeValueAsString(payload);
|
// Use MessageDeliveryService for reliable delivery
|
||||||
byte[] bytes = json.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
DeliveryOptions options = DeliveryOptions.builder()
|
||||||
// Default QoS 2
|
.requiresAck(true)
|
||||||
clientManager.publish(topic, bytes, 2, retained);
|
.retained(retained)
|
||||||
|
.build();
|
||||||
|
|
||||||
// Log all published JSON documents
|
deliveryService.sendMessage(topic, payload, options)
|
||||||
log.info("=== MQTT JSON PUBLISHED ===");
|
.thenAccept(receipt -> {
|
||||||
|
log.info("=== MESSAGE DELIVERY SUBMITTED ===");
|
||||||
log.info("Topic: {}", topic);
|
log.info("Topic: {}", topic);
|
||||||
|
log.info("Message ID: {}", receipt.getMessageId());
|
||||||
|
log.info("Status: {}", receipt.getStatus());
|
||||||
log.info("Retained: {}", retained);
|
log.info("Retained: {}", retained);
|
||||||
log.info("JSON Data: {}", json);
|
|
||||||
log.info("=== END MQTT PUBLISH ===");
|
// Log payload for debugging
|
||||||
|
try {
|
||||||
|
String json = (payload instanceof String s) ? s : objectMapper.writeValueAsString(payload);
|
||||||
|
log.info("Payload: {}", json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Could not serialize payload for logging: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("=== END MESSAGE DELIVERY ===");
|
||||||
|
})
|
||||||
|
.exceptionally(ex -> {
|
||||||
|
log.error("Failed to submit message for delivery to topic {}: {}", topic, ex.getMessage(), ex);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to serialize/publish MQTT message for topic {}: {}", topic, e.getMessage(), e);
|
log.error("Failed to publish message for topic {}: {}", topic, e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,288 +0,0 @@
|
|||||||
package de.assecutor.votianlt.mqtt;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.hivemq.client.mqtt.MqttGlobalPublishFilter;
|
|
||||||
import com.hivemq.client.mqtt.datatypes.MqttQos;
|
|
||||||
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
|
|
||||||
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
|
|
||||||
import de.assecutor.votianlt.config.MqttProperties;
|
|
||||||
import de.assecutor.votianlt.controller.MessageController;
|
|
||||||
import de.assecutor.votianlt.model.PendingMqttMessage;
|
|
||||||
import de.assecutor.votianlt.repository.PendingMqttMessageRepository;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.SmartLifecycle;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages a single MQTT v5 client connection with Spring lifecycle using HiveMQ
|
|
||||||
* MQTT Client.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@Slf4j
|
|
||||||
public class MqttV5ClientManager implements SmartLifecycle {
|
|
||||||
|
|
||||||
private final MqttProperties props;
|
|
||||||
private final MessageController messageController;
|
|
||||||
private final PendingMqttMessageRepository pendingMessageRepository;
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
private volatile boolean running = false;
|
|
||||||
private Mqtt5AsyncClient client;
|
|
||||||
|
|
||||||
public MqttV5ClientManager(MqttProperties props, @Lazy MessageController messageController, PendingMqttMessageRepository pendingMessageRepository) {
|
|
||||||
this.props = props;
|
|
||||||
this.messageController = messageController;
|
|
||||||
this.pendingMessageRepository = pendingMessageRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
if (!props.isEnabled()) {
|
|
||||||
log.warn("MQTT is disabled via app.mqtt.enabled=false");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String clientId = buildClientId(props.getClientId());
|
|
||||||
URI uri = URI.create(props.getBrokerUri());
|
|
||||||
String host = uri.getHost();
|
|
||||||
int port = 42099;
|
|
||||||
|
|
||||||
var builder = Mqtt5Client.builder().identifier(clientId).serverHost(host).serverPort(port);
|
|
||||||
if (props.isAutomaticReconnect()) {
|
|
||||||
builder = builder.automaticReconnectWithDefaultConfig();
|
|
||||||
}
|
|
||||||
client = builder.buildAsync();
|
|
||||||
|
|
||||||
var connect = client.connectWith().cleanStart(props.isCleanStart()).keepAlive(props.getKeepAlive())
|
|
||||||
.sessionExpiryInterval(props.getSessionExpiryInterval()).simpleAuth()
|
|
||||||
.username("app")
|
|
||||||
.password("apppwd".getBytes(StandardCharsets.UTF_8)).applySimpleAuth();
|
|
||||||
|
|
||||||
log.info("[MQTT] Connecting to {} with clientId={} ...", props.getBrokerUri(), clientId);
|
|
||||||
connect.send().join();
|
|
||||||
log.info("[MQTT] Connected");
|
|
||||||
|
|
||||||
// Handle all incoming publishes
|
|
||||||
client.publishes(MqttGlobalPublishFilter.ALL, publish -> {
|
|
||||||
String topic = publish.getTopic().toString();
|
|
||||||
byte[] bytes;
|
|
||||||
try {
|
|
||||||
ByteBuffer buf = publish.getPayload().orElse(null);
|
|
||||||
bytes = buf != null ? toByteArray(buf) : new byte[0];
|
|
||||||
} catch (Throwable t) {
|
|
||||||
bytes = new byte[0];
|
|
||||||
}
|
|
||||||
handleInbound(topic, bytes);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to topics with QoS
|
|
||||||
String[] topics = new String[] { "/server/+/task/photo/completed", "/server/+/task/confirm",
|
|
||||||
"/server/+/task/completed", "/server/+/task_completed", "/server/+/job/status",
|
|
||||||
"/server/+/jobs/assigned", "/server/+/message", "/server/login" };
|
|
||||||
MqttQos qos = mapQos(props.getDefaultQos());
|
|
||||||
for (String topic : topics) {
|
|
||||||
client.subscribeWith().topicFilter(topic).qos(qos).send().join();
|
|
||||||
}
|
|
||||||
running = true;
|
|
||||||
log.info("[MQTT] Subscribed to {} topics (QoS={}), awaiting messages ...", topics.length, qos);
|
|
||||||
|
|
||||||
// Process pending messages after successful connection
|
|
||||||
processPendingMessages();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to start HiveMQ MQTT client: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] toByteArray(ByteBuffer buffer) {
|
|
||||||
byte[] bytes = new byte[buffer.remaining()];
|
|
||||||
buffer.get(bytes);
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildClientId(String base) {
|
|
||||||
String b = (base == null || base.isBlank()) ? "server" : base;
|
|
||||||
if (!b.contains("${random.uuid}")) {
|
|
||||||
return b + "-" + UUID.randomUUID();
|
|
||||||
}
|
|
||||||
return b.replace("${random.uuid}", UUID.randomUUID().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInbound(String topic, byte[] payload) {
|
|
||||||
String json = new String(payload, StandardCharsets.UTF_8);
|
|
||||||
try {
|
|
||||||
Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
|
|
||||||
});
|
|
||||||
routeInbound(topic, map);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("Failed to parse inbound MQTT JSON on {}: {}", topic, ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void routeInbound(String topic, Map<String, Object> payload) {
|
|
||||||
try {
|
|
||||||
// The consolidated topic /server/{clientId}/task_completed is used by apps to
|
|
||||||
// report completion of any task type. Only PHOTO and CONFIRMATION require
|
|
||||||
// specialized processing on the server side. All other task types are handled
|
|
||||||
// by the
|
|
||||||
// generic handler handleTaskCompleted(). This keeps routing simple while
|
|
||||||
// allowing
|
|
||||||
// special logic (e.g., photo persistence) where necessary.
|
|
||||||
if (topic.matches("/server/.+/task_completed")) {
|
|
||||||
try {
|
|
||||||
Object tt = payload.get("taskType");
|
|
||||||
String taskType = tt != null ? tt.toString() : null;
|
|
||||||
messageController.handleTaskCompleted(payload, taskType);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error routing task_completed by taskType: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
} else if (topic.matches("/server/.+/jobs/assigned")) {
|
|
||||||
try {
|
|
||||||
// Extract clientId from topic: /server/{clientId}/jobs/assigned
|
|
||||||
String[] parts = topic.split("/");
|
|
||||||
String clientId = parts.length > 2 ? parts[2] : null;
|
|
||||||
if (clientId != null && !clientId.isBlank()) {
|
|
||||||
payload.put("clientId", clientId);
|
|
||||||
} else {
|
|
||||||
log.warn("Couldn't extract clientId from topic {} for jobs/assigned", topic);
|
|
||||||
}
|
|
||||||
messageController.handleGetAssignedJobs(payload);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error handling jobs/assigned on {}: {}", topic, e.getMessage(), e);
|
|
||||||
}
|
|
||||||
} else if (topic.equals("/server/login")) {
|
|
||||||
var om = new ObjectMapper();
|
|
||||||
de.assecutor.votianlt.dto.AppLoginRequest req = om.convertValue(payload,
|
|
||||||
de.assecutor.votianlt.dto.AppLoginRequest.class);
|
|
||||||
messageController.handleAppLogin(req);
|
|
||||||
} else if (topic.matches("/server/.+/message")) {
|
|
||||||
try {
|
|
||||||
// Extract clientId from topic: /server/{clientId}/message
|
|
||||||
String[] parts = topic.split("/");
|
|
||||||
String clientId = parts.length > 2 ? parts[2] : null;
|
|
||||||
if (clientId != null && !clientId.isBlank()) {
|
|
||||||
payload.put("clientId", clientId);
|
|
||||||
} else {
|
|
||||||
log.warn("Couldn't extract clientId from topic {} for message", topic);
|
|
||||||
}
|
|
||||||
messageController.handleIncomingMessage(payload);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error handling incoming message on {}: {}", topic, e.getMessage(), e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.debug("No route for topic {}", topic);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error routing inbound MQTT message on {}: {}", topic, e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
try {
|
|
||||||
if (client != null) {
|
|
||||||
client.disconnect().join();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Error during MQTT client shutdown: {}", e.getMessage());
|
|
||||||
} finally {
|
|
||||||
running = false;
|
|
||||||
client = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRunning() {
|
|
||||||
return running;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MqttQos mapQos(int q) {
|
|
||||||
return switch (q) {
|
|
||||||
case 0 -> MqttQos.AT_MOST_ONCE;
|
|
||||||
case 1 -> MqttQos.AT_LEAST_ONCE;
|
|
||||||
default -> MqttQos.EXACTLY_ONCE;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void publish(String topic, byte[] payload, int qos, boolean retained) {
|
|
||||||
try {
|
|
||||||
if (client == null || !running) {
|
|
||||||
log.warn("[MQTT] Not connected, saving message for later: topic={}", topic);
|
|
||||||
savePendingMessage(topic, payload, qos, retained);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send()
|
|
||||||
.whenComplete((ack, ex) -> {
|
|
||||||
if (ex != null) {
|
|
||||||
log.error("Failed to publish to {}: {}, saving for retry", topic, ex.getMessage(), ex);
|
|
||||||
savePendingMessage(topic, payload, qos, retained);
|
|
||||||
} else {
|
|
||||||
log.debug("Successfully published to topic: {}", topic);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to publish to {}: {}, saving for retry", topic, e.getMessage(), e);
|
|
||||||
savePendingMessage(topic, payload, qos, retained);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void savePendingMessage(String topic, byte[] payload, int qos, boolean retained) {
|
|
||||||
try {
|
|
||||||
PendingMqttMessage pendingMessage = new PendingMqttMessage(topic, payload, qos, retained);
|
|
||||||
pendingMessageRepository.save(pendingMessage);
|
|
||||||
log.info("[MQTT] Saved pending message for topic: {}", topic);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to save pending MQTT message: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processPendingMessages() {
|
|
||||||
try {
|
|
||||||
List<PendingMqttMessage> pendingMessages = pendingMessageRepository.findAllByOrderByCreatedAtAsc();
|
|
||||||
if (pendingMessages.isEmpty()) {
|
|
||||||
log.debug("[MQTT] No pending messages to process");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("[MQTT] Processing {} pending messages", pendingMessages.size());
|
|
||||||
for (PendingMqttMessage pendingMessage : pendingMessages) {
|
|
||||||
try {
|
|
||||||
// Attempt to send the pending message
|
|
||||||
client.publishWith()
|
|
||||||
.topic(pendingMessage.getTopic())
|
|
||||||
.payload(pendingMessage.getPayload())
|
|
||||||
.qos(mapQos(pendingMessage.getQos()))
|
|
||||||
.retain(pendingMessage.isRetained())
|
|
||||||
.send()
|
|
||||||
.whenComplete((ack, ex) -> {
|
|
||||||
if (ex != null) {
|
|
||||||
log.warn("Failed to resend pending message to {}: {}", pendingMessage.getTopic(), ex.getMessage());
|
|
||||||
// Update retry count
|
|
||||||
pendingMessage.incrementRetryCount();
|
|
||||||
pendingMessageRepository.save(pendingMessage);
|
|
||||||
} else {
|
|
||||||
// Successfully sent, remove from pending
|
|
||||||
log.info("Successfully resent pending message to topic: {}", pendingMessage.getTopic());
|
|
||||||
pendingMessageRepository.delete(pendingMessage);
|
|
||||||
}
|
|
||||||
}).join(); // Wait for completion to process sequentially
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Error processing pending message for {}: {}", pendingMessage.getTopic(), e.getMessage());
|
|
||||||
pendingMessage.incrementRetryCount();
|
|
||||||
pendingMessageRepository.save(pendingMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error processing pending MQTT messages: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package de.assecutor.votianlt.repository;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.messaging.model.MessageEnvelope;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for MessageEnvelope entities.
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface MessageEnvelopeRepository extends MongoRepository<MessageEnvelope, ObjectId> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find envelope by message ID
|
||||||
|
*/
|
||||||
|
Optional<MessageEnvelope> findByMessageId(String messageId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all envelopes for a specific topic
|
||||||
|
*/
|
||||||
|
List<MessageEnvelope> findByTopic(String topic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find expired envelopes
|
||||||
|
*/
|
||||||
|
List<MessageEnvelope> findByExpiresAtBefore(LocalDateTime dateTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find envelopes created after a specific time
|
||||||
|
*/
|
||||||
|
List<MessageEnvelope> findByTimestampAfter(LocalDateTime dateTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete envelopes older than specified time
|
||||||
|
*/
|
||||||
|
void deleteByTimestampBefore(LocalDateTime dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package de.assecutor.votianlt.repository;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.messaging.model.DeliveryStatus;
|
||||||
|
import de.assecutor.votianlt.messaging.model.PendingDelivery;
|
||||||
|
import org.bson.types.ObjectId;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for PendingDelivery entities.
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface PendingDeliveryRepository extends MongoRepository<PendingDelivery, ObjectId> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find pending delivery by message ID
|
||||||
|
*/
|
||||||
|
Optional<PendingDelivery> findByMessageId(String messageId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all deliveries with a specific status
|
||||||
|
*/
|
||||||
|
List<PendingDelivery> findByStatus(DeliveryStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find deliveries ready for retry (status = SENT and nextRetryAt is in the past)
|
||||||
|
*/
|
||||||
|
List<PendingDelivery> findByStatusAndNextRetryAtBefore(DeliveryStatus status, LocalDateTime dateTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find acknowledged deliveries older than specified time
|
||||||
|
*/
|
||||||
|
List<PendingDelivery> findByStatusAndAcknowledgedAtBefore(DeliveryStatus status, LocalDateTime dateTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find deliveries with specific statuses that have expired
|
||||||
|
*/
|
||||||
|
List<PendingDelivery> findByStatusInAndExpiresAtBefore(List<DeliveryStatus> statuses, LocalDateTime dateTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all deliveries for a specific client
|
||||||
|
*/
|
||||||
|
List<PendingDelivery> findByClientId(String clientId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all deliveries for a specific topic
|
||||||
|
*/
|
||||||
|
List<PendingDelivery> findByTopic(String topic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count deliveries by status
|
||||||
|
*/
|
||||||
|
long countByStatus(DeliveryStatus status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete deliveries older than specified time
|
||||||
|
*/
|
||||||
|
void deleteByCreatedAtBefore(LocalDateTime dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -54,24 +54,27 @@ spring.servlet.multipart.max-request-size=64MB
|
|||||||
# Jackson message converter limits
|
# Jackson message converter limits
|
||||||
spring.jackson.default-property-inclusion=non_null
|
spring.jackson.default-property-inclusion=non_null
|
||||||
|
|
||||||
# MQTT v5 settings
|
|
||||||
app.mqtt.enabled=true
|
|
||||||
#app.mqtt.broker-uri=mqtt://192.168.180.26:1883
|
|
||||||
app.mqtt.broker-uri=mqtt://mqtt-2.assecutor.de
|
|
||||||
# The server MQTT clientId; a random UUID suffix will be inserted where ${random.uuid} appears
|
|
||||||
app.mqtt.client-id=server-${random.uuid}
|
|
||||||
# v5 session and keepalive
|
|
||||||
app.mqtt.clean-start=false
|
|
||||||
app.mqtt.session-expiry-interval=86400
|
|
||||||
app.mqtt.keep-alive=30
|
|
||||||
app.mqtt.max-inflight=50
|
|
||||||
app.mqtt.automatic-reconnect=true
|
|
||||||
# Defaults for publishing
|
|
||||||
app.mqtt.default-qos=2
|
|
||||||
app.mqtt.default-retained=false
|
|
||||||
|
|
||||||
# 2FA Configuration
|
# 2FA Configuration
|
||||||
app.security.two-factor.enabled=false
|
app.security.two-factor.enabled=false
|
||||||
|
|
||||||
|
# Message Delivery Layer Configuration
|
||||||
|
app.messaging.delivery.max-retries=3
|
||||||
|
app.messaging.delivery.retry-initial-delay=5s
|
||||||
|
app.messaging.delivery.retry-max-delay=5m
|
||||||
|
app.messaging.delivery.retry-backoff-multiplier=2.0
|
||||||
|
app.messaging.delivery.ack-timeout=30s
|
||||||
|
app.messaging.delivery.message-expiry=24h
|
||||||
|
app.messaging.delivery.cleanup-interval-minutes=60
|
||||||
|
app.messaging.delivery.retry-interval-seconds=30
|
||||||
|
app.messaging.delivery.acknowledged-retention-days=7
|
||||||
|
|
||||||
|
# Messaging Plugin Configuration
|
||||||
|
app.messaging.plugin.type=mqtt
|
||||||
|
app.messaging.plugin.mqtt.broker.host=mqtt-2.assecutor.de
|
||||||
|
app.messaging.plugin.mqtt.broker.port=42099
|
||||||
|
app.messaging.plugin.mqtt.username=app
|
||||||
|
app.messaging.plugin.mqtt.password=apppwd
|
||||||
|
app.messaging.plugin.mqtt.client.id=votianlt-server
|
||||||
|
|
||||||
# Application Version - automatically set from pom.xml during build
|
# Application Version - automatically set from pom.xml during build
|
||||||
app.version=@project.version@
|
app.version=@project.version@
|
||||||
Reference in New Issue
Block a user