View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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 org.kuali.rice.web.health;
17  
18  import bitronix.tm.resource.jdbc.PoolingDataSource;
19  import org.apache.commons.dbcp.BasicDataSource;
20  import org.apache.commons.lang.StringUtils;
21  import org.codehaus.jackson.JsonNode;
22  import org.codehaus.jackson.map.ObjectMapper;
23  import org.enhydra.jdbc.pool.StandardXAPoolDataSource;
24  import org.junit.After;
25  import org.junit.Before;
26  import org.junit.Test;
27  import org.junit.runner.RunWith;
28  import org.kuali.rice.core.api.config.property.ConfigContext;
29  import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
30  import org.kuali.rice.core.api.util.RiceConstants;
31  import org.kuali.rice.core.framework.config.property.SimpleConfig;
32  import org.kuali.rice.core.framework.persistence.platform.DatabasePlatform;
33  import org.kuali.rice.core.framework.resourceloader.BaseResourceLoader;
34  import org.kuali.rice.core.framework.resourceloader.SimpleServiceLocator;
35  import org.mockito.Mock;
36  import org.mockito.runners.MockitoJUnitRunner;
37  import org.springframework.mock.web.MockHttpServletRequest;
38  import org.springframework.mock.web.MockHttpServletResponse;
39  
40  import javax.sql.DataSource;
41  import javax.xml.namespace.QName;
42  import java.sql.Connection;
43  import java.sql.SQLException;
44  import java.sql.Statement;
45  import java.util.*;
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  
49  import static org.junit.Assert.*;
50  import static org.mockito.Mockito.mock;
51  import static org.mockito.Mockito.when;
52  
53  /**
54   * Unit test for {@link HealthServlet}
55   *
56   * @author Eric Westfall
57   */
58  @RunWith(MockitoJUnitRunner.class)
59  public class HealthServletTest {
60  
61      @Mock
62      private StandardXAPoolDataSource primaryDataSource;
63      @Mock
64      private BasicDataSource nonTransactionalDataSource;
65      @Mock
66      private PoolingDataSource serverDataSource;
67      @Mock
68      private DatabasePlatform databasePlatform;
69  
70      private SimpleConfig config;
71      private SimpleServiceLocator serviceLocator;
72      private HealthServlet healthServlet;
73  
74      @Before
75      public void setUp() throws Exception {
76          this.config = new SimpleConfig();
77          this.config.putProperty("application.id", HealthServletTest.class.getName());
78          this.config.putObject(RiceConstants.DATASOURCE_OBJ, primaryDataSource);
79          this.config.putObject(RiceConstants.NON_TRANSACTIONAL_DATASOURCE_OBJ, nonTransactionalDataSource);
80          this.config.putObject(RiceConstants.SERVER_DATASOURCE_OBJ, serverDataSource);
81          ConfigContext.init(this.config);
82          stubDataSource(primaryDataSource);
83          stubDataSource(nonTransactionalDataSource);
84          stubDataSource(serverDataSource);
85  
86          this.serviceLocator = new SimpleServiceLocator();
87          this.serviceLocator.addService(new QName(RiceConstants.DB_PLATFORM), databasePlatform);
88          GlobalResourceLoader.addResourceLoaderFirst(new BaseResourceLoader(new QName(HealthServletTest.class.getName()), this.serviceLocator));
89          GlobalResourceLoader.start();
90  
91          stubDatabasePlatform(databasePlatform);
92  
93          this.healthServlet = new HealthServlet();
94      }
95  
96      private void stubDataSource(DataSource dataSource) throws SQLException {
97          Connection connection = mock(Connection.class);
98          when(dataSource.getConnection()).thenReturn(connection);
99          Statement statement = mock(Statement.class);
100         when(connection.createStatement()).thenReturn(statement);
101 
102         if (dataSource instanceof StandardXAPoolDataSource) {
103             StandardXAPoolDataSource ds = (StandardXAPoolDataSource)dataSource;
104             when(ds.getLockedObjectCount()).thenReturn(10);
105             when(ds.getMinSize()).thenReturn(5);
106             when(ds.getMaxSize()).thenReturn(20);
107         } else if (dataSource instanceof PoolingDataSource) {
108             PoolingDataSource ds = (PoolingDataSource)dataSource;
109             when(ds.getTotalPoolSize()).thenReturn(15L);
110             when(ds.getInPoolSize()).thenReturn(5L);
111             when(ds.getMinPoolSize()).thenReturn(5);
112             when(ds.getMaxPoolSize()).thenReturn(20);
113         } else if (dataSource instanceof BasicDataSource) {
114             BasicDataSource ds = (BasicDataSource)dataSource;
115             when(ds.getNumActive()).thenReturn(10);
116             when(ds.getMinIdle()).thenReturn(5);
117             when(ds.getMaxActive()).thenReturn(20);
118         } else {
119             fail("Invalid datasource class: " + dataSource.getClass());
120         }
121 
122     }
123 
124     private void stubDatabasePlatform(DatabasePlatform platform) {
125         when(platform.getValidationQuery()).thenReturn("select 1");
126         assertEquals("select 1", platform.getValidationQuery());
127     }
128 
129     @After
130     public void tearDown() throws Exception {
131         ConfigContext.init(new SimpleConfig());
132         GlobalResourceLoader.stop();
133     }
134 
135     @Test
136     public void testService_No_Details_Ok() throws Exception {
137         healthServlet.init();
138         MockHttpServletRequest request = new MockHttpServletRequest();
139         request.setRequestURI("http://localhost:8080/rice-standalone/health");
140         request.setMethod("GET");
141         MockHttpServletResponse response = new MockHttpServletResponse();
142         healthServlet.service(request, response);
143         assertEquals("Response code should be 204", 204, response.getStatus());
144         String content = response.getContentAsString();
145         assertTrue("Content should be empty", content.isEmpty());
146     }
147 
148     @Test
149     public void testService_No_Details_Failed() throws Exception {
150         // set memory usage threshold at 0 to guarantee a failure
151         this.config.putProperty("rice.health.memory.total.usageThreshold", "0.0");
152 
153         healthServlet.init();
154         MockHttpServletRequest request = new MockHttpServletRequest();
155         request.setRequestURI("http://localhost:8080/rice-standalone/health");
156         request.setMethod("GET");
157         MockHttpServletResponse response = new MockHttpServletResponse();
158         healthServlet.service(request, response);
159         assertEquals("Response code should be 503", 503, response.getStatus());
160         String content = response.getContentAsString();
161         assertTrue("Content should be empty", content.isEmpty());
162     }
163 
164     @Test
165     public void testService_Details_Ok() throws Exception {
166         // we'll use the defaults
167         ConfigContext.init(this.config);
168 
169         MockHttpServletResponse response = initAndExecuteDetailedCheck(healthServlet);
170         assertEquals("Response code should be 200", 200, response.getStatus());
171         JsonNode root = parseContent(response.getContentAsString());
172         assertEquals("Ok", root.get("Status").asText());
173         assertFalse(root.has("Message"));
174         Map<String, String> metricMap = loadMetricMap(root);
175 
176         // database connections
177         assertEquals("true", metricMap.get("database.primary:connected"));
178         assertEquals("true", metricMap.get("database.non-transactional:connected"));
179         assertEquals("true", metricMap.get("database.server:connected"));
180 
181         // database pools
182         assertEquals("20", metricMap.get("database.primary:pool.max"));
183         assertEquals("20", metricMap.get("database.non-transactional:pool.max"));
184         assertEquals("20", metricMap.get("database.server:pool.max"));
185 
186         // buffer pool
187         //
188         // hard to know exactly what these will be in different environments, let's just make sure there is at least one
189         // key in the map that starts with "buffer-pool:"
190         assertTrue("At least one metric name should start with 'buffer-pool:'", containsKeyStartsWith("buffer-pool:", metricMap));
191 
192         // classloader
193         String classloaderLoadedValue = metricMap.get("classloader:loaded");
194         assertNotNull(classloaderLoadedValue);
195         assertTrue(Long.parseLong(classloaderLoadedValue) > 0);
196 
197         // file descriptor
198         String fileDescriptorUsageValue = metricMap.get("file-descriptor:usage");
199         assertNotNull(fileDescriptorUsageValue);
200         double fileDescriptorUsage = Double.parseDouble(fileDescriptorUsageValue);
201         assertTrue(fileDescriptorUsage > 0);
202         assertTrue(fileDescriptorUsage < 1);
203 
204         // garbage collector
205         //
206         // hard to know exactly what these will be in different environments, let's just make sure there is at least one
207         // key in the map that starts with "garbage-collector"
208         assertTrue("At least one metric name should start with 'garbage-collector:'", containsKeyStartsWith("garbage-collector:", metricMap));
209 
210         // memory
211         String totalMemoryUsageValue = metricMap.get("memory:total.usage");
212         assertNotNull(totalMemoryUsageValue);
213         double totalMemoryUsage = Double.parseDouble(totalMemoryUsageValue);
214         assertTrue(totalMemoryUsage > 0);
215         assertTrue(totalMemoryUsage < 1);
216 
217         // uptime
218         String uptimeValue = metricMap.get("runtime:uptime");
219         assertNotNull(uptimeValue);
220         assertTrue(Long.parseLong(uptimeValue) > 0);
221 
222         // threads
223         String deadlockCountValue = metricMap.get("thread:deadlock.count");
224         assertNotNull(deadlockCountValue);
225         assertEquals(0, Integer.parseInt(deadlockCountValue));
226 
227     }
228 
229     @Test
230     public void testService_Details_Failed_HeapMemoryThreshold() throws Exception {
231         // configure "rice.health.memory.heap.usageThreshold" to a threshold that we know will fail
232         this.config.putProperty(HealthServlet.Config.HEAP_MEMORY_THRESHOLD_PROPERTY, "0");
233         ConfigContext.init(this.config);
234 
235         MockHttpServletResponse response = initAndExecuteDetailedCheck(healthServlet);
236         assertEquals("Response code should be 503", 503, response.getStatus());
237         JsonNode root = parseContent(response.getContentAsString());
238         assertEquals("Failed", root.get("Status").asText());
239         assertTrue(root.has("Message"));
240     }
241 
242     // note that we don't test rice.health.memory.nonHeap.usageThreshold because the max on non-heap space is usually
243     // -1 which means there is no max. In that case the health check doesn't even run
244 
245     @Test
246     public void testService_Details_Failed_TotalMemoryThreshold() throws Exception {
247         // configure "rice.health.memory.total.usageThreshold" to a threshold that we know will fail
248         this.config.putProperty(HealthServlet.Config.TOTAL_MEMORY_THRESHOLD_PROPERTY, "0");
249         ConfigContext.init(this.config);
250         assertFailedResponse(healthServlet);
251     }
252 
253     @Test
254     public void testService_Details_Failed_DeadlockThreshold() throws Exception {
255         // configure "rice.health.thread.deadlockThreshold" to a threshold that we know will fail
256         this.config.putProperty(HealthServlet.Config.DEADLOCK_THRESHOLD_PROPERTY, "0");
257         ConfigContext.init(this.config);
258         assertFailedResponse(healthServlet);
259     }
260 
261     @Test
262     public void testService_Details_Failed_FileDescriptorThreshold() throws Exception {
263         // configure "rice.health.fileDescriptor.usageThreshold" to a threshold that we know will fail
264         this.config.putProperty(HealthServlet.Config.FILE_DESCRIPTOR_THRESHOLD_PROPERTY, "0");
265         ConfigContext.init(this.config);
266         assertFailedResponse(healthServlet);
267     }
268 
269     @Test
270     public void testService_Details_Failed_PrimaryPoolUsageThreshold() throws Exception {
271         // configure "rice.health.database.primary.connectionPoolUsageThreshold" to a threshold that we know will fail
272         this.config.putProperty(HealthServlet.Config.PRIMARY_POOL_USAGE_THRESHOLD_PROPERTY, "0");
273         ConfigContext.init(this.config);
274         assertFailedResponse(healthServlet);
275     }
276 
277     @Test
278     public void testService_Details_Failed_NonTransactionalPoolUsageThreshold() throws Exception {
279         // configure "rice.health.database.nonTransactional.connectionPoolUsageThreshold" to a threshold that we know will fail
280         this.config.putProperty(HealthServlet.Config.NON_TRANSACTIONAL_POOL_USAGE_THRESHOLD_PROPERTY, "0");
281         ConfigContext.init(this.config);
282         assertFailedResponse(healthServlet);
283     }
284 
285     @Test
286     public void testService_Details_Failed_ServerPoolUsageThreshold() throws Exception {
287         // configure "rice.health.database.server.connectionPoolUsageThreshold" to a threshold that we know will fail
288         this.config.putProperty(HealthServlet.Config.SERVER_POOL_USAGE_THRESHOLD_PROPERTY, "0");
289         ConfigContext.init(this.config);
290         assertFailedResponse(healthServlet);
291     }
292 
293     @Test
294     public void testService_Details_Multiple_Failures() throws Exception {
295         // configure all of the connection pool health checks so that they fail
296         this.config.putProperty(HealthServlet.Config.PRIMARY_POOL_USAGE_THRESHOLD_PROPERTY, "0");
297         this.config.putProperty(HealthServlet.Config.NON_TRANSACTIONAL_POOL_USAGE_THRESHOLD_PROPERTY, "0");
298         this.config.putProperty(HealthServlet.Config.SERVER_POOL_USAGE_THRESHOLD_PROPERTY, "0");
299         ConfigContext.init(this.config);
300 
301         MockHttpServletResponse response = initAndExecuteDetailedCheck(healthServlet);
302         assertEquals("Response code should be 503", 503, response.getStatus());
303         JsonNode root = parseContent(response.getContentAsString());
304         assertEquals("Failed", root.get("Status").asText());
305         assertTrue(root.has("Message"));
306         String message = root.get("Message").asText();
307         assertFalse(StringUtils.isBlank(message));
308 
309         Pattern pattern = Pattern.compile("\\* database\\.primary:pool\\.usage -> .+");
310         Matcher matcher = pattern.matcher(message);
311         assertTrue(matcher.find());
312 
313         pattern = Pattern.compile("\\* database\\.non-transactional:pool\\.usage -> .+");
314         matcher = pattern.matcher(message);
315         assertTrue(matcher.find());
316 
317         pattern = Pattern.compile("\\* database\\.server:pool\\.usage -> .+");
318         matcher = pattern.matcher(message);
319         assertTrue(matcher.find());
320 
321         pattern = Pattern.compile("\\* ");
322         matcher = pattern.matcher(message);
323         // find should return true three times because there should be three of them
324         assertTrue(matcher.find());
325         assertTrue(matcher.find());
326         assertTrue(matcher.find());
327         // we've found all occurrences, should return false on next invocation
328         assertFalse(matcher.find());
329     }
330 
331     private MockHttpServletResponse initAndExecuteDetailedCheck(HealthServlet healthServlet) throws Exception {
332         healthServlet.init();
333         MockHttpServletRequest request = new MockHttpServletRequest();
334         request.setRequestURI("http://localhost:8080/rice-standalone/health");
335         request.setMethod("GET");
336         request.setParameter("detail", "true");
337         MockHttpServletResponse response = new MockHttpServletResponse();
338         healthServlet.service(request, response);
339         String content = response.getContentAsString();
340         assertEquals("application/json", response.getContentType());
341         assertFalse(content.isEmpty());
342         return response;
343     }
344 
345     private JsonNode parseContent(String content) throws Exception {
346         ObjectMapper mapper = new ObjectMapper();
347         return mapper.readTree(content);
348 
349     }
350 
351     private Map<String, String> loadMetricMap(JsonNode root) {
352         Map<String, String> metricMap = new HashMap<>();
353         Iterator<JsonNode> metricsIt = root.get("Metrics").getElements();
354         while (metricsIt.hasNext()) {
355             JsonNode metricNode = metricsIt.next();
356             String measure = metricNode.get("Measure").asText();
357             String metric = metricNode.get("Metric").asText();
358             String value = metricNode.get("Value").asText();
359             metricMap.put(measure + ":" + metric, value);
360         }
361         return metricMap;
362     }
363 
364     private void assertFailedResponse(HealthServlet healthServlet) throws Exception {
365         MockHttpServletResponse response = initAndExecuteDetailedCheck(healthServlet);
366         assertEquals("Response code should be 503", 503, response.getStatus());
367         JsonNode root = parseContent(response.getContentAsString());
368         assertEquals("Failed", root.get("Status").asText());
369         assertTrue(root.has("Message"));
370         assertFalse(StringUtils.isBlank(root.get("Message").asText()));
371     }
372 
373     private boolean containsKeyStartsWith(String keyPrefix, Map<String, String> map) {
374         for (String name : map.keySet()) {
375             if (name.startsWith(keyPrefix)) {
376                 return true;
377             }
378         }
379         return false;
380     }
381 
382 }