001 /*
002 Copyright (c) 1996-2012, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.test.dev;
031
032 import java.io.ByteArrayInputStream;
033 import java.io.DataInputStream;
034 import java.io.IOException;
035 import java.net.HttpURLConnection;
036 import java.net.URL;
037 import java.net.URLConnection;
038 import java.util.Random;
039
040 import javax.servlet.ServletContext;
041
042 import junit.framework.TestCase;
043
044 import org.hd.d.pg2k.svrCore.AbstractSimpleLogger;
045 import org.hd.d.pg2k.svrCore.AllExhibitImmutableData;
046 import org.hd.d.pg2k.svrCore.AllExhibitProperties;
047 import org.hd.d.pg2k.svrCore.CoreConsts;
048 import org.hd.d.pg2k.svrCore.Name;
049 import org.hd.d.pg2k.svrCore.PGMasterNotInServiceException;
050 import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
051 import org.hd.d.pg2k.svrCore.datasource.ExhibitDataHTTPTunnelSource;
052 import org.hd.d.pg2k.svrCore.datasource.ExhibitDataTunnelSource;
053 import org.hd.d.pg2k.svrCore.datasource.ExhibitDataTunnelSource.RawPacket;
054 import org.hd.d.pg2k.svrCore.datasource.ExhibitDataTunnelSource.RawPacket.OpCode;
055 import org.hd.d.pg2k.svrCore.datasource.SimpleExhibitPipelineIF;
056 import org.hd.d.pg2k.svrCore.props.GenProps;
057 import org.hd.d.pg2k.svrCore.vars.SimpleVariableValue;
058 import org.hd.d.pg2k.svrCore.vars.SystemVariables;
059 import org.hd.d.pg2k.webSvr.exhibit.DataSourceBean;
060
061 /**Test the HTTP communications tunnel.
062 * May need to be running in a WAR container and in master mode
063 * for this to work
064 * (unless, exceptionally, the loopbackURL is pointing at another instance).
065 */
066 public final class HTTPTunnelTest extends TestCase
067 {
068 public HTTPTunnelTest(final String name)
069 {
070 super(name);
071 }
072
073 private final SimpleLoggerIF logger = new AbstractSimpleLogger(){
074 public void log(final String message) { Main.getOut().println(message); }
075 };
076
077 /**Test that our hand-crafted serialisation routines work.
078 * This primarily means that we can decode with DataInputStream
079 * what we hand-encoded.
080 */
081 public static void testIntSerRoutines()
082 throws Exception
083 {
084 // Test for a few values...
085 for(int i = 10; --i >= 0; )
086 {
087 final int i1 = rnd.nextInt();
088 final DataInputStream dis1 = new DataInputStream(
089 new ByteArrayInputStream(
090 ExhibitDataTunnelSource.intSer(i1)));
091 assertEquals("DataInputStream.readInt() must be able to decode output of intSer()",
092 i1, dis1.readInt());
093
094 final long l1 = rnd.nextLong();
095 final DataInputStream dis2 = new DataInputStream(
096 new ByteArrayInputStream(
097 ExhibitDataTunnelSource.longSer(l1)));
098 assertEquals("DataInputStream.readInt() must be able to decode output of longSer()",
099 l1, dis2.readLong());
100 }
101 }
102
103 /**Test that stream-serialising compression works... */
104 public static void testRawPacketStreamSerialiseObject()
105 throws Exception
106 {
107 // Null object to serialise.
108 final RawPacket rp0 = RawPacket.streamSerialiseObject(OpCode.NOOP, null);
109 assertNull("Must be able to stream-serialise null object", rp0.getSerializedObjectPayload());
110
111 // Random small serialisable payload.
112 final Long l1 = rnd.nextLong();
113 // Construct packet...
114 final RawPacket rp1 = RawPacket.streamSerialiseObject(OpCode.NOOP, l1);
115 // Deserialise from packet.
116 final Object o1 = rp1.getSerializedObjectPayload();
117 assertNotNull(o1);
118 assertEquals(l1, o1);
119 assertFalse("expected a new instance from deserialisation", l1 == o1);
120
121 // Random small serialisable payload.
122 final Name n2 = Name.create("The quick brown fox jumps over the lazy dog: " + rnd.nextLong());
123 // Construct packet...
124 final RawPacket rp2 = RawPacket.streamSerialiseObject(OpCode.NOOP, n2);
125 // Deserialise from packet.
126 final Object o2 = rp2.getSerializedObjectPayload();
127 assertNotNull(o2);
128 assertEquals(n2, o2);
129 }
130
131 /**Check that we can open a basic HTTP connection OK to the loopbackURL.
132 * Returns immediately if no loopbackURL is set.
133 */
134 public static boolean canConnectToLoopbackURL()
135 {
136 final URL lb = Main.getLoopbackURL();
137 if(lb == null) { return(false); }
138 try
139 {
140 final URLConnection conn = lb.openConnection();
141 conn.setUseCaches(false); // Force new connection each time.
142 if(conn instanceof HttpURLConnection)
143 {
144 final HttpURLConnection hconn = (HttpURLConnection) conn;
145 hconn.setInstanceFollowRedirects(true); // Allow redirects.
146
147 // Check that the response is a 200 code.
148 final int respCode = hconn.getResponseCode();
149 if(respCode == HttpURLConnection.HTTP_OK)
150 { return(true); } // HTTP "OK" means loopback is OK.
151 }
152 }
153 catch(final IOException e)
154 { return(false); } // Regard this as a failure.
155
156 // Assume failure if we get here.
157 return(false);
158 }
159
160 /**If ServletContext exists and we are in master mode, return DataSource.
161 * Also, we check that basic loop-back is OK.
162 * <p>
163 * Else we return null.
164 */
165 public static DataSourceBean getDataSourceBeanIfMaster()
166 {
167 if(!canConnectToLoopbackURL()) { return(null); }
168 final ServletContext sc = (ServletContext) Main.getServletContext();
169 if(sc == null) { return(null); }
170 final DataSourceBean dsb = DataSourceBean.getApplicationInstance(sc);
171 if(!dsb.isSlave().equals(Boolean.FALSE)) { return(null); }
172 return(dsb);
173 }
174
175 /**Does basic loopback test.
176 * If not, does not cause a failure but warns that this is not possible.
177 * Mainly useful in case other tests are silently disabled by this.
178 */
179 public static void testHTTPLoopback()
180 {
181 if(!canConnectToLoopbackURL())
182 {
183 Main.getErr().println("WARNING: HTTPTunnelTest: cannot connect to loopback HTTP[S] server, some tests may be disabled: " +
184 Main.getLoopbackURL());
185 }
186
187 final DataSourceBean dsb = getDataSourceBeanIfMaster();
188 if(dsb == null)
189 {
190 Main.getErr().println("WARNING: HTTPTunnelTest: no servlet context, test skipped");
191 return;
192 }
193 }
194
195 /**Attempts basic test of tunnel by doing a NO-OP (NOOP).
196 * May require us to be in "master" mode.
197 */
198 public void testHTTPTunnelNOOP()
199 throws Exception
200 {
201 final DataSourceBean dsb = getDataSourceBeanIfMaster();
202 if(dsb == null)
203 {
204 Main.getErr().println("WARNING: HTTPTunnelTest: no servlet context, test skipped");
205 return;
206 }
207
208 final ServletContext ctxt = (ServletContext) Main.getServletContext();
209 // Get the usual URL for connecting to the master server tunnel...
210 final URL masterURL = new URL(ctxt.getInitParameter(CoreConsts.WAR_CTXTPARAM_BOOTURL));
211 // Then take the loopback URL and add the server tunnel URI...
212
213 final String tunnelLoopbackURL = Main.getLoopbackURL() +
214 masterURL.getPath();
215
216 // Now try to set up a tunnel connection.
217 final ExhibitDataTunnelSource ts =
218 new ExhibitDataHTTPTunnelSource(tunnelLoopbackURL, "test", logger);
219
220 // Check that we can successfully send a NOOP request to the server.
221 // (Throws an IOException if not.)
222 ts.doNOOP(rnd.nextBoolean());
223 }
224
225 /**Attempts to test basic use of the tunnel for normal traffic.
226 * May require us to be in "master" mode.
227 */
228 public void testTunnelBasic()
229 throws Exception
230 {
231 final DataSourceBean dsb = getDataSourceBeanIfMaster();
232 if(dsb == null)
233 {
234 Main.getErr().println("WARNING: HTTPTunnelTest: no servlet context, test skipped");
235 return;
236 }
237
238 final ServletContext ctxt = (ServletContext) Main.getServletContext();
239
240 // Get the usual URL for connecting to the master server tunnel...
241 final URL masterURL = new URL(ctxt.getInitParameter(CoreConsts.WAR_CTXTPARAM_BOOTURL));
242
243 // Then take the loopback URL and add the server tunnel URI...
244 final String tunnelLoopbackURL = Main.getLoopbackURL() +
245 masterURL.getPath();
246
247 // Now try to set up a tunnel connection.
248 final ExhibitDataTunnelSource ts =
249 new ExhibitDataHTTPTunnelSource(tunnelLoopbackURL, "test", logger);
250
251 // Check that we can successfully send a NOOP request to the server.
252 // (Throws an IOException if not.)
253 ts.doNOOP(rnd.nextBoolean());
254
255 // Now try to get the entire set of exhibit properties.
256 final AllExhibitProperties aepRemote = ts.getAllExhibitProperties(-1);
257 final AllExhibitProperties aep = dsb.getAllExhibitProperties(-1);
258 // Must not be null.
259 assertNotNull("AEP retrieved over tunnel must not be null", aepRemote);
260 // Must be equal to the set I have locally.
261 assertEquals("AEP retrieved over tunnel must match local version",
262 aep, aepRemote);
263 // But must not be the same object,
264 // it it must really have been sent over the "wire".
265 assertNotSame("AEP retrieved over tunnel must have been (de)serialised",
266 aep, aepRemote);
267
268 // Now try to get the entire set of exhibit immutable properties.
269 final AllExhibitImmutableData aeidRemote = ts.getAllExhibitImmutableData(-1);
270 final AllExhibitImmutableData aeid = dsb.getAllExhibitImmutableData(-1);
271 // Must not be null.
272 assertNotNull("AEID retrieved over tunnel must not be null", aeidRemote);
273 // Must be equal to the set I have locally.
274 assertEquals("AEID retrieved over tunnel must match local version",
275 aeid, aeidRemote);
276 // But must not be the same object,
277 // it it must really have been sent over the "wire".
278 assertNotSame("AEID retrieved over tunnel must have been (de)serialised",
279 aeid, aeidRemote);
280
281 // Now try to get the entire set of exhibit properties.
282 final GenProps gpRemote = ts.getGenProps(-1);
283 final GenProps gp = dsb.getGenProps(-1);
284 // Must not be null.
285 assertNotNull("GP retrieved over tunnel must not be null", gpRemote);
286 // NOTE: cannot test for equality because of local overrides of gp.
287 // Must not be the same object,
288 // it it must really have been sent over the "wire".
289 assertNotSame("GP retrieved over tunnel must have been (de)serialised",
290 gp, gpRemote);
291
292 // Now try to get the system ID.
293 // It should be different via the tunnel.
294 final SimpleVariableValue id1 = dsb.getVariable(SystemVariables.LOCAL_SYS_ID);
295 final SimpleVariableValue id2 = ts.getVariable(SystemVariables.LOCAL_SYS_ID);
296 assertNotNull("Should be able to retrieve system ID from DataSourceBean", id1);
297 assertNotNull("System ID from DataSourceBean should have non-null value", id1.getValue());
298 assertNotNull("Should be able to retrieve system ID from tunnel", id2);
299 assertNotNull("System ID from tunnel should have non-null value", id2.getValue());
300 assertFalse("System ID from DataSourceBean and tunnel should be different",
301 id1.getValue().equals(id2.getValue()));
302
303
304 // Test that we can set a global on either side of the tunnel
305 // and read it immediately on either side.
306 // We can't test for it being initially unset
307 // since the server may hold a value from a previous run.
308 // assertNull("Must see null for unset global on master",
309 // dsb.getVariable(SystemVariables.TEST_NUMBER_GLOBAL));
310 // assertNull("Must see null for unset global on slave",
311 // ts.getVariable(SystemVariables.TEST_NUMBER_GLOBAL));
312 // Set from global side, test on both.
313 final Number v1 = new Integer(1);
314 final SimpleVariableValue svv1 = new SimpleVariableValue(
315 SystemVariables.TEST_NUMBER_GLOBAL, v1);
316 dsb.setVariable(svv1);
317 assertEquals("Must be able to see on master variable set on master",
318 v1, dsb.getVariable(SystemVariables.TEST_NUMBER_GLOBAL).getValue());
319 assertEquals("Must be able to see on slave variable set on master",
320 v1, ts.getVariable(SystemVariables.TEST_NUMBER_GLOBAL).getValue());
321 // Now check that getVariables() is working on both sides...
322 assertTrue("Must be able to retrieve value set on master with getVariables(-1) on master",
323 SystemVariablesTest.checkValuePresent(dsb.getVariables(-1), svv1));
324 assertTrue("Must be able to retrieve value set on master with getVariables(-1) on slave",
325 SystemVariablesTest.checkValuePresent(ts.getVariables(-1), svv1));
326 // Set from slave side, test on both.
327 final Number v2 = new Short((short) 42);
328 final SimpleVariableValue svv2 = new SimpleVariableValue(
329 SystemVariables.TEST_NUMBER_GLOBAL, v2);
330 ts.setVariable(svv2);
331 assertEquals("Must be able to see on master variable set on slave",
332 v2, dsb.getVariable(SystemVariables.TEST_NUMBER_GLOBAL).getValue());
333 assertEquals("Must be able to see on slave variable set on slave",
334 v2, ts.getVariable(SystemVariables.TEST_NUMBER_GLOBAL).getValue());
335 // Now check that getVariables() is working on both sides...
336 assertTrue("Must be able to retrieve value set on slave with getVariables(-1) on master",
337 SystemVariablesTest.checkValuePresent(dsb.getVariables(-1), svv2));
338 assertTrue("Must be able to retrieve value set on slave with getVariables(-1) on slave",
339 SystemVariablesTest.checkValuePresent(ts.getVariables(-1), svv2));
340
341 // Do general pipeline and tunnel tests...
342 SystemVariablesTest.pipelineVariableTest(
343 new SimpleExhibitPipelineIF[]{ts}, // Simple tunnel pipeline.
344 null,
345 new SimpleExhibitPipelineIF[]{dsb}, // Top of master pipeline.
346 System.currentTimeMillis() + 10000L); // Run for up to 10s.
347
348 // Check that data is seen consistently on either side of the tunnel.
349 TunnelTest.checkExhibitDataConsistency(ts, dsb,
350 true, // On two sides of a tunnel.
351 System.currentTimeMillis() + 10000L); // Run for up to 10s.
352 }
353
354 /**Test behaviour against dead master.
355 * We do this by connecting to a bogus loopback address.
356 * <p>
357 * We do not have to be master for this,
358 * nor even running as a servlet...
359 * <p>
360 * We simply check that we recover from a first dead connection,
361 * then subsequent connection attempts are refused for at least
362 * the minimum failure time and not much more than the maximum...
363 * These refusals should be with a "PGMasterNotInService" exception.
364 */
365 public void testAgainstDeadMaster()
366 throws Exception
367 {
368 Main.getOut().println("Checking for correct connection back-off against failed master...");
369
370 // Bogus tunnel URL that should fail quickly (and locally/cheaply)...
371 final String bogusTunnelURL = "http://localhost:12345/bogus.noway.Jose";
372
373 // Now try to set up a tunnel connection.
374 final ExhibitDataTunnelSource ts =
375 new ExhibitDataHTTPTunnelSource(bogusTunnelURL, "", logger);
376
377 // Check that we cannot successfully send a NOOP request to the server.
378 // If this works, something is wrong!
379 try
380 {
381 ts.doNOOP(rnd.nextBoolean());
382 fail("This connection should not have succeeded!");
383 }
384 catch(final IOException e) /* We expect this to fail... */ { }
385
386 final SimpleVariableValue sysID = ts.getVariable(SystemVariables.LOCAL_SYS_ID);
387 assertNotNull("Should be able to get system ID even with master down with getVariable()",
388 sysID);
389 // Just make sure that at least one value is returned...
390 final SimpleVariableValue[] variables = ts.getVariables(-1);
391 assertTrue("Should be able to get system ID even with master down with getVariables()",
392 SystemVariablesTest.checkValuePresent(variables, sysID));
393
394
395 // Now all subsequent attempts should be deferred by a time
396 // approximately bounded by the min/max retry times.
397 // We test this out a few times...
398 for(int i = 3; --i >= 0; )
399 {
400 final long start = System.currentTimeMillis();
401
402 // Intensively try to talk to the master...
403 // We care about when we get the first "real" IOException
404 // indicating that a real connection attempt was made.
405 for( ; ; )
406 {
407 // Sleep a fairly short time before trying again...
408 try { Thread.sleep(1 + rnd.nextInt(131)); }
409 catch(final InterruptedException e) {}
410
411 try
412 {
413 // Should not be able to do NO-OP with master unreachable.
414 ts.doNOOP(false); // This should show normal deferral behaviour...
415 fail("This connection should not have succeeded!");
416 }
417 catch(final PGMasterNotInServiceException e) /* Good, deferring... */ { }
418 catch(final IOException e)
419 {
420 // We take an IOException to indicate that real I/O was attempted again...
421 final long realIOTime = System.currentTimeMillis();
422
423 // We compute how long "real" I/O was deferred...
424 final long deferralTime = realIOTime - start;
425
426 assertTrue("We must wait at least about the minimum retry time before real I/O",
427 deferralTime > ExhibitDataTunnelSource.FAIL_RETRY_WAIT_MIN_MS / 2);
428 assertTrue("We must wait at most about the maximum retry time before real I/O",
429 deferralTime < ExhibitDataTunnelSource.FAIL_RETRY_WAIT_MAX_MS * 2);
430
431 // OK, timing was reasonable.
432 break;
433 }
434 }
435 }
436
437 // Check that tunnel still behaves rationally with a dead master.
438 // Note that it is not required to be able to hold any values,
439 // though it must be able to return the system ID...
440 SystemVariablesTest.pipelineVariableTest(
441 new SimpleExhibitPipelineIF[]{ts}, null, null,
442 System.currentTimeMillis() + 10000L);
443 }
444
445 /**Private source of OK pseudo-random numbers. */
446 private static final Random rnd = new Random();
447 }