From ceb08874670a3bacaf70eb3a4055cbcf8e983977 Mon Sep 17 00:00:00 2001 From: Martin Furmanski Date: Wed, 14 Mar 2018 23:10:37 +0100 Subject: [PATCH] Safe lifecycle for core life --- .../neo4j/kernel/lifecycle/SafeLifecycle.java | 195 ++++++++++++++ .../kernel/lifecycle/SafeLifecycleTest.java | 244 ++++++++++++++++++ .../causalclustering/core/state/CoreLife.java | 12 +- 3 files changed, 445 insertions(+), 6 deletions(-) create mode 100644 community/common/src/main/java/org/neo4j/kernel/lifecycle/SafeLifecycle.java create mode 100644 community/common/src/test/java/org/neo4j/kernel/lifecycle/SafeLifecycleTest.java diff --git a/community/common/src/main/java/org/neo4j/kernel/lifecycle/SafeLifecycle.java b/community/common/src/main/java/org/neo4j/kernel/lifecycle/SafeLifecycle.java new file mode 100644 index 000000000000..fba244e5e65d --- /dev/null +++ b/community/common/src/main/java/org/neo4j/kernel/lifecycle/SafeLifecycle.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.lifecycle; + +import static org.neo4j.kernel.lifecycle.SafeLifecycle.State.HALT; +import static org.neo4j.kernel.lifecycle.SafeLifecycle.State.IDLE; +import static org.neo4j.kernel.lifecycle.SafeLifecycle.State.PRE; +import static org.neo4j.kernel.lifecycle.SafeLifecycle.State.RUN; + +/** + * A safer lifecycle adapter with strict semantics and as + * a result simpler error handling and reasoning. If the + * semantics of this class is not consistent with what you + * are after, then it is probably better to create a new + * adapter with the specific semantics required. + *
+ * Guide
+ *
+ *  *: No-op    (will not invoke operation)
+ *  -: Error    (will throw IllegalStateException)
+ *
+ *  P: PRE      ("pre init")
+ *  I: IDLE     ("initialized" or "stopped")
+ *  R: RUN      ("started")
+ *  H: HALT     ("shutdown")
+ * 
+ * A successful operation is an operation not throwing an exception. + *

+ * End states on a successful operation. + *

+ *  ---------------------------------------------------
+ * | FROM \ op |  init()  start()  stop()  shutdown()  |
+ *  ---------------------------------------------------
+ * | PRE       |   IDLE      -       -       HALT(*)   |
+ * | IDLE      |    -       RUN    IDLE(*)    HALT     |
+ * | RUN       |    -        -      IDLE       -       |
+ * | HALT      |    -        -       -         -       |
+ *  ---------------------------------------------------
+ * 
+ * End states on a failed operation. + *
+ *  ---------------------------------------------------
+ * | FROM \ op |  init()  start()  stop()  shutdown()  |
+ *  ---------------------------------------------------
+ * | PRE       |   PRE       -       -       HALT(*)   |
+ * | IDLE      |    -       IDLE   IDLE(*)    HALT     |
+ * | RUN       |    -        -      IDLE       -       |
+ * | HALT      |    -        -       -         -       |
+ *  ---------------------------------------------------
+ * 
+ * A few notes: + * + * The expectation with regards to error handling and cleanup is that + * an unclean start() is cleaned up by the start0() method and thus + * the component is left in IDLE. The same goes for issues happening + * during init0(), which leaves the component in PRE. + *

+ * Because of the way that {@link LifeSupport} operates today, this + * class will ignore stop() calls made while in IDLE. Similarly, calls + * to shutdown() will be ignored while in PRE. This allows this class + * to be managed by a {@link LifeSupport} without throwing + * {@link IllegalStateException} on those state transitions, which + * otherwise would have been disallowed and handled in the same way + * as other illegal state transitions. + *

+ * This adapter will not allow a shutdown lifecycle to be reinitialized + * and started again. + */ +public abstract class SafeLifecycle implements Lifecycle +{ + private State state; + + protected SafeLifecycle() + { + this( PRE ); + } + + SafeLifecycle( State state ) + { + this.state = state; + } + + /** + * @param expected The expected from state. + * @param to The to state. + * @param op The state transition operation. + * @param force Causes the state to be updated regardless of if the operation throws. + * @throws Throwable Issues. + */ + private void transition( State expected, State to, Operation op, boolean force ) throws Throwable + { + if ( state != expected ) + { + throw new IllegalStateException( String.format( "Expected %s but was %s", expected, state ) ); + } + + if ( force ) + { + state = to; + op.run(); + } + else + { + op.run(); + state = to; + } + } + + @Override + public final synchronized void init() throws Throwable + { + transition( PRE, IDLE, this::init0, false ); + } + + @Override + public final synchronized void start() throws Throwable + { + transition( IDLE, RUN, this::start0, false ); + } + + @Override + public final synchronized void stop() throws Throwable + { + if ( state == IDLE ) + { + return; + } + transition( RUN, IDLE, this::stop0, true ); + } + + @Override + public final synchronized void shutdown() throws Throwable + { + if ( state == PRE ) + { + state = HALT; + return; + } + transition( IDLE, HALT, this::shutdown0, true ); + } + + public void init0() throws Throwable + { + } + + public void start0() throws Throwable + { + } + + public void stop0() throws Throwable + { + } + + public void shutdown0() throws Throwable + { + } + + public State state() + { + return state; + } + + enum State + { + PRE, + IDLE, + RUN, + HALT + } + + interface Operation + { + void run() throws Throwable; + } +} diff --git a/community/common/src/test/java/org/neo4j/kernel/lifecycle/SafeLifecycleTest.java b/community/common/src/test/java/org/neo4j/kernel/lifecycle/SafeLifecycleTest.java new file mode 100644 index 000000000000..b8d9a0e1ae2a --- /dev/null +++ b/community/common/src/test/java/org/neo4j/kernel/lifecycle/SafeLifecycleTest.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.lifecycle; + +import org.junit.Test; + +import org.neo4j.function.ThrowingConsumer; +import org.neo4j.kernel.lifecycle.SafeLifecycle.State; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.neo4j.kernel.lifecycle.SafeLifecycle.State.HALT; +import static org.neo4j.kernel.lifecycle.SafeLifecycle.State.IDLE; +import static org.neo4j.kernel.lifecycle.SafeLifecycle.State.PRE; +import static org.neo4j.kernel.lifecycle.SafeLifecycle.State.RUN; + +public class SafeLifecycleTest +{ + private ThrowingConsumer init = Lifecycle::init; + private ThrowingConsumer start = Lifecycle::start; + private ThrowingConsumer stop = Lifecycle::stop; + private ThrowingConsumer shutdown = Lifecycle::shutdown; + @SuppressWarnings( "unchecked" ) + private ThrowingConsumer[] ops = new ThrowingConsumer[]{init, start, stop, shutdown}; + + private Object[][] onSuccess = new Object[][]{ + // init() start() stop() shutdown() + new State[] { IDLE, null, null, HALT }, // from PRE + new State[] { null, RUN, IDLE, HALT }, // from IDLE + new State[] { null, null, IDLE, null }, // from RUN + new State[] { null, null, null, null }, // from HALT + }; + + private Object[][] onFailed = new Object[][]{ + // init() start() stop() shutdown() + new State[] { PRE, null, null, HALT }, // from PRE + new State[] { null, IDLE, IDLE, HALT }, // from IDLE + new State[] { null, null, IDLE, null }, // from RUN + new State[] { null, null, null, null }, // from HALT + }; + + private Boolean[][] ignored = new Boolean[][]{ + // init() start() stop() shutdown() + new Boolean[] { false, false, false, true }, // from PRE + new Boolean[] { false, false, true, false }, // from IDLE + new Boolean[] { false, false, false, false }, // from RUN + new Boolean[] { false, false, false, false }, // from HALT + }; + + @Test + public void shouldPerformSuccessfulTransitionsCorrectly() throws Throwable + { + for ( int state = 0; state < State.values().length; state++ ) + { + for ( int op = 0; op < ops.length; op++ ) + { + MySafeAndSuccessfulLife sf = new MySafeAndSuccessfulLife( State.values()[state] ); + boolean caughtIllegalTransition = false; + try + { + ops[op].accept( sf ); + } + catch ( IllegalStateException e ) + { + caughtIllegalTransition = true; + } + + if ( onSuccess[state][op] == null ) + { + assertTrue( caughtIllegalTransition ); + assertEquals( State.values()[state], sf.state() ); + } + else + { + assertFalse( caughtIllegalTransition ); + assertEquals( onSuccess[state][op], sf.state() ); + int expectedOpCode = ignored[state][op] ? -1 : op; + assertEquals( expectedOpCode, sf.opCode ); + } + } + } + } + + @Test + public void shouldPerformFailedTransitionsCorrectly() throws Throwable + { + for ( int state = 0; state < State.values().length; state++ ) + { + for ( int op = 0; op < ops.length; op++ ) + { + MyFailedLife sf = new MyFailedLife( State.values()[state] ); + boolean caughtIllegalTransition = false; + boolean failedOperation = false; + try + { + ops[op].accept( sf ); + } + catch ( IllegalStateException e ) + { + caughtIllegalTransition = true; + } + catch ( UnsupportedOperationException e ) + { + failedOperation = true; + } + + if ( onFailed[state][op] == null ) + { + assertTrue( caughtIllegalTransition ); + assertEquals( State.values()[state], sf.state() ); + } + else + { + assertFalse( caughtIllegalTransition ); + assertEquals( onFailed[state][op], sf.state() ); + + if ( ignored[state][op] ) + { + assertEquals( -1, sf.opCode ); + assertFalse( failedOperation ); + } + else + { + assertEquals( op, sf.opCode ); + assertTrue( failedOperation ); + } + } + } + } + } + + private static class MySafeAndSuccessfulLife extends SafeLifecycle + { + int opCode; + + MySafeAndSuccessfulLife( State state ) + { + super( state ); + opCode = -1; + } + + @Override + public void init0() + { + invoke( 0 ); + } + + @Override + public void start0() + { + invoke( 1 ); + } + + @Override + public void stop0() + { + invoke( 2 ); + } + + @Override + public void shutdown0() + { + invoke( 3 ); + } + + private void invoke( int opCode ) + { + if ( this.opCode == -1 ) + { + this.opCode = opCode; + } + else + { + throw new RuntimeException( "Double invocation" ); + } + } + } + + private static class MyFailedLife extends SafeLifecycle + { + int opCode; + + MyFailedLife( State state ) + { + super( state ); + opCode = -1; + } + + @Override + public void init0() + { + invoke( 0 ); + } + + @Override + public void start0() + { + invoke( 1 ); + } + + @Override + public void stop0() + { + invoke( 2 ); + } + + @Override + public void shutdown0() + { + invoke( 3 ); + } + + private void invoke( int opCode ) + { + if ( this.opCode == -1 ) + { + this.opCode = opCode; + throw new UnsupportedOperationException( "I made a bo-bo" ); + } + else + { + throw new RuntimeException( "Double invocation" ); + } + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/CoreLife.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/CoreLife.java index 0059b9042c57..746b083848d7 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/CoreLife.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/CoreLife.java @@ -26,9 +26,9 @@ import org.neo4j.causalclustering.identity.BoundState; import org.neo4j.causalclustering.identity.ClusterBinder; import org.neo4j.causalclustering.messaging.LifecycleMessageHandler; -import org.neo4j.kernel.lifecycle.Lifecycle; +import org.neo4j.kernel.lifecycle.SafeLifecycle; -public class CoreLife implements Lifecycle +public class CoreLife extends SafeLifecycle { private final RaftMachine raftMachine; private final LocalDatabase localDatabase; @@ -57,13 +57,13 @@ public CoreLife( RaftMachine raftMachine, } @Override - public synchronized void init() throws Throwable + public void init0() throws Throwable { localDatabase.init(); } @Override - public synchronized void start() throws Throwable + public void start0() throws Throwable { BoundState boundState = clusterBinder.bindToCluster(); raftMessageHandler.start( boundState.clusterId() ); @@ -86,7 +86,7 @@ public synchronized void start() throws Throwable } @Override - public synchronized void stop() throws Throwable + public void stop0() throws Throwable { raftMachine.stopTimers(); raftMessageHandler.stop(); @@ -95,7 +95,7 @@ public synchronized void stop() throws Throwable } @Override - public synchronized void shutdown() throws Throwable + public void shutdown0() throws Throwable { localDatabase.shutdown(); }