Skip to content

Commit ea1baee

Browse files
committed
HttpResendQueue
1 parent 5f7020e commit ea1baee

7 files changed

Lines changed: 379 additions & 3 deletions

File tree

src/main/java/com/stackify/api/common/http/HttpException.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.stackify.api.common.http;
1717

18+
import java.net.HttpURLConnection;
19+
1820
/**
1921
* HttpException
2022
* @author Eric Martin
@@ -45,4 +47,11 @@ public HttpException(int statusCode) {
4547
public int getStatusCode() {
4648
return statusCode;
4749
}
50+
51+
/**
52+
* @return True if 4xx status code
53+
*/
54+
public boolean isClientError() {
55+
return (HttpURLConnection.HTTP_BAD_REQUEST <= statusCode) && (statusCode < HttpURLConnection.HTTP_INTERNAL_ERROR);
56+
}
4857
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2015 Stackify
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.stackify.api.common.http;
17+
18+
import java.io.IOException;
19+
import java.util.Queue;
20+
import java.util.concurrent.TimeUnit;
21+
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
import com.stackify.api.common.collect.SynchronizedEvictingQueue;
26+
import com.stackify.api.common.lang.Threads;
27+
28+
/**
29+
* HttpRetransmissionQueue
30+
* @author Eric Martin
31+
*/
32+
public class HttpResendQueue {
33+
34+
/**
35+
* The service logger
36+
*/
37+
private static final Logger LOGGER = LoggerFactory.getLogger(HttpResendQueue.class);
38+
39+
/**
40+
* The queue of requests to be retransmitted
41+
*/
42+
private final Queue<byte[]> resendQueue;
43+
44+
/**
45+
* Constructor
46+
* @param maxSize Maximum size of the queue
47+
*/
48+
public HttpResendQueue(final int maxSize) {
49+
this.resendQueue = new SynchronizedEvictingQueue<byte[]>(maxSize);
50+
}
51+
52+
/**
53+
* @return Current size of the resend queue
54+
*/
55+
public int size() {
56+
return resendQueue.size();
57+
}
58+
59+
/**
60+
* Offers a failed request to the resend queue
61+
* @param request The failed request
62+
* @param e IOException
63+
*/
64+
public void offer(final byte[] request, final IOException e) {
65+
resendQueue.offer(request);
66+
}
67+
68+
/**
69+
* Offers a failed request to the resend queue
70+
* @param request The failed request
71+
* @param e HttpException
72+
*/
73+
public void offer(final byte[] request, final HttpException e) {
74+
if (!e.isClientError()) {
75+
resendQueue.offer(request);
76+
}
77+
}
78+
79+
/**
80+
* Drains the resend queue until empty or error
81+
* @param httpClient The HTTP client
82+
* @param path REST path
83+
*/
84+
public void drain(final HttpClient httpClient, final String path) {
85+
drain(httpClient, path, false);
86+
}
87+
88+
/**
89+
* Drains the resend queue until empty or error
90+
* @param httpClient The HTTP client
91+
* @param path REST path
92+
* @param gzip True if the post should be gzipped, false otherwise
93+
*/
94+
public void drain(final HttpClient httpClient, final String path, final boolean gzip) {
95+
if (!resendQueue.isEmpty()) {
96+
try {
97+
LOGGER.info("Attempting to retransmit {} requests", resendQueue.size());
98+
99+
while (!resendQueue.isEmpty()) {
100+
byte[] jsonBytes = resendQueue.peek();
101+
httpClient.post(path, jsonBytes, gzip);
102+
resendQueue.remove();
103+
Threads.sleepQuietly(250, TimeUnit.MILLISECONDS);
104+
}
105+
} catch (Throwable t) {
106+
LOGGER.info("Failure retransmitting queued requests", t);
107+
}
108+
}
109+
}
110+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2015 Stackify
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.stackify.api.common.lang;
17+
18+
import java.util.concurrent.TimeUnit;
19+
20+
/**
21+
* Threads
22+
* @author Eric Martin
23+
*/
24+
public class Threads {
25+
26+
/**
27+
* Sleeps and eats any exceptions
28+
* @param sleepFor Sleep for value
29+
* @param unit Unit of measure
30+
*/
31+
public static void sleepQuietly(final long sleepFor, final TimeUnit unit) {
32+
try {
33+
Thread.sleep(unit.toMillis(sleepFor));
34+
} catch (Throwable t) {
35+
// do nothing
36+
}
37+
}
38+
39+
/**
40+
* Hidden to prevent construction
41+
*/
42+
private Threads() {
43+
}
44+
}

src/main/java/com/stackify/api/common/log/LogSender.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
import java.io.IOException;
1919
import java.net.HttpURLConnection;
2020

21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
2124
import com.fasterxml.jackson.databind.ObjectMapper;
2225
import com.stackify.api.LogMsgGroup;
2326
import com.stackify.api.common.ApiConfiguration;
2427
import com.stackify.api.common.http.HttpClient;
2528
import com.stackify.api.common.http.HttpException;
29+
import com.stackify.api.common.http.HttpResendQueue;
2630
import com.stackify.api.common.util.Preconditions;
2731

2832
/**
@@ -31,6 +35,16 @@
3135
*/
3236
public class LogSender {
3337

38+
/**
39+
* The service logger
40+
*/
41+
private static final Logger LOGGER = LoggerFactory.getLogger(LogSender.class);
42+
43+
/**
44+
* REST path for log save
45+
*/
46+
private static final String LOG_SAVE_PATH = "/Log/Save";
47+
3448
/**
3549
* The API configuration
3650
*/
@@ -41,6 +55,11 @@ public class LogSender {
4155
*/
4256
private final ObjectMapper objectMapper;
4357

58+
/**
59+
* The queue of requests to be retransmitted (max of 100 batches of 100 messages)
60+
*/
61+
private final HttpResendQueue resendQueue = new HttpResendQueue(100);
62+
4463
/**
4564
* Default constructor
4665
* @param apiConfig API configuration
@@ -63,21 +82,31 @@ public LogSender(final ApiConfiguration apiConfig, final ObjectMapper objectMapp
6382
public int send(final LogMsgGroup group) throws IOException {
6483
Preconditions.checkNotNull(group);
6584

85+
HttpClient httpClient = new HttpClient(apiConfig);
86+
87+
// retransmit any logs on the resend queue
88+
89+
resendQueue.drain(httpClient, LOG_SAVE_PATH, true);
90+
6691
// convert to json bytes
6792

6893
byte[] jsonBytes = objectMapper.writer().writeValueAsBytes(group);
6994

7095
// post to stackify
7196

72-
HttpClient httpClient = new HttpClient(apiConfig);
73-
7497
int statusCode = HttpURLConnection.HTTP_INTERNAL_ERROR;
7598

7699
try {
77-
httpClient.post("/Log/Save", jsonBytes, true);
100+
httpClient.post(LOG_SAVE_PATH, jsonBytes, true);
78101
statusCode = HttpURLConnection.HTTP_OK;
102+
} catch (IOException t) {
103+
LOGGER.info("Queueing logs for retransmission due to IOException");
104+
resendQueue.offer(jsonBytes, t);
105+
throw t;
79106
} catch (HttpException e) {
80107
statusCode = e.getStatusCode();
108+
LOGGER.info("Queueing logs for retransmission due to HttpException", e);
109+
resendQueue.offer(jsonBytes, e);
81110
}
82111

83112
return statusCode;

src/test/java/com/stackify/api/common/http/HttpExceptionTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,16 @@ public void testConstructor() {
3434
HttpException e = new HttpException(HttpURLConnection.HTTP_BAD_REQUEST);
3535
Assert.assertEquals(HttpURLConnection.HTTP_BAD_REQUEST, e.getStatusCode());
3636
}
37+
38+
/**
39+
* testIsClientError
40+
*/
41+
@Test
42+
public void testIsClientError() {
43+
HttpException badRequest = new HttpException(HttpURLConnection.HTTP_BAD_REQUEST);
44+
Assert.assertTrue(badRequest.isClientError());
45+
46+
HttpException internalError = new HttpException(HttpURLConnection.HTTP_INTERNAL_ERROR);
47+
Assert.assertFalse(internalError.isClientError());
48+
}
3749
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2015 Stackify
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.stackify.api.common.http;
17+
18+
import java.io.IOException;
19+
import java.net.HttpURLConnection;
20+
21+
import org.junit.Assert;
22+
import org.junit.Test;
23+
import org.mockito.Mockito;
24+
25+
/**
26+
* HttpResendQueue JUnit Test
27+
* @author Eric Martin
28+
*/
29+
public class HttpResendQueueTest {
30+
31+
/**
32+
* testOfferIOException
33+
*/
34+
@Test
35+
public void testOfferIOException() {
36+
HttpResendQueue resendQueue = new HttpResendQueue(3);
37+
Assert.assertEquals(0, resendQueue.size());
38+
39+
resendQueue.offer(new byte[]{}, new IOException());
40+
Assert.assertEquals(1, resendQueue.size());
41+
}
42+
43+
/**
44+
* testOfferHttpException
45+
*/
46+
@Test
47+
public void testOfferHttpException() {
48+
HttpResendQueue resendQueue = new HttpResendQueue(3);
49+
Assert.assertEquals(0, resendQueue.size());
50+
51+
resendQueue.offer(new byte[]{}, new HttpException(HttpURLConnection.HTTP_INTERNAL_ERROR));
52+
Assert.assertEquals(1, resendQueue.size());
53+
54+
resendQueue.offer(new byte[]{}, new HttpException(HttpURLConnection.HTTP_UNAUTHORIZED));
55+
Assert.assertEquals(1, resendQueue.size());
56+
}
57+
58+
/**
59+
* testDrain
60+
* @throws Exception
61+
*/
62+
@Test
63+
public void testDrain() throws Exception {
64+
byte[] request1 = new byte[]{1};
65+
byte[] request2 = new byte[]{2};
66+
67+
HttpResendQueue resendQueue = new HttpResendQueue(3);
68+
resendQueue.offer(request1, new IOException());
69+
resendQueue.offer(request2, new IOException());
70+
71+
Assert.assertEquals(2, resendQueue.size());
72+
73+
HttpClient httpClient = Mockito.mock(HttpClient.class);
74+
75+
resendQueue.drain(httpClient, "/path");
76+
77+
Assert.assertEquals(0, resendQueue.size());
78+
79+
Mockito.verify(httpClient).post("/path", request1, false);
80+
Mockito.verify(httpClient).post("/path", request2, false);
81+
}
82+
83+
/**
84+
* testDrainWithException
85+
* @throws Exception
86+
*/
87+
@Test
88+
public void testDrainWithException() throws Exception {
89+
byte[] request1 = new byte[]{1};
90+
byte[] request2 = new byte[]{2};
91+
92+
HttpResendQueue resendQueue = new HttpResendQueue(3);
93+
resendQueue.offer(request1, new IOException());
94+
resendQueue.offer(request2, new IOException());
95+
96+
Assert.assertEquals(2, resendQueue.size());
97+
98+
HttpClient httpClient = Mockito.mock(HttpClient.class);
99+
Mockito.when(httpClient.post("/path", request1, false)).thenReturn("");
100+
Mockito.when(httpClient.post("/path", request2, false)).thenThrow(new RuntimeException()).thenReturn("");
101+
102+
resendQueue.drain(httpClient, "/path");
103+
104+
Assert.assertEquals(1, resendQueue.size());
105+
106+
Mockito.verify(httpClient).post("/path", request1, false);
107+
Mockito.verify(httpClient).post("/path", request2, false);
108+
109+
resendQueue.drain(httpClient, "/path");
110+
111+
Mockito.verify(httpClient, Mockito.times(2)).post("/path", request2, false);
112+
113+
Assert.assertEquals(0, resendQueue.size());
114+
}
115+
}

0 commit comments

Comments
 (0)