@@ -23,7 +23,6 @@ import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.Client
23
23
import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_TERMINATE
24
24
import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ClientMessages.GQL_START
25
25
import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ClientMessages.GQL_STOP
26
- import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ServerMessages.GQL_COMPLETE
27
26
import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ServerMessages.GQL_CONNECTION_ACK
28
27
import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ServerMessages.GQL_CONNECTION_ERROR
29
28
import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ServerMessages.GQL_CONNECTION_KEEP_ALIVE
@@ -32,12 +31,10 @@ import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.Server
32
31
import com.fasterxml.jackson.databind.ObjectMapper
33
32
import com.fasterxml.jackson.module.kotlin.convertValue
34
33
import com.fasterxml.jackson.module.kotlin.readValue
35
- import org.reactivestreams.Subscription
36
34
import org.slf4j.LoggerFactory
37
35
import org.springframework.web.reactive.socket.WebSocketSession
38
36
import reactor.core.publisher.Flux
39
37
import java.time.Duration
40
- import java.util.concurrent.ConcurrentHashMap
41
38
42
39
/* *
43
40
* Implementation of the `graphql-ws` protocol defined by Apollo
@@ -48,11 +45,7 @@ class ApolloSubscriptionProtocolHandler(
48
45
private val subscriptionHandler : SubscriptionHandler ,
49
46
private val objectMapper : ObjectMapper
50
47
) {
51
- // Sessions are saved by web socket session id
52
- private val activeKeepAliveSessions = ConcurrentHashMap <String , Subscription >()
53
- // Operations are saved by web socket session id, then operation id
54
- private val activeOperations = ConcurrentHashMap <String , ConcurrentHashMap <String , Subscription >>()
55
-
48
+ private val sessionState = ApolloSubscriptionSessionState ()
56
49
private val logger = LoggerFactory .getLogger(ApolloSubscriptionProtocolHandler ::class .java)
57
50
private val keepAliveMessage = SubscriptionOperationMessage (type = GQL_CONNECTION_KEEP_ALIVE .type)
58
51
private val basicConnectionErrorMessage = SubscriptionOperationMessage (type = GQL_CONNECTION_ERROR .type)
@@ -63,36 +56,26 @@ class ApolloSubscriptionProtocolHandler(
63
56
try {
64
57
val operationMessage: SubscriptionOperationMessage = objectMapper.readValue(payload)
65
58
66
- return when (operationMessage.type) {
67
- GQL_CONNECTION_INIT .type -> {
68
- val flux = Flux .just(acknowledgeMessage)
69
- val keepAliveInterval = config.subscriptions.keepAliveInterval
70
- if (keepAliveInterval != null ) {
71
- // Send the GQL_CONNECTION_KEEP_ALIVE message every interval until the connection is closed or terminated
72
- val keepAliveFlux = Flux .interval(Duration .ofMillis(keepAliveInterval))
73
- .map { keepAliveMessage }
74
- .doOnSubscribe {
75
- logger.debug(" GraphQL subscription INIT, sessionId=${session.id} activeSessions=${activeKeepAliveSessions.count()} " )
76
- activeKeepAliveSessions[session.id] = it
77
- }
78
-
79
- return flux.concatWith(keepAliveFlux)
80
- }
59
+ logger.debug(" GraphQL subscription client message, sessionId=${session.id} operationMessage=$operationMessage " )
81
60
82
- return flux
61
+ when (operationMessage.type) {
62
+ GQL_CONNECTION_INIT .type -> {
63
+ val ackowledgeMessageFlux = Flux .just(acknowledgeMessage)
64
+ val keepAliveFlux = getKeepAliveFlux(session)
65
+ return ackowledgeMessageFlux.concatWith(keepAliveFlux)
83
66
}
84
- GQL_START .type -> startSubscription(operationMessage, session)
67
+ GQL_START .type -> return startSubscription(operationMessage, session)
85
68
GQL_STOP .type -> {
86
- stopSubscription(operationMessage, session )
69
+ sessionState.stopOperation(session, operationMessage )
87
70
return Flux .empty()
88
71
}
89
72
GQL_CONNECTION_TERMINATE .type -> {
90
- terminateSession(session)
73
+ sessionState. terminateSession(session)
91
74
return Flux .empty()
92
75
}
93
76
else -> {
94
77
logger.error(" Unknown subscription operation $operationMessage " )
95
- stopSubscription(operationMessage, session )
78
+ sessionState.stopOperation(session, operationMessage )
96
79
return Flux .just(SubscriptionOperationMessage (type = GQL_CONNECTION_ERROR .type, id = operationMessage.id))
97
80
}
98
81
}
@@ -102,6 +85,21 @@ class ApolloSubscriptionProtocolHandler(
102
85
}
103
86
}
104
87
88
+ /* *
89
+ * If the keep alive configuraation is set, send a message back to client at every interval until the session is terminated.
90
+ * Otherwise just return empty flux to append to the acknowledge message.
91
+ */
92
+ private fun getKeepAliveFlux (session : WebSocketSession ): Flux <SubscriptionOperationMessage > {
93
+ val keepAliveInterval: Long? = config.subscriptions.keepAliveInterval
94
+ if (keepAliveInterval != null ) {
95
+ return Flux .interval(Duration .ofMillis(keepAliveInterval))
96
+ .map { keepAliveMessage }
97
+ .doOnSubscribe { sessionState.saveKeepAliveSubscription(session, it) }
98
+ }
99
+
100
+ return Flux .empty()
101
+ }
102
+
105
103
@Suppress(" Detekt.TooGenericExceptionCaught" )
106
104
private fun startSubscription (operationMessage : SubscriptionOperationMessage , session : WebSocketSession ): Flux <SubscriptionOperationMessage > {
107
105
if (operationMessage.id == null ) {
@@ -113,7 +111,7 @@ class ApolloSubscriptionProtocolHandler(
113
111
114
112
if (payload == null ) {
115
113
logger.error(" GraphQL subscription payload was null instead of a GraphQLRequest object" )
116
- stopSubscription(operationMessage, session )
114
+ sessionState.stopOperation(session, operationMessage )
117
115
return Flux .just(SubscriptionOperationMessage (type = GQL_CONNECTION_ERROR .type, id = operationMessage.id))
118
116
}
119
117
@@ -127,35 +125,13 @@ class ApolloSubscriptionProtocolHandler(
127
125
SubscriptionOperationMessage (type = GQL_DATA .type, id = operationMessage.id, payload = it)
128
126
}
129
127
}
130
- .concatWith(Flux .just(SubscriptionOperationMessage (type = GQL_COMPLETE .type, id = operationMessage.id)))
131
- .doOnSubscribe {
132
- logger.debug(" GraphQL subscription START, sessionId=${session.id} operationId=${operationMessage.id} " )
133
- activeOperations[session.id]?.put(operationMessage.id, it)
134
- }
135
- .doOnCancel { logger.debug(" GraphQL subscription CANCEL, sessionId=${session.id} operationId=${operationMessage.id} " ) }
136
- .doOnComplete { logger.debug(" GraphQL subscription COMPELTE, sessionId=${session.id} operationId=${operationMessage.id} " ) }
128
+ .concatWith(Flux .just(SubscriptionOperationMessage (type = SubscriptionOperationMessage .ServerMessages .GQL_COMPLETE .type, id = operationMessage.id)))
129
+ .doOnSubscribe { sessionState.saveOperation(session, operationMessage, it) }
137
130
} catch (exception: Exception ) {
138
131
logger.error(" Error running graphql subscription" , exception)
139
- stopSubscription(operationMessage, session)
132
+ // Do not terminate the session, just stop the operation messages
133
+ sessionState.stopOperation(session, operationMessage)
140
134
return Flux .just(SubscriptionOperationMessage (type = GQL_CONNECTION_ERROR .type, id = operationMessage.id))
141
135
}
142
136
}
143
-
144
- private fun stopSubscription (operationMessage : SubscriptionOperationMessage , session : WebSocketSession ) {
145
- logger.debug(" GraphQL subscription STOP, sessionId=${session.id} operationId=${operationMessage.id} " )
146
- if (operationMessage.id != null ) {
147
- val operationsForSession = activeOperations[session.id]
148
- operationsForSession?.get(operationMessage.id)?.cancel()
149
- operationsForSession?.remove(operationMessage.id)
150
- }
151
- }
152
-
153
- private fun terminateSession (session : WebSocketSession ) {
154
- logger.debug(" GraphQL subscription TERMINATE, sessionId=${session.id} " )
155
- activeOperations[session.id]?.forEach { _, subscription -> subscription.cancel() }
156
- activeOperations.remove(session.id)
157
- activeKeepAliveSessions[session.id]?.cancel()
158
- activeKeepAliveSessions.remove(session.id)
159
- session.close()
160
- }
161
137
}
0 commit comments