24
24
import java .util .List ;
25
25
import java .util .Map ;
26
26
import java .util .Optional ;
27
+ import java .util .UUID ;
27
28
import java .util .concurrent .CompletableFuture ;
28
29
import java .util .concurrent .CompletionStage ;
30
+ import java .util .concurrent .atomic .AtomicBoolean ;
29
31
import java .util .function .Consumer ;
30
32
import java .util .function .Function ;
31
33
53
55
import org .eclipse .ditto .json .JsonObject ;
54
56
import org .eclipse .ditto .json .JsonPointer ;
55
57
import org .eclipse .ditto .json .JsonValue ;
58
+ import org .eclipse .ditto .model .base .common .HttpStatusCode ;
59
+ import org .eclipse .ditto .model .base .exceptions .DittoRuntimeException ;
60
+ import org .eclipse .ditto .model .base .headers .DittoHeaderDefinition ;
56
61
import org .eclipse .ditto .model .messages .Message ;
57
62
import org .eclipse .ditto .model .messages .MessageDirection ;
58
63
import org .eclipse .ditto .model .messages .MessageHeaders ;
67
72
import org .eclipse .ditto .signals .base .Signal ;
68
73
import org .eclipse .ditto .signals .base .WithOptionalEntity ;
69
74
import org .eclipse .ditto .signals .commands .base .CommandResponse ;
75
+ import org .eclipse .ditto .signals .commands .things .ThingErrorResponse ;
70
76
import org .eclipse .ditto .signals .commands .things .modify .CreateThing ;
71
77
import org .eclipse .ditto .signals .commands .things .modify .DeleteThing ;
72
78
import org .eclipse .ditto .signals .commands .things .modify .ModifyThing ;
@@ -93,6 +99,7 @@ public abstract class CommonManagementImpl<T extends ThingHandle<F>, F extends F
93
99
94
100
protected final OutgoingMessageFactory outgoingMessageFactory ;
95
101
102
+ private final AtomicBoolean subscriptionRequestPending = new AtomicBoolean (false );
96
103
private final HandlerRegistry <T , F > handlerRegistry ;
97
104
private final PointerBus bus ;
98
105
@@ -111,12 +118,20 @@ protected CommonManagementImpl(
111
118
112
119
@ Override
113
120
public CompletableFuture <Void > startConsumption () {
114
- return doStartConsumption (Collections .emptyMap ());
121
+ // do not call doStartConsumption directly
122
+ return startConsumption (new Option []{});
115
123
}
116
124
117
125
@ Override
118
126
public CompletableFuture <Void > startConsumption (final Option <?>... consumptionOptions ) {
119
127
128
+ // concurrent consumption requests can have strange effects, so better avoid it
129
+ if (!subscriptionRequestPending .compareAndSet (false , true )) {
130
+ final CompletableFuture <Void > failedFuture = new CompletableFuture <>();
131
+ failedFuture .completeExceptionally (new ConcurrentConsumptionRequestException ());
132
+ return failedFuture ;
133
+ }
134
+
120
135
// only accept "Consumption" related options here:
121
136
final Optional <Option <?>> unknownOptionIncluded = Arrays .stream (consumptionOptions )
122
137
.filter (option -> !option .getName ().equals (OptionName .Consumption .NAMESPACES ))
@@ -139,7 +154,8 @@ public CompletableFuture<Void> startConsumption(final Option<?>... consumptionOp
139
154
options .getExtraFields ().ifPresent (extraFields ->
140
155
subscriptionConfig .put (CONSUMPTION_PARAM_EXTRA_FIELDS , extraFields .toString ()));
141
156
142
- return doStartConsumption (subscriptionConfig );
157
+ // make sure to reset the flag when consumption request completes
158
+ return doStartConsumption (subscriptionConfig ).whenComplete ((v , t ) -> subscriptionRequestPending .set (false ));
143
159
}
144
160
145
161
/**
@@ -639,8 +655,11 @@ protected AdaptableBus.SubscriptionId subscribeAndPublishMessage(
639
655
final CompletableFuture <Void > futureToCompleteOrFailAfterAck ,
640
656
final Function <Adaptable , NotifyMessage > adaptableToNotifier ) {
641
657
642
- LOGGER .trace ("Sending {} and waiting for {}" , protocolCommand , protocolCommandAck );
658
+ final String correlationId = UUID .randomUUID ().toString ();
659
+ final String protocolCommandWithCorrelationId = appendCorrelationIdParameter (protocolCommand , correlationId );
660
+ LOGGER .trace ("Sending {} and waiting for {}" , protocolCommandWithCorrelationId , protocolCommandAck );
643
661
final AdaptableBus adaptableBus = messagingProvider .getAdaptableBus ();
662
+
644
663
if (previousSubscriptionId != null ) {
645
664
// remove previous subscription without going through back-end because subscription will be replaced
646
665
adaptableBus .unsubscribe (previousSubscriptionId );
@@ -649,11 +668,42 @@ protected AdaptableBus.SubscriptionId subscribeAndPublishMessage(
649
668
adaptableBus .subscribeForAdaptable (streamingType ,
650
669
adaptable -> adaptableToNotifier .apply (adaptable ).accept (getBus ()));
651
670
final Classification tag = Classification .forString (protocolCommandAck );
652
- adjoin (adaptableBus .subscribeOnceForString (tag , getTimeout ()), futureToCompleteOrFailAfterAck );
653
- messagingProvider .emit (protocolCommand );
671
+
672
+ // subscribe exclusively because we allow only one request at a time
673
+ final CompletionStage <String > ackStage = adaptableBus .subscribeOnceForStringExclusively (tag , getTimeout ());
674
+ final CompletableFuture <String > ackFuture = ackStage .toCompletableFuture ();
675
+
676
+ // subscribe for possible error responses by correlationId
677
+ final Classification correlationIdTag = Classification .forCorrelationId (correlationId );
678
+ adaptableBus .subscribeOnceForAdaptable (correlationIdTag , getTimeout ())
679
+ .thenAccept (adaptable -> {
680
+ final Signal <?> signal = AbstractHandle .PROTOCOL_ADAPTER .fromAdaptable (adaptable );
681
+ if (signal instanceof ThingErrorResponse ) {
682
+ ackFuture .completeExceptionally (((ThingErrorResponse ) signal ).getDittoRuntimeException ());
683
+ } else {
684
+ ackFuture .completeExceptionally (getUnexpectedSignalException (signal ));
685
+ }
686
+ });
687
+
688
+ adjoin (ackFuture , futureToCompleteOrFailAfterAck );
689
+ messagingProvider .emit (protocolCommandWithCorrelationId );
690
+
654
691
return subscriptionId ;
655
692
}
656
693
694
+ private DittoRuntimeException getUnexpectedSignalException (final Signal <?> signal ) {
695
+ return DittoRuntimeException
696
+ .newBuilder ("signal.unexpected" , HttpStatusCode .BAD_REQUEST )
697
+ .message (() -> String .format ("Received unexpected response of type '%s'." , signal .getType ()))
698
+ .build ();
699
+ }
700
+
701
+ private String appendCorrelationIdParameter (final String protocolCommand , final String correlationId ) {
702
+ final String separator = protocolCommand .contains ("?" ) ? "&" : "?" ;
703
+ return String .format ("%s%s%s=%s" , protocolCommand , separator ,
704
+ DittoHeaderDefinition .CORRELATION_ID .getKey (), correlationId );
705
+ }
706
+
657
707
/**
658
708
* Remove a subscription.
659
709
*
0 commit comments