@onekeyfe/react-native-sni-connect 3.0.81-alpha.8 → 3.0.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/android/src/main/java/com/sniconnect/SniConnectModule.kt +213 -61
- package/android/src/main/java/com/sniconnect/SniConnectRequestAdmission.kt +276 -0
- package/android/src/main/java/com/sniconnect/SniConnectValidation.kt +6 -76
- package/android/src/test/java/com/sniconnect/SniConnectRequestAdmissionTest.kt +299 -0
- package/android/src/test/java/com/sniconnect/SniConnectValidationTest.kt +26 -26
- package/ios/SniConnect.mm +19 -0
- package/ios/SniConnect.swift +29 -0
- package/ios/SniConnectClient.swift +75 -84
- package/ios/SniConnectValidation.swift +214 -41
- package/ios/Tests/SniConnectValidationTests/SniConnectValidationTests.swift +206 -8
- package/lib/module/index.js +3 -0
- package/lib/typescript/src/NativeSniConnect.d.ts +13 -0
- package/lib/typescript/src/index.d.ts +3 -2
- package/package.json +2 -3
- package/src/NativeSniConnect.ts +17 -0
- package/src/index.tsx +10 -0
- package/android/src/main/java/com/sniconnect/SniPinnedTransport.kt +0 -58
- package/ios/SniConnectPinnedTransport.swift +0 -92
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
package com.sniconnect
|
|
2
|
+
|
|
3
|
+
import java.util.ArrayDeque
|
|
4
|
+
import java.util.Locale
|
|
5
|
+
import java.util.concurrent.ScheduledExecutorService
|
|
6
|
+
import java.util.concurrent.ScheduledFuture
|
|
7
|
+
import java.util.concurrent.ScheduledThreadPoolExecutor
|
|
8
|
+
import java.util.concurrent.TimeUnit
|
|
9
|
+
|
|
10
|
+
internal data class SniConnectAdmissionSnapshot(
|
|
11
|
+
val activeRequests: Int,
|
|
12
|
+
val activeRequestsForPair: Int,
|
|
13
|
+
val pendingRequests: Int,
|
|
14
|
+
val pendingRequestsForPair: Int,
|
|
15
|
+
val activeRequestIdsForPair: List<String> = emptyList(),
|
|
16
|
+
val pendingRequestIdsForPair: List<String> = emptyList(),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
internal class SniConnectRequestAdmission(
|
|
20
|
+
private val maxActiveRequests: Int = SniConnectValidation.MAX_ACTIVE_REQUESTS,
|
|
21
|
+
private val maxActiveRequestsPerPair: Int = SniConnectValidation.MAX_ACTIVE_REQUESTS_PER_PAIR,
|
|
22
|
+
private val maxPendingRequests: Int = SniConnectValidation.MAX_PENDING_REQUESTS,
|
|
23
|
+
private val scheduler: ScheduledExecutorService = createAdmissionScheduler(),
|
|
24
|
+
private val nanoTime: () -> Long = System::nanoTime,
|
|
25
|
+
) {
|
|
26
|
+
internal data class PairKey(
|
|
27
|
+
val hostname: String,
|
|
28
|
+
val ip: String,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
internal enum class State {
|
|
32
|
+
CREATED,
|
|
33
|
+
PENDING,
|
|
34
|
+
ACTIVE,
|
|
35
|
+
TERMINAL,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private data class Dispatch(
|
|
39
|
+
val ticket: Ticket,
|
|
40
|
+
val remainingTimeoutMillis: Long?,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
inner class Ticket internal constructor(
|
|
44
|
+
internal val pair: PairKey,
|
|
45
|
+
internal val requestId: String?,
|
|
46
|
+
internal val deadlineNanos: Long,
|
|
47
|
+
internal val onAdmitted: (remainingTimeoutMillis: Long) -> Unit,
|
|
48
|
+
internal val onPendingFailure: (code: String, message: String) -> Unit,
|
|
49
|
+
) {
|
|
50
|
+
internal var state = State.CREATED
|
|
51
|
+
internal var timeoutFuture: ScheduledFuture<*>? = null
|
|
52
|
+
|
|
53
|
+
fun submit() {
|
|
54
|
+
this@SniConnectRequestAdmission.submit(this)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fun cancelPending(): Boolean =
|
|
58
|
+
this@SniConnectRequestAdmission.cancelPending(this)
|
|
59
|
+
|
|
60
|
+
fun release() {
|
|
61
|
+
this@SniConnectRequestAdmission.release(this)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private val lock = Any()
|
|
66
|
+
private var activeRequests = 0
|
|
67
|
+
private val activeRequestsByPair = mutableMapOf<PairKey, Int>()
|
|
68
|
+
private val activeTickets = mutableSetOf<Ticket>()
|
|
69
|
+
private val pendingRequests = ArrayDeque<Ticket>()
|
|
70
|
+
|
|
71
|
+
fun createTicket(
|
|
72
|
+
hostname: String,
|
|
73
|
+
ip: String,
|
|
74
|
+
requestId: String? = null,
|
|
75
|
+
timeoutMillis: Long,
|
|
76
|
+
onAdmitted: (remainingTimeoutMillis: Long) -> Unit,
|
|
77
|
+
onPendingFailure: (code: String, message: String) -> Unit,
|
|
78
|
+
): Ticket = Ticket(
|
|
79
|
+
pair = pairKey(hostname, ip),
|
|
80
|
+
requestId = requestId?.takeIf { it.isNotEmpty() },
|
|
81
|
+
deadlineNanos = nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis),
|
|
82
|
+
onAdmitted = onAdmitted,
|
|
83
|
+
onPendingFailure = onPendingFailure,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
fun snapshot(hostname: String, ip: String): SniConnectAdmissionSnapshot {
|
|
87
|
+
val pair = pairKey(hostname, ip)
|
|
88
|
+
return synchronized(lock) {
|
|
89
|
+
val activeForPair = activeTickets.filter { ticket -> ticket.pair == pair }
|
|
90
|
+
val pendingForPair = pendingRequests.filter { ticket -> ticket.pair == pair }
|
|
91
|
+
SniConnectAdmissionSnapshot(
|
|
92
|
+
activeRequests = activeRequests,
|
|
93
|
+
activeRequestsForPair = activeForPair.size,
|
|
94
|
+
pendingRequests = pendingRequests.size,
|
|
95
|
+
pendingRequestsForPair = pendingForPair.size,
|
|
96
|
+
activeRequestIdsForPair = activeForPair.mapNotNull { ticket -> ticket.requestId }.sorted(),
|
|
97
|
+
pendingRequestIdsForPair = pendingForPair.mapNotNull { ticket -> ticket.requestId }.sorted(),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
internal fun shutdownForTests() {
|
|
103
|
+
scheduler.shutdownNow()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun submit(ticket: Ticket) {
|
|
107
|
+
var remainingTimeoutMillis: Long? = null
|
|
108
|
+
var timedOut = false
|
|
109
|
+
synchronized(lock) {
|
|
110
|
+
// A runtime can cancel immediately after registering the handle but
|
|
111
|
+
// before submit() reaches this lock. Cancellation already settled the
|
|
112
|
+
// ticket, so submission becomes an idempotent no-op.
|
|
113
|
+
if (ticket.state == State.TERMINAL) return
|
|
114
|
+
check(ticket.state == State.CREATED) { "Admission ticket already submitted" }
|
|
115
|
+
if (ticket.deadlineNanos <= nanoTime()) {
|
|
116
|
+
ticket.state = State.TERMINAL
|
|
117
|
+
timedOut = true
|
|
118
|
+
} else if (canActivateLocked(ticket.pair)) {
|
|
119
|
+
activateLocked(ticket)
|
|
120
|
+
remainingTimeoutMillis = remainingMillis(ticket.deadlineNanos)
|
|
121
|
+
} else {
|
|
122
|
+
if (pendingRequests.size >= maxPendingRequests) {
|
|
123
|
+
ticket.state = State.TERMINAL
|
|
124
|
+
throw SniConnectValidation.ValidationException("Too many pending SNI requests")
|
|
125
|
+
}
|
|
126
|
+
ticket.state = State.PENDING
|
|
127
|
+
pendingRequests.addLast(ticket)
|
|
128
|
+
val delayNanos = (ticket.deadlineNanos - nanoTime()).coerceAtLeast(0L)
|
|
129
|
+
ticket.timeoutFuture = scheduler.schedule(
|
|
130
|
+
{ timeoutPending(ticket) },
|
|
131
|
+
delayNanos,
|
|
132
|
+
TimeUnit.NANOSECONDS,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (timedOut) {
|
|
138
|
+
ticket.onPendingFailure(
|
|
139
|
+
"SNI_REQUEST_TIMEOUT",
|
|
140
|
+
"Request timed out while waiting for admission",
|
|
141
|
+
)
|
|
142
|
+
} else {
|
|
143
|
+
remainingTimeoutMillis?.let(ticket.onAdmitted)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private fun cancelPending(ticket: Ticket): Boolean {
|
|
148
|
+
val cancelled = synchronized(lock) {
|
|
149
|
+
when (ticket.state) {
|
|
150
|
+
State.CREATED -> {
|
|
151
|
+
ticket.state = State.TERMINAL
|
|
152
|
+
true
|
|
153
|
+
}
|
|
154
|
+
State.PENDING -> {
|
|
155
|
+
pendingRequests.remove(ticket)
|
|
156
|
+
ticket.timeoutFuture?.cancel(false)
|
|
157
|
+
ticket.timeoutFuture = null
|
|
158
|
+
ticket.state = State.TERMINAL
|
|
159
|
+
true
|
|
160
|
+
}
|
|
161
|
+
State.ACTIVE, State.TERMINAL -> false
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (cancelled) {
|
|
165
|
+
ticket.onPendingFailure("SNI_CANCELLED", "Request cancelled")
|
|
166
|
+
}
|
|
167
|
+
return cancelled
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private fun timeoutPending(ticket: Ticket) {
|
|
171
|
+
val timedOut = synchronized(lock) {
|
|
172
|
+
if (ticket.state != State.PENDING) {
|
|
173
|
+
false
|
|
174
|
+
} else {
|
|
175
|
+
pendingRequests.remove(ticket)
|
|
176
|
+
ticket.timeoutFuture = null
|
|
177
|
+
ticket.state = State.TERMINAL
|
|
178
|
+
true
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (timedOut) {
|
|
182
|
+
ticket.onPendingFailure(
|
|
183
|
+
"SNI_REQUEST_TIMEOUT",
|
|
184
|
+
"Request timed out while waiting for admission",
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun release(ticket: Ticket) {
|
|
190
|
+
val admissions = synchronized(lock) {
|
|
191
|
+
if (ticket.state != State.ACTIVE) return
|
|
192
|
+
ticket.state = State.TERMINAL
|
|
193
|
+
activeTickets.remove(ticket)
|
|
194
|
+
activeRequests -= 1
|
|
195
|
+
decrementPairLocked(ticket.pair)
|
|
196
|
+
collectAdmissionsLocked()
|
|
197
|
+
}
|
|
198
|
+
admissions.forEach { dispatch ->
|
|
199
|
+
val timeoutMillis = dispatch.remainingTimeoutMillis
|
|
200
|
+
if (timeoutMillis == null) {
|
|
201
|
+
dispatch.ticket.onPendingFailure(
|
|
202
|
+
"SNI_REQUEST_TIMEOUT",
|
|
203
|
+
"Request timed out while waiting for admission",
|
|
204
|
+
)
|
|
205
|
+
} else {
|
|
206
|
+
dispatch.ticket.onAdmitted(timeoutMillis)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun collectAdmissionsLocked(): List<Dispatch> {
|
|
212
|
+
val admissions = mutableListOf<Dispatch>()
|
|
213
|
+
while (activeRequests < maxActiveRequests) {
|
|
214
|
+
val iterator = pendingRequests.iterator()
|
|
215
|
+
var next: Ticket? = null
|
|
216
|
+
while (iterator.hasNext()) {
|
|
217
|
+
val candidate = iterator.next()
|
|
218
|
+
if (canActivateLocked(candidate.pair)) {
|
|
219
|
+
iterator.remove()
|
|
220
|
+
next = candidate
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
val ticket = next ?: break
|
|
225
|
+
ticket.timeoutFuture?.cancel(false)
|
|
226
|
+
ticket.timeoutFuture = null
|
|
227
|
+
if (ticket.deadlineNanos <= nanoTime()) {
|
|
228
|
+
ticket.state = State.TERMINAL
|
|
229
|
+
admissions += Dispatch(ticket, null)
|
|
230
|
+
} else {
|
|
231
|
+
activateLocked(ticket)
|
|
232
|
+
admissions += Dispatch(ticket, remainingMillis(ticket.deadlineNanos))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return admissions
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private fun canActivateLocked(pair: PairKey): Boolean =
|
|
239
|
+
activeRequests < maxActiveRequests &&
|
|
240
|
+
(activeRequestsByPair[pair] ?: 0) < maxActiveRequestsPerPair
|
|
241
|
+
|
|
242
|
+
private fun pairKey(hostname: String, ip: String): PairKey = PairKey(
|
|
243
|
+
hostname = hostname.lowercase(Locale.US),
|
|
244
|
+
ip = SniConnectValidation.canonicalizePublicIp(ip),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
private fun activateLocked(ticket: Ticket) {
|
|
248
|
+
ticket.state = State.ACTIVE
|
|
249
|
+
activeTickets.add(ticket)
|
|
250
|
+
activeRequests += 1
|
|
251
|
+
activeRequestsByPair[ticket.pair] = (activeRequestsByPair[ticket.pair] ?: 0) + 1
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private fun decrementPairLocked(pair: PairKey) {
|
|
255
|
+
val pairCount = activeRequestsByPair[pair] ?: return
|
|
256
|
+
if (pairCount == 1) {
|
|
257
|
+
activeRequestsByPair.remove(pair)
|
|
258
|
+
} else {
|
|
259
|
+
activeRequestsByPair[pair] = pairCount - 1
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private fun remainingMillis(deadlineNanos: Long): Long {
|
|
264
|
+
val remainingNanos = (deadlineNanos - nanoTime()).coerceAtLeast(1L)
|
|
265
|
+
return ((remainingNanos + 999_999L) / 1_000_000L).coerceAtLeast(1L)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private companion object {
|
|
269
|
+
fun createAdmissionScheduler(): ScheduledExecutorService =
|
|
270
|
+
ScheduledThreadPoolExecutor(1) { runnable ->
|
|
271
|
+
Thread(runnable, "SniConnectAdmission").apply { isDaemon = true }
|
|
272
|
+
}.apply {
|
|
273
|
+
removeOnCancelPolicy = true
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -4,7 +4,6 @@ import java.net.Inet6Address
|
|
|
4
4
|
import java.net.InetAddress
|
|
5
5
|
import java.nio.charset.StandardCharsets
|
|
6
6
|
import java.util.Locale
|
|
7
|
-
import java.util.concurrent.atomic.AtomicBoolean
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Boundary validation/normalization for SNI request inputs.
|
|
@@ -29,6 +28,7 @@ internal object SniConnectValidation {
|
|
|
29
28
|
const val MAX_TOTAL_HEADER_BYTES = 32 * 1024
|
|
30
29
|
const val MAX_ACTIVE_REQUESTS = 64
|
|
31
30
|
const val MAX_ACTIVE_REQUESTS_PER_PAIR = 16
|
|
31
|
+
const val MAX_PENDING_REQUESTS = 256
|
|
32
32
|
|
|
33
33
|
private val ALLOWED_METHODS =
|
|
34
34
|
setOf("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")
|
|
@@ -225,6 +225,11 @@ internal object SniConnectValidation {
|
|
|
225
225
|
throw ValidationException("Invalid IP: $ip")
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
fun canonicalizePublicIp(ip: String): String {
|
|
229
|
+
validatePublicIp(ip)
|
|
230
|
+
return literalToInetAddress(ip).hostAddress
|
|
231
|
+
}
|
|
232
|
+
|
|
228
233
|
private fun isForbiddenIpv4(o: List<Int>): Boolean {
|
|
229
234
|
val a = o[0]; val b = o[1]; val c = o[2]; val d = o[3]
|
|
230
235
|
return when {
|
|
@@ -313,78 +318,3 @@ internal object SniConnectValidation {
|
|
|
313
318
|
return InetAddress.getByName(ip) // safe: already validated as an IPv6 literal
|
|
314
319
|
}
|
|
315
320
|
}
|
|
316
|
-
|
|
317
|
-
internal class SniConnectRequestLimiter(
|
|
318
|
-
private val maxActiveRequests: Int = SniConnectValidation.MAX_ACTIVE_REQUESTS,
|
|
319
|
-
private val maxActiveRequestsPerPair: Int = SniConnectValidation.MAX_ACTIVE_REQUESTS_PER_PAIR,
|
|
320
|
-
) {
|
|
321
|
-
private val lock = Any()
|
|
322
|
-
private var activeRequests = 0
|
|
323
|
-
private val activeRequestsByPair = mutableMapOf<String, Int>()
|
|
324
|
-
|
|
325
|
-
fun acquire(hostname: String, ip: String): Token {
|
|
326
|
-
val key = pairKey(hostname, ip)
|
|
327
|
-
synchronized(lock) {
|
|
328
|
-
if (activeRequests >= maxActiveRequests) {
|
|
329
|
-
SniConnectLogger.warn(
|
|
330
|
-
SniConnectLogger.event(
|
|
331
|
-
"sni_resource_limit",
|
|
332
|
-
"activeCount" to activeRequests,
|
|
333
|
-
"pairCount" to (activeRequestsByPair[key] ?: 0),
|
|
334
|
-
"limit" to maxActiveRequests,
|
|
335
|
-
"reason" to "max_active_requests",
|
|
336
|
-
"hostname" to hostname.lowercase(Locale.US),
|
|
337
|
-
"ipHash" to SniConnectLogger.shortHash(ip),
|
|
338
|
-
),
|
|
339
|
-
)
|
|
340
|
-
throw SniConnectValidation.ValidationException("Too many active SNI requests")
|
|
341
|
-
}
|
|
342
|
-
val pairCount = activeRequestsByPair[key] ?: 0
|
|
343
|
-
if (pairCount >= maxActiveRequestsPerPair) {
|
|
344
|
-
SniConnectLogger.warn(
|
|
345
|
-
SniConnectLogger.event(
|
|
346
|
-
"sni_resource_limit",
|
|
347
|
-
"activeCount" to activeRequests,
|
|
348
|
-
"pairCount" to pairCount,
|
|
349
|
-
"limit" to maxActiveRequestsPerPair,
|
|
350
|
-
"reason" to "max_active_requests_per_pair",
|
|
351
|
-
"hostname" to hostname.lowercase(Locale.US),
|
|
352
|
-
"ipHash" to SniConnectLogger.shortHash(ip),
|
|
353
|
-
),
|
|
354
|
-
)
|
|
355
|
-
throw SniConnectValidation.ValidationException("Too many active SNI requests for destination")
|
|
356
|
-
}
|
|
357
|
-
activeRequests += 1
|
|
358
|
-
activeRequestsByPair[key] = pairCount + 1
|
|
359
|
-
}
|
|
360
|
-
return Token(this, key)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private fun release(key: String) {
|
|
364
|
-
synchronized(lock) {
|
|
365
|
-
activeRequests = (activeRequests - 1).coerceAtLeast(0)
|
|
366
|
-
val pairCount = activeRequestsByPair[key] ?: return
|
|
367
|
-
if (pairCount <= 1) {
|
|
368
|
-
activeRequestsByPair.remove(key)
|
|
369
|
-
} else {
|
|
370
|
-
activeRequestsByPair[key] = pairCount - 1
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
private fun pairKey(hostname: String, ip: String): String =
|
|
376
|
-
"${hostname.lowercase(Locale.US)}|$ip"
|
|
377
|
-
|
|
378
|
-
class Token internal constructor(
|
|
379
|
-
private val limiter: SniConnectRequestLimiter,
|
|
380
|
-
private val key: String,
|
|
381
|
-
) {
|
|
382
|
-
private val released = AtomicBoolean(false)
|
|
383
|
-
|
|
384
|
-
fun release() {
|
|
385
|
-
if (released.compareAndSet(false, true)) {
|
|
386
|
-
limiter.release(key)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
package com.sniconnect
|
|
2
|
+
|
|
3
|
+
import java.util.Collections
|
|
4
|
+
import java.util.concurrent.CountDownLatch
|
|
5
|
+
import java.util.concurrent.TimeUnit
|
|
6
|
+
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertFalse
|
|
8
|
+
import org.junit.Assert.assertTrue
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
|
|
11
|
+
class SniConnectRequestAdmissionTest {
|
|
12
|
+
@Test
|
|
13
|
+
fun twentySamePairRequestsProduceSixteenActiveAndFourPending() {
|
|
14
|
+
val admission = SniConnectRequestAdmission()
|
|
15
|
+
val admitted = mutableListOf<String>()
|
|
16
|
+
val failures = mutableListOf<Pair<String, String>>()
|
|
17
|
+
try {
|
|
18
|
+
val tickets = (0 until 20).map { index ->
|
|
19
|
+
val requestId = "req-${index.toString().padStart(2, '0')}"
|
|
20
|
+
admission.createTicket(
|
|
21
|
+
hostname = "Example.com",
|
|
22
|
+
ip = if (index % 2 == 0) "093.184.216.034" else "93.184.216.34",
|
|
23
|
+
requestId = requestId,
|
|
24
|
+
timeoutMillis = 10_000,
|
|
25
|
+
onAdmitted = { admitted += requestId },
|
|
26
|
+
onPendingFailure = { code, message -> failures += code to message },
|
|
27
|
+
).also { it.submit() }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
assertEquals((0 until 16).map { "req-${it.toString().padStart(2, '0')}" }, admitted)
|
|
31
|
+
assertEquals(
|
|
32
|
+
SniConnectAdmissionSnapshot(
|
|
33
|
+
activeRequests = 16,
|
|
34
|
+
activeRequestsForPair = 16,
|
|
35
|
+
pendingRequests = 4,
|
|
36
|
+
pendingRequestsForPair = 4,
|
|
37
|
+
activeRequestIdsForPair = (0 until 16).map { "req-${it.toString().padStart(2, '0')}" },
|
|
38
|
+
pendingRequestIdsForPair = (16 until 20).map { "req-${it.toString().padStart(2, '0')}" },
|
|
39
|
+
),
|
|
40
|
+
admission.snapshot("EXAMPLE.COM", "93.184.216.34"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
tickets.drop(16).forEach { pending -> assertTrue(pending.cancelPending()) }
|
|
44
|
+
assertEquals(4, failures.size)
|
|
45
|
+
assertTrue(failures.all { it.first == "SNI_CANCELLED" })
|
|
46
|
+
assertEquals(0, admission.snapshot("example.com", "093.184.216.034").pendingRequests)
|
|
47
|
+
|
|
48
|
+
tickets.take(16).forEach { active -> active.release() }
|
|
49
|
+
assertEquals(
|
|
50
|
+
SniConnectAdmissionSnapshot(0, 0, 0, 0),
|
|
51
|
+
admission.snapshot("example.com", "93.184.216.34"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
val recovery = ticket(admission, "example.com", "93.184.216.34")
|
|
55
|
+
recovery.submit()
|
|
56
|
+
assertEquals(1, admission.snapshot("example.com", "93.184.216.34").activeRequests)
|
|
57
|
+
recovery.release()
|
|
58
|
+
} finally {
|
|
59
|
+
admission.shutdownForTests()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun queuesAtGlobalLimitAndDispatchesAfterRelease() {
|
|
65
|
+
val admission = admission(maxActive = 2, maxPerPair = 2)
|
|
66
|
+
val admitted = mutableListOf<String>()
|
|
67
|
+
try {
|
|
68
|
+
val first = submit(admission, "first", "one.example", "93.184.216.34", admitted)
|
|
69
|
+
val second = submit(admission, "second", "two.example", "93.184.216.35", admitted)
|
|
70
|
+
val pending = submit(admission, "pending", "three.example", "93.184.216.36", admitted)
|
|
71
|
+
|
|
72
|
+
assertEquals(listOf("first", "second"), admitted)
|
|
73
|
+
assertEquals(2, admission.snapshot("three.example", "93.184.216.36").activeRequests)
|
|
74
|
+
assertEquals(1, admission.snapshot("three.example", "93.184.216.36").pendingRequests)
|
|
75
|
+
|
|
76
|
+
first.release()
|
|
77
|
+
assertEquals(listOf("first", "second", "pending"), admitted)
|
|
78
|
+
first.release()
|
|
79
|
+
second.release()
|
|
80
|
+
pending.release()
|
|
81
|
+
assertEquals(0, admission.snapshot("three.example", "93.184.216.36").activeRequests)
|
|
82
|
+
} finally {
|
|
83
|
+
admission.shutdownForTests()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test
|
|
88
|
+
fun sameHostnameDifferentIpsHaveIndependentPairLimits() {
|
|
89
|
+
val admission = admission(maxActive = 4, maxPerPair = 2)
|
|
90
|
+
val admitted = mutableListOf<String>()
|
|
91
|
+
try {
|
|
92
|
+
val firstA = submit(admission, "a1", "Example.com", "93.184.216.34", admitted)
|
|
93
|
+
val secondA = submit(admission, "a2", "example.com", "93.184.216.34", admitted)
|
|
94
|
+
val pendingA = submit(admission, "a3", "example.com", "93.184.216.34", admitted)
|
|
95
|
+
val firstB = submit(admission, "b1", "example.com", "93.184.216.35", admitted)
|
|
96
|
+
val secondB = submit(admission, "b2", "example.com", "93.184.216.35", admitted)
|
|
97
|
+
|
|
98
|
+
assertEquals(listOf("a1", "a2", "b1", "b2"), admitted)
|
|
99
|
+
assertEquals(
|
|
100
|
+
SniConnectAdmissionSnapshot(
|
|
101
|
+
activeRequests = 4,
|
|
102
|
+
activeRequestsForPair = 2,
|
|
103
|
+
pendingRequests = 1,
|
|
104
|
+
pendingRequestsForPair = 1,
|
|
105
|
+
activeRequestIdsForPair = listOf("a1", "a2"),
|
|
106
|
+
pendingRequestIdsForPair = listOf("a3"),
|
|
107
|
+
),
|
|
108
|
+
admission.snapshot("EXAMPLE.COM", "93.184.216.34"),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
firstA.release()
|
|
112
|
+
assertEquals(listOf("a1", "a2", "b1", "b2", "a3"), admitted)
|
|
113
|
+
secondA.release()
|
|
114
|
+
pendingA.release()
|
|
115
|
+
firstB.release()
|
|
116
|
+
secondB.release()
|
|
117
|
+
assertEquals(
|
|
118
|
+
SniConnectAdmissionSnapshot(0, 0, 0, 0),
|
|
119
|
+
admission.snapshot("example.com", "93.184.216.34"),
|
|
120
|
+
)
|
|
121
|
+
} finally {
|
|
122
|
+
admission.shutdownForTests()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Test
|
|
127
|
+
fun rejectsTheTwoHundredFiftySeventhPendingRequest() {
|
|
128
|
+
val admission = admission(maxActive = 1, maxPerPair = 1, maxPending = 256)
|
|
129
|
+
try {
|
|
130
|
+
val active = ticket(admission, "example.com", "93.184.216.34").also { it.submit() }
|
|
131
|
+
val pending = (0 until 256).map {
|
|
132
|
+
ticket(admission, "example.com", "93.184.216.34").also { request -> request.submit() }
|
|
133
|
+
}
|
|
134
|
+
val overflow = ticket(admission, "example.com", "93.184.216.34")
|
|
135
|
+
|
|
136
|
+
assertValidationFails { overflow.submit() }
|
|
137
|
+
assertEquals(256, admission.snapshot("example.com", "93.184.216.34").pendingRequests)
|
|
138
|
+
|
|
139
|
+
pending.forEach { request -> assertTrue(request.cancelPending()) }
|
|
140
|
+
active.release()
|
|
141
|
+
assertEquals(
|
|
142
|
+
SniConnectAdmissionSnapshot(0, 0, 0, 0),
|
|
143
|
+
admission.snapshot("example.com", "93.184.216.34"),
|
|
144
|
+
)
|
|
145
|
+
} finally {
|
|
146
|
+
admission.shutdownForTests()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Test
|
|
151
|
+
fun cancellingPendingRequestRemovesItAndSettlesImmediately() {
|
|
152
|
+
val admission = admission(maxActive = 1, maxPerPair = 1)
|
|
153
|
+
val failures = mutableListOf<Pair<String, String>>()
|
|
154
|
+
try {
|
|
155
|
+
val active = ticket(admission, "example.com", "93.184.216.34").also { it.submit() }
|
|
156
|
+
val pending = admission.createTicket(
|
|
157
|
+
hostname = "example.com",
|
|
158
|
+
ip = "93.184.216.34",
|
|
159
|
+
timeoutMillis = 10_000,
|
|
160
|
+
onAdmitted = { throw AssertionError("Cancelled request must not be admitted") },
|
|
161
|
+
onPendingFailure = { code, message -> failures += code to message },
|
|
162
|
+
).also { it.submit() }
|
|
163
|
+
|
|
164
|
+
assertTrue(pending.cancelPending())
|
|
165
|
+
assertEquals(listOf("SNI_CANCELLED" to "Request cancelled"), failures)
|
|
166
|
+
assertEquals(0, admission.snapshot("example.com", "93.184.216.34").pendingRequests)
|
|
167
|
+
assertFalse(pending.cancelPending())
|
|
168
|
+
active.release()
|
|
169
|
+
} finally {
|
|
170
|
+
admission.shutdownForTests()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@Test
|
|
175
|
+
fun cancellationBeforeSubmitSettlesOnceAndSubmitBecomesNoOp() {
|
|
176
|
+
val admission = admission(maxActive = 1, maxPerPair = 1)
|
|
177
|
+
val failures = mutableListOf<String>()
|
|
178
|
+
try {
|
|
179
|
+
val request = admission.createTicket(
|
|
180
|
+
hostname = "example.com",
|
|
181
|
+
ip = "93.184.216.34",
|
|
182
|
+
timeoutMillis = 10_000,
|
|
183
|
+
onAdmitted = { throw AssertionError("Cancelled request must not be admitted") },
|
|
184
|
+
onPendingFailure = { code, _ -> failures += code },
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assertTrue(request.cancelPending())
|
|
188
|
+
request.submit()
|
|
189
|
+
assertEquals(listOf("SNI_CANCELLED"), failures)
|
|
190
|
+
assertEquals(
|
|
191
|
+
SniConnectAdmissionSnapshot(0, 0, 0, 0),
|
|
192
|
+
admission.snapshot("example.com", "93.184.216.34"),
|
|
193
|
+
)
|
|
194
|
+
} finally {
|
|
195
|
+
admission.shutdownForTests()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Test
|
|
200
|
+
fun timeoutIncludesTimeSpentWaitingForAdmission() {
|
|
201
|
+
val admission = admission(maxActive = 1, maxPerPair = 1)
|
|
202
|
+
val timedOut = CountDownLatch(1)
|
|
203
|
+
val failureCodes = Collections.synchronizedList(mutableListOf<String>())
|
|
204
|
+
try {
|
|
205
|
+
val active = ticket(admission, "example.com", "93.184.216.34").also { it.submit() }
|
|
206
|
+
admission.createTicket(
|
|
207
|
+
hostname = "example.com",
|
|
208
|
+
ip = "93.184.216.34",
|
|
209
|
+
timeoutMillis = 40,
|
|
210
|
+
onAdmitted = { throw AssertionError("Timed-out request must not be admitted") },
|
|
211
|
+
onPendingFailure = { code, _ ->
|
|
212
|
+
failureCodes += code
|
|
213
|
+
timedOut.countDown()
|
|
214
|
+
},
|
|
215
|
+
).submit()
|
|
216
|
+
|
|
217
|
+
assertTrue(timedOut.await(2, TimeUnit.SECONDS))
|
|
218
|
+
assertEquals(listOf("SNI_REQUEST_TIMEOUT"), failureCodes.toList())
|
|
219
|
+
assertEquals(0, admission.snapshot("example.com", "93.184.216.34").pendingRequests)
|
|
220
|
+
active.release()
|
|
221
|
+
} finally {
|
|
222
|
+
admission.shutdownForTests()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@Test
|
|
227
|
+
fun admittedRequestReceivesOnlyTheRemainingTimeout() {
|
|
228
|
+
var nowNanos = 0L
|
|
229
|
+
val admission = admission(
|
|
230
|
+
maxActive = 1,
|
|
231
|
+
maxPerPair = 1,
|
|
232
|
+
nanoTime = { nowNanos },
|
|
233
|
+
)
|
|
234
|
+
val remainingTimeouts = mutableListOf<Long>()
|
|
235
|
+
try {
|
|
236
|
+
val active = ticket(admission, "example.com", "93.184.216.34").also { it.submit() }
|
|
237
|
+
val pending = admission.createTicket(
|
|
238
|
+
hostname = "example.com",
|
|
239
|
+
ip = "93.184.216.34",
|
|
240
|
+
timeoutMillis = 1_000,
|
|
241
|
+
onAdmitted = { remainingTimeout -> remainingTimeouts += remainingTimeout },
|
|
242
|
+
onPendingFailure = { code, _ -> throw AssertionError("Unexpected failure: $code") },
|
|
243
|
+
).also { it.submit() }
|
|
244
|
+
|
|
245
|
+
nowNanos = TimeUnit.MILLISECONDS.toNanos(250)
|
|
246
|
+
active.release()
|
|
247
|
+
|
|
248
|
+
assertEquals(listOf(750L), remainingTimeouts)
|
|
249
|
+
pending.release()
|
|
250
|
+
} finally {
|
|
251
|
+
admission.shutdownForTests()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private fun admission(
|
|
256
|
+
maxActive: Int,
|
|
257
|
+
maxPerPair: Int,
|
|
258
|
+
maxPending: Int = 10,
|
|
259
|
+
nanoTime: () -> Long = System::nanoTime,
|
|
260
|
+
) = SniConnectRequestAdmission(
|
|
261
|
+
maxActiveRequests = maxActive,
|
|
262
|
+
maxActiveRequestsPerPair = maxPerPair,
|
|
263
|
+
maxPendingRequests = maxPending,
|
|
264
|
+
nanoTime = nanoTime,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
private fun submit(
|
|
268
|
+
admission: SniConnectRequestAdmission,
|
|
269
|
+
id: String,
|
|
270
|
+
hostname: String,
|
|
271
|
+
ip: String,
|
|
272
|
+
admitted: MutableList<String>,
|
|
273
|
+
): SniConnectRequestAdmission.Ticket =
|
|
274
|
+
ticket(admission, hostname, ip, requestId = id, onAdmitted = { admitted += id }).also { it.submit() }
|
|
275
|
+
|
|
276
|
+
private fun ticket(
|
|
277
|
+
admission: SniConnectRequestAdmission,
|
|
278
|
+
hostname: String,
|
|
279
|
+
ip: String,
|
|
280
|
+
requestId: String? = null,
|
|
281
|
+
onAdmitted: (Long) -> Unit = {},
|
|
282
|
+
): SniConnectRequestAdmission.Ticket = admission.createTicket(
|
|
283
|
+
hostname = hostname,
|
|
284
|
+
ip = ip,
|
|
285
|
+
requestId = requestId,
|
|
286
|
+
timeoutMillis = 60_000,
|
|
287
|
+
onAdmitted = onAdmitted,
|
|
288
|
+
onPendingFailure = { _, _ -> },
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
private fun assertValidationFails(block: () -> Unit) {
|
|
292
|
+
try {
|
|
293
|
+
block()
|
|
294
|
+
} catch (_: SniConnectValidation.ValidationException) {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
throw AssertionError("Expected SNI validation failure")
|
|
298
|
+
}
|
|
299
|
+
}
|