From 6bdecd78a6ea4f9ac505c70ad806a8749635b182 Mon Sep 17 00:00:00 2001 From: Zhen Date: Mon, 1 Aug 2016 10:32:46 +0200 Subject: [PATCH 1/2] Added `session#kill` to stop running more statements and reset the session to a clean state --- .../org/neo4j/driver/internal/InternalSession.java | 8 ++++++++ .../internal/net/ConcurrencyGuardingConnection.java | 6 ++++++ .../neo4j/driver/internal/net/SocketConnection.java | 7 +++++++ .../internal/net/pooling/PooledConnection.java | 13 +++++++++++++ .../org/neo4j/driver/internal/spi/Connection.java | 5 +++++ .../src/main/java/org/neo4j/driver/v1/Session.java | 5 ++++- .../org/neo4j/driver/v1/util/TestNeo4jSession.java | 6 ++++++ 7 files changed, 49 insertions(+), 1 deletion(-) diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalSession.java b/driver/src/main/java/org/neo4j/driver/internal/InternalSession.java index 042176e0bf..5eabb5c6e3 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/InternalSession.java +++ b/driver/src/main/java/org/neo4j/driver/internal/InternalSession.java @@ -98,6 +98,14 @@ public StatementResult run( Statement statement ) return cursor; } + public void kill() + { + ensureNoUnrecoverableError(); + ensureConnectionIsOpen(); + + connection.resetAndFlushAsync(); + } + @Override public boolean isOpen() { diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java index a054639d1b..017e282956 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java @@ -201,6 +201,12 @@ public boolean hasUnrecoverableErrors() return delegate.hasUnrecoverableErrors(); } + @Override + public void resetAndFlushAsync() + { + delegate.resetAndFlushAsync(); + } + private void markAsAvailable() { inUse.set( false ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java index df0951587b..18bd883b01 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java @@ -206,4 +206,11 @@ public boolean hasUnrecoverableErrors() { throw new UnsupportedOperationException( "Unrecoverable error detection is not supported on SocketConnection." ); } + + @Override + public void resetAndFlushAsync() + { + reset(); + flush(); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnection.java index 54784d9551..17141a32ad 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnection.java @@ -211,6 +211,19 @@ public boolean hasUnrecoverableErrors() return unrecoverableErrorsOccurred; } + @Override + public void resetAndFlushAsync() + { + try + { + delegate.resetAndFlushAsync(); + } + catch( RuntimeException e ) + { + onDelegateException( e ); + } + } + public void dispose() { delegate.close(); diff --git a/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java b/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java index a88a27910b..8294331203 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java @@ -106,4 +106,9 @@ public interface Connection extends AutoCloseable boolean hasUnrecoverableErrors(); + + /** + * Asynchronously sending reset and flush to the socket output channel. + */ + void resetAndFlushAsync(); } diff --git a/driver/src/main/java/org/neo4j/driver/v1/Session.java b/driver/src/main/java/org/neo4j/driver/v1/Session.java index e5be539dd9..ff31e06462 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Session.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Session.java @@ -58,7 +58,10 @@ public interface Session extends Resource, StatementRunner */ Transaction beginTransaction(); - + /** + * Stop running more statements in this session and rest the session to a clean state. + */ + void kill(); /** * Signal that you are done using this session. In the default driver usage, closing * and accessing sessions is very low cost, because sessions are pooled by {@link Driver}. diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java index 6afe4372b2..76632d994b 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java +++ b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java @@ -100,6 +100,12 @@ public Transaction beginTransaction() return realSession.beginTransaction(); } + @Override + public void kill() + { + realSession.kill(); + } + @Override public StatementResult run( String statementText, Map statementParameters ) { From 8e521d1d9ea2fe11b8c836ae596b2202d56c6036 Mon Sep 17 00:00:00 2001 From: Zhen Date: Wed, 3 Aug 2016 16:45:09 +0200 Subject: [PATCH 2/2] Renamed the kill method to reset Added test using long running procedures to test session.reset Made the client to not ack_failure while a reset is called asynclly but has not yet received success to avoid ack_failure on IDEL state in server --- .../driver/internal/InternalSession.java | 4 +- .../net/ConcurrencyGuardingConnection.java | 10 +- .../driver/internal/net/SocketConnection.java | 26 ++++- .../internal/net/SocketResponseHandler.java | 1 - .../net/pooling/PooledConnection.java | 12 +- .../neo4j/driver/internal/spi/Connection.java | 10 +- .../driver/internal/spi/StreamCollector.java | 27 ++++- .../java/org/neo4j/driver/v1/Session.java | 8 +- .../driver/v1/integration/SessionIT.java | 108 ++++++++++++++++++ .../org/neo4j/driver/v1/util/FileTools.java | 2 +- .../org/neo4j/driver/v1/util/TestNeo4j.java | 10 ++ .../driver/v1/util/TestNeo4jSession.java | 4 +- .../test/resources/longRunningStatement.jar | Bin 0 -> 4675 bytes 13 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 driver/src/test/resources/longRunningStatement.jar diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalSession.java b/driver/src/main/java/org/neo4j/driver/internal/InternalSession.java index 5eabb5c6e3..e0ebb45bb9 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/InternalSession.java +++ b/driver/src/main/java/org/neo4j/driver/internal/InternalSession.java @@ -98,12 +98,12 @@ public StatementResult run( Statement statement ) return cursor; } - public void kill() + public void reset() { ensureNoUnrecoverableError(); ensureConnectionIsOpen(); - connection.resetAndFlushAsync(); + connection.resetAsync(); } @Override diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java index 017e282956..89ce975d1a 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/ConcurrencyGuardingConnection.java @@ -202,9 +202,15 @@ public boolean hasUnrecoverableErrors() } @Override - public void resetAndFlushAsync() + public void resetAsync() { - delegate.resetAndFlushAsync(); + delegate.resetAsync(); + } + + @Override + public boolean isInterrupted() + { + return delegate.isInterrupted(); } private void markAsAvailable() diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java index 18bd883b01..a04502600c 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java @@ -23,6 +23,7 @@ import java.util.LinkedList; import java.util.Map; import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; import org.neo4j.driver.internal.messaging.InitMessage; import org.neo4j.driver.internal.messaging.Message; @@ -30,8 +31,8 @@ import org.neo4j.driver.internal.messaging.RunMessage; import org.neo4j.driver.internal.security.SecurityPlan; import org.neo4j.driver.internal.spi.Connection; -import org.neo4j.driver.v1.Logger; import org.neo4j.driver.internal.spi.StreamCollector; +import org.neo4j.driver.v1.Logger; import org.neo4j.driver.v1.Logging; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.ClientException; @@ -45,6 +46,7 @@ public class SocketConnection implements Connection { private final Queue pendingMessages = new LinkedList<>(); private final SocketResponseHandler responseHandler; + private AtomicBoolean interrupted = new AtomicBoolean( false ); private final SocketClient socket; @@ -208,9 +210,25 @@ public boolean hasUnrecoverableErrors() } @Override - public void resetAndFlushAsync() + public void resetAsync() { - reset(); - flush(); + if( interrupted.compareAndSet( false, true ) ) + { + queueMessage( RESET, new StreamCollector.ResetStreamCollector( new Runnable() + { + @Override + public void run() + { + interrupted.set( false ); + } + } ) ); + flush(); + } + } + + @Override + public boolean isInterrupted() + { + return interrupted.get(); } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java index 3e7e4d04ef..ea62973f20 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java @@ -244,5 +244,4 @@ public void clearError() { error = null; } - } diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnection.java b/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnection.java index 17141a32ad..f0b63859fc 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnection.java @@ -212,11 +212,11 @@ public boolean hasUnrecoverableErrors() } @Override - public void resetAndFlushAsync() + public void resetAsync() { try { - delegate.resetAndFlushAsync(); + delegate.resetAsync(); } catch( RuntimeException e ) { @@ -224,6 +224,12 @@ public void resetAndFlushAsync() } } + @Override + public boolean isInterrupted() + { + return delegate.isInterrupted(); + } + public void dispose() { delegate.close(); @@ -241,7 +247,7 @@ private void onDelegateException( RuntimeException e ) { unrecoverableErrorsOccurred = true; } - else + else if( !isInterrupted() ) { ackFailure(); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java b/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java index 8294331203..ecdbac18e0 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java +++ b/driver/src/main/java/org/neo4j/driver/internal/spi/Connection.java @@ -108,7 +108,13 @@ public interface Connection extends AutoCloseable boolean hasUnrecoverableErrors(); /** - * Asynchronously sending reset and flush to the socket output channel. + * Asynchronously sending reset to the socket output channel. */ - void resetAndFlushAsync(); + void resetAsync(); + + /** + * Return true if the current session statement execution has been interrupted by another thread, otherwise false. + * @return true if the current session statement execution has been interrupted by another thread, otherwise false + */ + boolean isInterrupted(); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/spi/StreamCollector.java b/driver/src/main/java/org/neo4j/driver/internal/spi/StreamCollector.java index aeb1fc22e9..09a459b492 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/spi/StreamCollector.java +++ b/driver/src/main/java/org/neo4j/driver/internal/spi/StreamCollector.java @@ -60,9 +60,22 @@ public void doneIgnored() } }; + StreamCollector RESET = new ResetStreamCollector(); - StreamCollector RESET = new NoOperationStreamCollector() + class ResetStreamCollector extends NoOperationStreamCollector { + private final Runnable doneSuccessCallBack; + + public ResetStreamCollector() + { + this( null ); + } + + public ResetStreamCollector( Runnable doneSuccessCallBack ) + { + this.doneSuccessCallBack = doneSuccessCallBack; + } + @Override public void doneFailure( Neo4jException error ) { @@ -76,7 +89,17 @@ public void doneIgnored() throw new ClientException( "Invalid server response message `IGNORED` received for client message `RESET`." ); } - }; + + @Override + public void doneSuccess() + { + if( doneSuccessCallBack != null ) + { + doneSuccessCallBack.run(); + } + } + } + class NoOperationStreamCollector implements StreamCollector { diff --git a/driver/src/main/java/org/neo4j/driver/v1/Session.java b/driver/src/main/java/org/neo4j/driver/v1/Session.java index ff31e06462..80a44dd44b 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Session.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Session.java @@ -59,9 +59,13 @@ public interface Session extends Resource, StatementRunner Transaction beginTransaction(); /** - * Stop running more statements in this session and rest the session to a clean state. + * Reset the current session. This sends an immediate RESET signal to the server which both interrupts + * any statement that is currently executing and ignores any subsequently queued statements. Following + * the reset, the current transaction will have been rolled back and any outstanding failures will + * have been acknowledged. */ - void kill(); + void reset(); + /** * Signal that you are done using this session. In the default driver usage, closing * and accessing sessions is very low cost, because sessions are pooled by {@link Driver}. diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java index 4ed59dcbf7..48aed49013 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionIT.java @@ -26,9 +26,17 @@ import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.GraphDatabase; import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.util.TestNeo4j; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.neo4j.driver.v1.Values.parameters; public class SessionIT { @@ -78,4 +86,104 @@ public void shouldHandleNullAuthToken() throws Throwable // Then assertFalse( session.isOpen() ); } + + @Test + public void shouldKillLongRunningStatement() throws Throwable + { + neo4j.ensureProcedures( "longRunningStatement.jar" ); + // Given + Driver driver = GraphDatabase.driver( neo4j.uri() ); + + int executionTimeout = 10; // 10s + final int killTimeout = 1; // 1s + long startTime = -1, endTime; + + try( final Session session = driver.session() ) + { + StatementResult result = + session.run( "CALL test.driver.longRunningStatement({seconds})", + parameters( "seconds", executionTimeout ) ); + + resetSessionAfterTimeout( session, killTimeout ); + + // When + startTime = System.currentTimeMillis(); + result.consume();// blocking to run the statement + + fail("Should have got an exception about statement get killed."); + } + catch( ClientException e ) + { + endTime = System.currentTimeMillis(); + assertThat( e.code(), equalTo("Neo.ClientError.Procedure.ProcedureCallFailed") ); + + assertTrue( startTime > 0 ); + assertTrue( endTime - startTime > killTimeout * 1000 ); // get killed by session.kill + assertTrue( endTime - startTime < executionTimeout * 1000 / 2 ); // finished before execution finished + } + } + + @Test + public void shouldKillLongStreamingResult() throws Throwable + { + neo4j.ensureProcedures( "longRunningStatement.jar" ); + // Given + Driver driver = GraphDatabase.driver( neo4j.uri() ); + + int executionTimeout = 10; // 10s + final int killTimeout = 1; // 1s + long startTime = -1, endTime; + int recordCount = 0; + + try( final Session session = driver.session() ) + { + StatementResult result = session.run( "CALL test.driver.longStreamingResult({seconds})", + parameters( "seconds", executionTimeout ) ); + + resetSessionAfterTimeout( session, killTimeout ); + + // When + startTime = System.currentTimeMillis(); + while( result.hasNext() ) + { + result.next(); + recordCount++; + } + + fail("Should have got an exception about statement get killed."); + } + catch( ClientException e ) + { + endTime = System.currentTimeMillis(); + assertThat( e.code(), equalTo("Neo.ClientError.Procedure.ProcedureCallFailed") ); + assertThat( recordCount, greaterThan(1) ); + + assertTrue( startTime > 0 ); + assertTrue( endTime - startTime > killTimeout * 1000 ); // get killed by session.kill + assertTrue( endTime - startTime < executionTimeout * 1000 / 2 ); // finished before execution finished + } + } + + private void resetSessionAfterTimeout( final Session session, final int timeout ) + { + new Thread( new Runnable() + { + @Override + public void run() + { + try + { + Thread.sleep( timeout * 1000 ); // let the statement executing for timeout seconds + } + catch ( InterruptedException e ) + { + e.printStackTrace(); + } + finally + { + session.reset(); // kill the session after timeout + } + } + } ).start(); + } } diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/FileTools.java b/driver/src/test/java/org/neo4j/driver/v1/util/FileTools.java index a6e22638e1..8a516133e0 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/FileTools.java +++ b/driver/src/test/java/org/neo4j/driver/v1/util/FileTools.java @@ -164,7 +164,7 @@ public static void copyFile( File srcFile, File dstFile ) throws IOException catch ( IOException e ) { // Because the message from this cause may not mention which file it's about - throw new IOException( "Could not copy '" + srcFile + "' to '" + dstFile + "'", e ); + throw new IOException( "Could not copy '" + srcFile.getCanonicalPath() + "' to '" + dstFile.getCanonicalPath() + "'", e ); } finally { diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4j.java b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4j.java index 1f32e2ce18..9b16ac72f1 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4j.java +++ b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4j.java @@ -123,4 +123,14 @@ public void updateEncryptionKeyAndCert( File key, File cert ) throws Exception FileTools.copyFile( cert, Neo4jSettings.DEFAULT_TLS_CERT_FILE ); runner.forceToRestart(); // needs to force to restart as no configuration changed } + + public void ensureProcedures( String jarName ) throws IOException + { + File procedureJar = new File( Neo4jRunner.NEO4J_HOME, "plugins/" + jarName ); + if( !procedureJar.exists() ) + { + FileTools.copyFile( new File( "src/test/resources", jarName ), procedureJar ); + runner.forceToRestart(); // needs to force to restart as no configuration changed + } + } } diff --git a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java index 76632d994b..05a63501bd 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java +++ b/driver/src/test/java/org/neo4j/driver/v1/util/TestNeo4jSession.java @@ -101,9 +101,9 @@ public Transaction beginTransaction() } @Override - public void kill() + public void reset() { - realSession.kill(); + realSession.reset(); } @Override diff --git a/driver/src/test/resources/longRunningStatement.jar b/driver/src/test/resources/longRunningStatement.jar new file mode 100644 index 0000000000000000000000000000000000000000..a5416792a74e0165ae83de9283718e21de65339c GIT binary patch literal 4675 zcmbtX2UJtp77e`%N^b@sF!V?>ARQzF2}KBmA~GNZqy-2ebVXEM+k zDFPBeKxxv86al3u{xI`L{mvi%%>0w}*19X}?tSig_rA05J`1OyWCPIB(gK14DD(i| z1Ra19V4!2FDWzu!1s`+*02F|;a2ndf8RY+-cJ@!fY@``qzy_LzdQcsNsk8xf)1bRc zA1)>Rf(b4q-qYP%X>xKvZgLhQ3F~<#3768MrJ2b_CU8hR1^GQngUNZru<7E(Yz49= zn;2RDM`=+etZ);=(+e?F5gpw5^oIP zqchqWi!wpkdte--?cMCWy%UXJ88&OMs50of7e{i|XZabh+^^rsm5~8~j*&wLKID{G z5YYxBJeRv@u5L6juiuZZOuWH);Z4^{cZRA{Pj_RZ>Jz|lOYU{~IekXE@n&)ksZI=a z64MNKf2L>XdYoMau4T=yW4hwJOF>`a+h8qb>%{`?Ft;g4y-yQfoF+Ncu6_+&XJza~R;kbFe# zW_C8tpe=Fzx+(9($c*fW@rek|f>C1=s2W9~ti^KydVXUspXFUmlez#AUJ;}l7uTsW z7kj4Vy>{8Mrm|1WLX39~B6*5=r$jM`z6@D)UxMvxFYvLO5)GqEL-)q!50-A4?X}7W zgKriTBa-e#2O;<}3$%Pf4GV{8k8>5o z);od>04O1y=|4Xog8yTmf7{q}q$`5eoa@q&6C&5lSd61;0D@srtq(2EUA59MK6y`A z7ySN>9)^}7bqwo!wQqkLxRX!3^gdqBCzn`2Tx4FTx}0@)Pv~@YucaVM1h-W8>Z69W zz>InS3Ga$0pZC`xfR>$8VZn;PmSrv`p&f=#=NAQlEnMxXWt^NTUPoDvTK8Y=l4(6T z5-n!!4CNuZ%cl=E=<`yJ4>Z&q55J+aj-y;VPsRpn?#W81CK|6s-a%AN$YkBh5PEQC zqF!UVHr||E%9)6io4tU1OCIofY%Xtmj|uw5%VvH(tL#e#~FsB#CfH#H2D9`W!n zyqWO`k7=Emea32pSVNv|cF$Iv^({TP$u&KU*BGY5d^f64$*jMd8}!E5K)Lr*Yl6n` zW=zJMG8&^5-}98L1ekc1~RhS+(KHeElKnOJL~2mG2t%uD&qP&=I?Vp~n@UYSYTNst3zQ`A|=Wtt;i+-ATI#t63PGUsIoc!QNCy zJ>If^G{|;RCfJdm(MK`V?>rj*I->e0K|;^;f;) zOs{`gL4lMokvhu8H0;Tkux=;uI!Ux1xkKG)A>+y5;Q1gUh_NAJ$pxo3tEjNf7iv!R ztjJ&f*l@Z;owWPBg&8pywaD7q0x?6W)aZ_FKJJ#vfsr@#;_gwUFmwW|i@1;5x4$*m z8my(i-6h}=KVW&62dNW{CU$N*T#*@N4L%nm1P@_zuB@30)`{*;nx^0S>0w|_p{P<# z=-aJK_1fZlMBdqUytL|#YzY~&!uJpZgiO)YeJ=2FY{&T|*wE;MykshoW80-U#a^79 zIKyO1TmP}7)zQIY)f@cD_1ZcejMn-pslmaure4$X9%2&x!?8w`hD&YpN-q+`1*AOX zH%zry6UmD2&&bYuBCg!ZJ2qN&d5?*`DUVvMC7HTMAg9+i)O=P%f@tHF)FDu->nA=N z+HK6&>?Vzl5!^{p*Kj6SN+TxKA0`UmmCLk$<- zC>y4wTQ4>-ST94>CkVR};l!1@O}2pIyGX)|&XG$CrNyRH*{e)_VNZ0c)SGN*wbR_8 z3qbi(`gV*Qc?(JgfBG;m+Rl{I_WVYttzrAGjRr$v3f++e`7cZC=Ghq;6Q4ReVq_ zSE%5RhKwB?nc5k{tz@qF_MN(4t)`i8L<#c{5xkV#XdL>gA=3{+5V*$w8XQ?^%{#s1 zU!dN}7mawuK(=EzTH|F_Se(y)Onb^>()HK zL;s5>E?2O_Y}BTOHwf!-Wze4DV@vuiZ_+U^{q)UaT8|iBCw-&AZ4|8~WxJ&!FyA>nh z9@D2-8t2XRuO1f?V+L|u5f{*4HYkq;=mCG08yLT@D9w1>rCJkhH!;xv5o2TdYTVJX<)J8INY=)#VC!_?&O5Pl*~yEu{$=%JyfT(+ zxH_!w^xG0JxU}MldT;K&Kv;VzZay$yC>2!Ub(2ZK|%LJezJR7)SkcQ))R}bTXXW}ZmyJJ!{Zbz zf^dG5cHJ~a2(`;AD=>NMs3GHXubXIT+hL^%Ve^Iq!fjO4VDdT3mE#)DgM#O&4W^dy zG=|Vg2sV=!rv3_h$#R7&q*@f7@R7x3bSWyceb-2j+$iy68OJ&KKJGGhD5Np4mS)ho zFTE;DbJA43LMy&qe3v0pkgAU=nbKYYCYfxjNJcKKIn3Y(?Rmz8V~4>s>8?izu~b=Y zTZH0S4L)4W0IP_EJN&eiqSbU86tn)CriN|Rysz+xg=MIzEM3!u(@(`?PvC&`bKZ2S zA&|r+7fu~WQAuEOK!+JIk|!AtVvYKlC&T$=>KSr=$+@^~sJ-Up17GCiXThl>NymSY zd53qhr<903KU8@o3aIZlwaweI+cCFDoaNF+9LiPQ_>Ud zX<&?6Tw12~WbnC6T!L|_hI0eg7(?pgQQl;*LWAk2m}QUQB;otpboAD-)*lxFV;4@P zv*o!eSfl}JbxTV=|I|1V;BvF^1oNExm~k%zFyU+9$LQSsXly zJ3R+0X?;xW?s-qsR?o#sEq_3yb0IuI7))t{@ETN$;X>^1D2$ID5C~hRUtVn4SpN(F z1+vrKjOS)1RPj;Mz(7vq9U*fUD+JL&oVN={jbfs9^QfqV!6Ut?-Yr|^0Vli9Gz@Vn zrU&k?Xoo*U7oGKuA>82Q>B_8!i$U{|vqvKUF`-g`%tpcXO11w8f)MJdpFJ2Zy9*V?^8U`U|QYIWI# zCj9=At?*2el3H?RFg;>V@5>-GED^T0%+BSvVOv8{j`(FgfY}!#H%o{~2HODK^}%+2qpzid&zr~g)61;28W zMvSBr^CSA-oYD7uhZMgTd8o->V}Mlim)G>0LjMSSsK{S~mF^GVKkD<3=nnPxYdHKB zy8oii-)H#t%Q{r!ukq|3X85-@f1l{vL#_2S3u(l0