Can we test an object that needs initialization and teardown?
Adam Retter
adam.retter at googlemail.com
Sun Oct 14 14:35:08 UTC 2018
I am experiencing a problem with the discussed approach of setting up
my object in the State's constructor and tearing down in the tests
@arbiter. Basically... I am seeing instances of the state object being
created which are never used for a test run. At a glance, the number
of these unused states seems to be related to the number of JVM forks
and tests executed.
For me the problem manifests because I setup my object in the State's
constructor, but as the state is never executed in a test, the
@arbiter of the test never fires and calls my shutdown method on the
state to teardown my object. I then have a bunch of objects which have
not been torn down which leaves dangling resources.
I am wondering if what I am seeing is either:
a) A bug in JCStress, i.e. Every instantiated State object SHOULD be
used by exactly one test.
b) Not a bug in JCStress. This means that I can't use the State
class's constructor for setup/initialization of my objects state
(this is probably relevant to what Jerzy Krolak is trying to
achieve!).
Code to reproduce this in JCStress very easily (with no external dependencies):
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.L_Result;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.*;
import java.text.SimpleDateFormat;
import java.util.*;
public class StartStopTest {
@State
public static class S {
public final String id = UUID.randomUUID().toString();
private final Logger logger = new Logger(id);
public S() {
logger.log("<init> S() start");
// do setup...
logger.log("<init> S() end");
}
}
@JCStressTest
@Outcome(id = "false", expect = Expect.ACCEPTABLE, desc =
"something that won't happen")
public static class startupShutdown {
@Actor
public void actor1(final S s) {
s.logger.log("actor1() start");
// do something
s.logger.log("actor1() end");
}
@Actor
public void actor2(final S s) {
s.logger.log("actor2() start");
// do something
s.logger.log("actor2() end");
}
@Arbiter
public void arbiter(final S s, final L_Result r) {
s.logger.log("arbiter() start");
// do something...
// do s.teardown...
r.r1 = true;
s.logger.log("arbiter() end");
}
}
public static class Logger {
private final String id;
private final Path logFile;
public Logger(final String id) {
this.id = id;
this.logFile = Paths.get("/tmp/StartStopTest-" + id + ".log");
try {
Files.write(logFile,
Arrays.asList(logTime(System.currentTimeMillis()) + " [" + id + " @ "
+ getPid() + " / " + Thread.currentThread().getName() + "]: <init>
Logger"), StandardOpenOption.CREATE_NEW);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private static String logTime(final long ms) {
final SimpleDateFormat sdfDate = new
SimpleDateFormat("HH:mm:ss.SSS");
return sdfDate.format(new Date(ms));
}
private String getPid() {
return ManagementFactory.getRuntimeMXBean().getName();
}
public void log(final String msg) {
try {
synchronized (logFile) {
Files.write(logFile,
Arrays.asList(logTime(System.currentTimeMillis()) + " [" + id + " @ "
+ getPid() + " / " + Thread.currentThread().getName() + "]: " + msg),
StandardOpenOption.APPEND);
}
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
}
}
If you run this with jcstress, you will find a bunch of log files in
/tmp named StartStopTest-{uuid}.log. A state that has been run through
a test creates a log with content like:
19:15:43.246 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: <init> Logger
19:15:43.246 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: <init> S() start
19:15:43.247 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: <init> S() end
19:15:43.247 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: actor1() start
19:15:43.248 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: actor1() end
19:15:43.248 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: actor2() start
19:15:43.249 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: actor2() end
19:15:43.249 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: arbiter() start
19:15:43.249 [7ee40f98-c66a-4019-94ad-8fcabe317e47 @ 11211 at bob-3.local
/ main]: arbiter() end
You can find the log files for the state instances that were not run
through tests by running:
grep -r -L "arbiter() end" /tmp/StartStopTest-*
Their content looks like:
19:15:47.330 [3cfc94cf-c430-4b40-9f03-d018cf07611f @ 11211 at bob-3.local
/ jcstress-worker-1]: <init> Logger
19:15:47.330 [3cfc94cf-c430-4b40-9f03-d018cf07611f @ 11211 at bob-3.local
/ jcstress-worker-1]: <init> S() start
19:15:47.331 [3cfc94cf-c430-4b40-9f03-d018cf07611f @ 11211 at bob-3.local
/ jcstress-worker-1]: <init> S() end
Which shows that some state instances were not used by a test, as no
actor or arbiter was recorded acting on them in the log file.
On Tue, 9 Oct 2018 at 18:24, Aleksey Shipilev <shade at redhat.com> wrote:
>
> On 10/09/2018 01:22 PM, Adam Retter wrote:
> > I just want to check whether this approach is sane? ...or if I am
> > doing something totally nuts here?
>
> Putting teardown into finally block in @Arbiter is probably okay.
>
> But the "nuts" part is assuming it is sensible to have very heavy-weight @State. The harness is able
> to capture concurrency bugs by executing lot of tests, and spending the significant time at
> setup/teardown works against that. In other words, @State-s are created by millions per test
> (because @Actor-s are basically one-shot per @State), instantiating and shutting down the database
> every time does not look very sane.
>
> -Aleksey
>
--
Adam Retter
skype: adam.retter
tweet: adamretter
http://www.adamretter.org.uk
More information about the jcstress-dev
mailing list