Ratpack的Promise.cache在ParallelBatch中具有多个下游承诺

时间:2018-06-12 18:59:48

标签: java concurrency promise ratpack

当使用Ratpack的Promise.cache结合多个下游承诺和NullPointerException时,我在Ratpack的胆量中遇到了ParallelBatch,而且从文档中我不清楚我的用法不正确,或者如果这代表了Ratpack中的错误。

这是一个简化的测试用例,用于演示此问题:

@Test
public void foo() throws Exception {
    List<Promise<Integer>> promises = new ArrayList<>();

    for (int i = 0; i < 25; i++) {
        Promise<Integer> p = Promise.value(12);
        p = p.cache();
        promises.add(p.map(v -> v + 1));
        promises.add(p.map(v -> v + 2));
    }

    final List<Integer> results = ExecHarness.yieldSingle(c ->
            ParallelBatch.of(promises).yield()
    ).getValueOrThrow();
}

在本地运行此测试10000次导致失败率约为10/10000,NullPointerException如下所示:

java.lang.NullPointerException
    at ratpack.exec.internal.CachingUpstream.yield(CachingUpstream.java:93)
    at ratpack.exec.internal.CachingUpstream.tryDrain(CachingUpstream.java:65)
    at ratpack.exec.internal.CachingUpstream.lambda$connect$0(CachingUpstream.java:116)
    at ratpack.exec.internal.CachingUpstream$$Lambda$58/1438461739.connect(Unknown Source)
    at ratpack.exec.internal.DefaultExecution.lambda$null$2(DefaultExecution.java:122)
    at ratpack.exec.internal.DefaultExecution$$Lambda$33/2092087501.execute(Unknown Source)
    at ratpack.exec.internal.DefaultExecution$SingleEventExecStream.exec(DefaultExecution.java:489)
    at ratpack.exec.internal.DefaultExecution.exec(DefaultExecution.java:216)
    at ratpack.exec.internal.DefaultExecution.exec(DefaultExecution.java:209)
    at ratpack.exec.internal.DefaultExecution.drain(DefaultExecution.java:179)
    at ratpack.exec.internal.DefaultExecution.<init>(DefaultExecution.java:92)
    at ratpack.exec.internal.DefaultExecController$1.lambda$start$0(DefaultExecController.java:195)
    at ratpack.exec.internal.DefaultExecController$1$$Lambda$7/1411892748.call(Unknown Source)
    at io.netty.util.concurrent.PromiseTask.run(PromiseTask.java:73)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:163)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:404)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:463)
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:886)
    at ratpack.exec.internal.DefaultExecController$ExecControllerBindingThreadFactory.lambda$newThread$0(DefaultExecController.java:136)
    at ratpack.exec.internal.DefaultExecController$ExecControllerBindingThreadFactory$$Lambda$8/1157058691.run(Unknown Source)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:745)

在此测试用例中不使用cache会导致问题消失,而不会两次订阅每个缓存的promise。

我的问题是:这是对Ratpack API的错误使用,还是代表了框架中的错误?如果是前者,你能否指出我在文档中解释为什么这种用法是错误的?

1 个答案:

答案 0 :(得分:2)

即使您的示例不是缓存承诺的最佳用例(重新创建和缓存承诺,每个迭代步骤保持相同的值没有多大意义),您实际上在{{3}中找到了竞争条件错误} class。

我做了一些实验来弄清楚发生了什么,这是我的发现。首先,我创建了一个值12的承诺,它提供CachingUpstream<T>对象的自定义(更详细)实现。我使用了Promise.value(12)的正文,并且我覆盖了一个方法CachingUpstream<T>,它默认返回CachingUpstream<T>个实例:

Promise<Integer> p = new DefaultPromise<Integer>(down -> DefaultExecution.require().delimit(down::error, continuation ->
        continuation.resume(() -> down.success(12))
)) {
    @Override
    public Promise<Integer> cacheResultIf(Predicate<? super ExecResult<Integer>> shouldCache) {
        return transform(up -> {
            return new TestCachingUpstream<>(up, shouldCache.function(Duration.ofSeconds(-1), Duration.ZERO));
        });
    }
};

接下来,我只是通过复制原始类的主体创建了一个类TestCachingUpstream<T>,并添加了一些内容,例如。

  • 我让每个TestCachingUpstream<T>都有内部ID(随机UUID),以便更轻松地跟踪承诺的执行情况。
  • 在承诺执行期间发生特定事情时,我添加了一些详细的日志消息。

我还没有改变方法的实现,我只是想跟踪执行流程并保持原始实现不变。我的自定义类看起来像这样:

private static class TestCachingUpstream<T> implements Upstream<T> {
    private final String id = UUID.randomUUID().toString();

    private Upstream<? extends T> upstream;

    private final Clock clock;
    private final AtomicReference<TestCachingUpstream.Cached<? extends T>> ref = new AtomicReference<>();
    private final Function<? super ExecResult<T>, Duration> ttlFunc;

    private final AtomicBoolean pending = new AtomicBoolean();
    private final AtomicBoolean draining = new AtomicBoolean();
    private final Queue<Downstream<? super T>> waiting = PlatformDependent.newMpscQueue();

    public TestCachingUpstream(Upstream<? extends T> upstream, Function<? super ExecResult<T>, Duration> ttl) {
        this(upstream, ttl, Clock.systemUTC());
    }

    @VisibleForTesting
    TestCachingUpstream(Upstream<? extends T> upstream, Function<? super ExecResult<T>, Duration> ttl, Clock clock) {
        this.upstream = upstream;
        this.ttlFunc = ttl;
        this.clock = clock;
    }

    private void tryDrain() {
        if (draining.compareAndSet(false, true)) {
            try {
                TestCachingUpstream.Cached<? extends T> cached = ref.get();
                if (needsFetch(cached)) {
                    if (pending.compareAndSet(false, true)) {
                        Downstream<? super T> downstream = waiting.poll();

                        System.out.printf("[%s] [%s] no pending execution and downstream is %s and cached is %s...%n", id, Thread.currentThread().getName(), downstream == null ? "null" : "not null", cached);

                        if (downstream == null) {
                            pending.set(false);
                        } else {
                            try {
                                yield(downstream);
                            } catch (Throwable e) {
                                System.out.printf("[%s] [%s] calling receiveResult after catching exception %s%n", id, Thread.currentThread().getName(), e.getClass());
                                receiveResult(downstream, ExecResult.of(Result.error(e)));
                            }
                        }
                    }
                } else {
                    System.out.printf("[%s] [%s] upstream does not need fetching...%n", id, Thread.currentThread().getName());
                    Downstream<? super T> downstream = waiting.poll();
                    while (downstream != null) {
                        downstream.accept(cached.result);
                        downstream = waiting.poll();
                    }
                }
            } finally {
                draining.set(false);
            }
        }

        if (!waiting.isEmpty() && !pending.get() && needsFetch(ref.get())) {
            tryDrain();
        }
    }

    private boolean needsFetch(TestCachingUpstream.Cached<? extends T> cached) {
        return cached == null || (cached.expireAt != null && cached.expireAt.isBefore(clock.instant()));
    }

    private void yield(final Downstream<? super T> downstream) throws Exception {
        System.out.printf("[%s] [%s] calling yield... %s %n", id, Thread.currentThread().getName(), upstream == null ? "upstream is null..." : "");
        upstream.connect(new Downstream<T>() {
            public void error(Throwable throwable) {
                System.out.printf("[%s] [%s] upstream.connect.error%n", id, Thread.currentThread().getName());
                receiveResult(downstream, ExecResult.of(Result.<T>error(throwable)));
            }

            @Override
            public void success(T value) {
                System.out.printf("[%s] [%s] upstream.connect.success%n", id, Thread.currentThread().getName());
                receiveResult(downstream, ExecResult.of(Result.success(value)));
            }

            @Override
            public void complete() {
                System.out.printf("[%s] [%s] upstream.connect.complete%n", id, Thread.currentThread().getName());
                receiveResult(downstream, CompleteExecResult.get());
            }
        });
    }

    @Override
    public void connect(Downstream<? super T> downstream) throws Exception {
        TestCachingUpstream.Cached<? extends T> cached = this.ref.get();
        if (needsFetch(cached)) {
            Promise.<T>async(d -> {
                waiting.add(d);
                tryDrain();
            }).result(downstream::accept);
        } else {
            downstream.accept(cached.result);
        }
    }

    private void receiveResult(Downstream<? super T> downstream, ExecResult<T> result) {
        Duration ttl = Duration.ofSeconds(0);
        try {
            ttl = ttlFunc.apply(result);
        } catch (Throwable e) {
            if (result.isError()) {
                //noinspection ThrowableResultOfMethodCallIgnored
                result.getThrowable().addSuppressed(e);
            } else {
                result = ExecResult.of(Result.error(e));
            }
        }

        Instant expiresAt;
        if (ttl.isNegative()) {
            expiresAt = null; // eternal
            System.out.printf("[%s] [%s] releasing upstream... (%s) %n", id, Thread.currentThread().getName(), result.toString());
            upstream = null; // release
        } else if (ttl.isZero()) {
            expiresAt = clock.instant().minus(Duration.ofSeconds(1));
        } else {
            expiresAt = clock.instant().plus(ttl);
        }

        ref.set(new TestCachingUpstream.Cached<>(result, expiresAt));
        pending.set(false);

        downstream.accept(result);

        tryDrain();
    }

    static class Cached<T> {
        final ExecResult<T> result;
        final Instant expireAt;

        Cached(ExecResult<T> result, Instant expireAt) {
            this.result = result;
            this.expireAt = expireAt;
        }
    }
}

我已将for循环中的步骤数从25减少到3,以使控制台输出更简洁。

成功测试执行(无竞争条件)

让我们来看看正确执行的流程是什么样的:

[c9a70043-36b8-44f1-b8f3-dd8ce30ca0ef] [ratpack-compute-22-2] no pending execution and downstream is not null and cached is null...
[c9a70043-36b8-44f1-b8f3-dd8ce30ca0ef] [ratpack-compute-22-2] calling yield...  
[c9a70043-36b8-44f1-b8f3-dd8ce30ca0ef] [ratpack-compute-22-2] upstream.connect.success
[c9a70043-36b8-44f1-b8f3-dd8ce30ca0ef] [ratpack-compute-22-2] releasing upstream... (ExecResult{complete=false, error=null, value=12}) 
[c9a70043-36b8-44f1-b8f3-dd8ce30ca0ef] [ratpack-compute-22-2] upstream does not need fetching...
[5c740555-3638-4f3d-8a54-162d37bcb695] [ratpack-compute-22-4] no pending execution and downstream is not null and cached is null...
[5c740555-3638-4f3d-8a54-162d37bcb695] [ratpack-compute-22-4] calling yield...  
[5c740555-3638-4f3d-8a54-162d37bcb695] [ratpack-compute-22-4] upstream.connect.success
[5c740555-3638-4f3d-8a54-162d37bcb695] [ratpack-compute-22-4] releasing upstream... (ExecResult{complete=false, error=null, value=12}) 
[5c740555-3638-4f3d-8a54-162d37bcb695] [ratpack-compute-22-4] upstream does not need fetching...
[c47a8f8a-5f93-4d2f-ac18-63ed76848b9f] [ratpack-compute-22-6] no pending execution and downstream is not null and cached is null...
[c47a8f8a-5f93-4d2f-ac18-63ed76848b9f] [ratpack-compute-22-6] calling yield...  
[c47a8f8a-5f93-4d2f-ac18-63ed76848b9f] [ratpack-compute-22-6] upstream.connect.success
[c47a8f8a-5f93-4d2f-ac18-63ed76848b9f] [ratpack-compute-22-6] releasing upstream... (ExecResult{complete=false, error=null, value=12}) 
[c47a8f8a-5f93-4d2f-ac18-63ed76848b9f] [ratpack-compute-22-6] upstream does not need fetching...

正如您所看到的,每次迭代都会导致缓存的promise生成5个控制台日志行。

对于在for循环中创建的所有3个promise,这个场景重复。

测试执行失败(竞争条件)

使用那些System.out.printf()测试运行测试失败的次数少了几次,主要是因为这个I / O操作消耗了一些CPU周期,并且去同步的部分代码有几个周期以避免竞争条件。但是它仍然会发生,现在让我们来看看失败测试的输出结果如何:

[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-2] no pending execution and downstream is not null and cached is null...
[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-2] calling yield...  
[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-2] upstream.connect.success
[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-2] releasing upstream... (ExecResult{complete=false, error=null, value=12}) 
[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-3] no pending execution and downstream is not null and cached is null...
[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-3] calling yield... upstream is null... 
[4f00c50a-4706-4d22-b905-096934b7c374] [ratpack-compute-786-4] no pending execution and downstream is not null and cached is null...
[4f00c50a-4706-4d22-b905-096934b7c374] [ratpack-compute-786-4] calling yield...  
[4f00c50a-4706-4d22-b905-096934b7c374] [ratpack-compute-786-4] upstream.connect.success
[4f00c50a-4706-4d22-b905-096934b7c374] [ratpack-compute-786-4] releasing upstream... (ExecResult{complete=false, error=null, value=12}) 
[4f00c50a-4706-4d22-b905-096934b7c374] [ratpack-compute-786-4] upstream does not need fetching...
[8b27d16f-dc91-4341-b630-cf5c959c45e8] [ratpack-compute-786-6] no pending execution and downstream is not null and cached is null...
[8b27d16f-dc91-4341-b630-cf5c959c45e8] [ratpack-compute-786-6] calling yield...  
[8b27d16f-dc91-4341-b630-cf5c959c45e8] [ratpack-compute-786-6] upstream.connect.success
[8b27d16f-dc91-4341-b630-cf5c959c45e8] [ratpack-compute-786-6] releasing upstream... (ExecResult{complete=false, error=null, value=12}) 
[8b27d16f-dc91-4341-b630-cf5c959c45e8] [ratpack-compute-786-6] upstream does not need fetching...
[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-3] calling receiveResult after catching exception class java.lang.NullPointerException
[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-3] releasing upstream... (ExecResult{complete=false, error=java.lang.NullPointerException, value=null}) 

java.lang.NullPointerException
    at app.AnotherPromiseTest$TestCachingUpstream.yield(AnotherPromiseTest.java:120)
    at app.AnotherPromiseTest$TestCachingUpstream.tryDrain(AnotherPromiseTest.java:89)
    at app.AnotherPromiseTest$TestCachingUpstream.lambda$connect$0(AnotherPromiseTest.java:146)
    at ratpack.exec.internal.DefaultExecution.lambda$null$2(DefaultExecution.java:122)
    at ratpack.exec.internal.DefaultExecution$SingleEventExecStream.exec(DefaultExecution.java:489)
    at ratpack.exec.internal.DefaultExecution.exec(DefaultExecution.java:216)
    at ratpack.exec.internal.DefaultExecution.exec(DefaultExecution.java:209)
    at ratpack.exec.internal.DefaultExecution.drain(DefaultExecution.java:179)
    at ratpack.exec.internal.DefaultExecution.<init>(DefaultExecution.java:92)
    at ratpack.exec.internal.DefaultExecController$1.lambda$start$0(DefaultExecController.java:195)
    at io.netty.util.concurrent.PromiseTask.run(PromiseTask.java:73)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:163)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:404)
    at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:309)
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:886)
    at ratpack.exec.internal.DefaultExecController$ExecControllerBindingThreadFactory.lambda$newThread$0(DefaultExecController.java:136)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:748)

这是失败测试的输出 - 我在IntelliJ IDEA中运行它并且我已配置执行此测试以重复直到失败。我花了一些时间让这个测试失败了,但是经过几次测试后它最终在第1500次迭代时失败了。在这种情况下,我们可以看到竞争条件发生在for循环中创建的第一个承诺。在receiveResult()方法

中释放上游对象后,您可以看到
[088a234e-17d0-4f3a-bb7c-ec6e4a464fa2] [ratpack-compute-786-2] releasing upstream... (ExecResult{complete=false, error=null, value=12}) 

并在退出方法之前调用tryDrain,缓存的promise的下一次执行还没有看到之前的缓存结果,并且再次向下运行yield(downstream)方法。通过将upstream对象设置为null后,已释放yield(downstream)对象。并且private boolean needsFetch(TestCachingUpstream.Cached<? extends T> cached) { return cached == null || (cached.expireAt != null && cached.expireAt.isBefore(clock.instant())); } 方法期望正确初始化上游对象,否则它会抛出NPE。

我正在尝试调试方法:

StackOverflowError

这是决定是否需要获取缓存的promise的方法。但是,当我添加任何日志记录语句时,它开始导致cached.expireAt.isBefore(clock.instant())。我猜测在极少数情况下false会返回cached,因为AtomicReference对象来自import com.google.common.annotations.VisibleForTesting; import io.netty.util.internal.PlatformDependent; import org.junit.Test; import ratpack.exec.*; import ratpack.exec.internal.CompleteExecResult; import ratpack.exec.internal.DefaultExecution; import ratpack.exec.internal.DefaultPromise; import ratpack.exec.util.ParallelBatch; import ratpack.func.Function; import ratpack.func.Predicate; import ratpack.test.exec.ExecHarness; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; public class AnotherPromiseTest { @Test public void foo() throws Exception { List<Promise<Integer>> promises = new ArrayList<>(); for (int i = 0; i < 3; i++) { Promise<Integer> p = new DefaultPromise<Integer>(down -> DefaultExecution.require().delimit(down::error, continuation -> continuation.resume(() -> down.success(12)) )) { @Override public Promise<Integer> cacheResultIf(Predicate<? super ExecResult<Integer>> shouldCache) { return transform(up -> { return new TestCachingUpstream<>(up, shouldCache.function(Duration.ofSeconds(-1), Duration.ZERO)); }); } }; p = p.cache(); promises.add(p.map(v -> v + 1)); promises.add(p.map(v -> v + 2)); } ExecHarness.yieldSingle(c -> ParallelBatch.of(promises).yield()).getValueOrThrow(); } private static class TestCachingUpstream<T> implements Upstream<T> { private final String id = UUID.randomUUID().toString(); private Upstream<? extends T> upstream; private final Clock clock; private final AtomicReference<TestCachingUpstream.Cached<? extends T>> ref = new AtomicReference<>(); private final Function<? super ExecResult<T>, Duration> ttlFunc; private final AtomicBoolean pending = new AtomicBoolean(); private final AtomicBoolean draining = new AtomicBoolean(); private final Queue<Downstream<? super T>> waiting = PlatformDependent.newMpscQueue(); public TestCachingUpstream(Upstream<? extends T> upstream, Function<? super ExecResult<T>, Duration> ttl) { this(upstream, ttl, Clock.systemUTC()); } @VisibleForTesting TestCachingUpstream(Upstream<? extends T> upstream, Function<? super ExecResult<T>, Duration> ttl, Clock clock) { this.upstream = upstream; this.ttlFunc = ttl; this.clock = clock; } private void tryDrain() { if (draining.compareAndSet(false, true)) { try { TestCachingUpstream.Cached<? extends T> cached = ref.get(); if (needsFetch(cached)) { if (pending.compareAndSet(false, true)) { Downstream<? super T> downstream = waiting.poll(); System.out.printf("[%s] [%s] no pending execution and downstream is %s and cached is %s...%n", id, Thread.currentThread().getName(), downstream == null ? "null" : "not null", cached); if (downstream == null) { pending.set(false); } else { try { yield(downstream); } catch (Throwable e) { System.out.printf("[%s] [%s] calling receiveResult after catching exception %s%n", id, Thread.currentThread().getName(), e.getClass()); receiveResult(downstream, ExecResult.of(Result.error(e))); } } } } else { System.out.printf("[%s] [%s] upstream does not need fetching...%n", id, Thread.currentThread().getName()); Downstream<? super T> downstream = waiting.poll(); while (downstream != null) { downstream.accept(cached.result); downstream = waiting.poll(); } } } finally { draining.set(false); } } if (!waiting.isEmpty() && !pending.get() && needsFetch(ref.get())) { tryDrain(); } } private boolean needsFetch(TestCachingUpstream.Cached<? extends T> cached) { return cached == null || (cached.expireAt != null && cached.expireAt.isBefore(clock.instant())); } private void yield(final Downstream<? super T> downstream) throws Exception { System.out.printf("[%s] [%s] calling yield... %s %n", id, Thread.currentThread().getName(), upstream == null ? "upstream is null..." : ""); upstream.connect(new Downstream<T>() { public void error(Throwable throwable) { System.out.printf("[%s] [%s] upstream.connect.error%n", id, Thread.currentThread().getName()); receiveResult(downstream, ExecResult.of(Result.<T>error(throwable))); } @Override public void success(T value) { System.out.printf("[%s] [%s] upstream.connect.success%n", id, Thread.currentThread().getName()); receiveResult(downstream, ExecResult.of(Result.success(value))); } @Override public void complete() { System.out.printf("[%s] [%s] upstream.connect.complete%n", id, Thread.currentThread().getName()); receiveResult(downstream, CompleteExecResult.get()); } }); } @Override public void connect(Downstream<? super T> downstream) throws Exception { TestCachingUpstream.Cached<? extends T> cached = this.ref.get(); if (needsFetch(cached)) { Promise.<T>async(d -> { waiting.add(d); tryDrain(); }).result(downstream::accept); } else { downstream.accept(cached.result); } } private void receiveResult(Downstream<? super T> downstream, ExecResult<T> result) { Duration ttl = Duration.ofSeconds(0); try { ttl = ttlFunc.apply(result); } catch (Throwable e) { if (result.isError()) { //noinspection ThrowableResultOfMethodCallIgnored result.getThrowable().addSuppressed(e); } else { result = ExecResult.of(Result.error(e)); } } Instant expiresAt; if (ttl.isNegative()) { expiresAt = null; // eternal System.out.printf("[%s] [%s] releasing upstream... (%s) %n", id, Thread.currentThread().getName(), result.toString()); upstream = null; // release } else if (ttl.isZero()) { expiresAt = clock.instant().minus(Duration.ofSeconds(1)); } else { expiresAt = clock.instant().plus(ttl); } ref.set(new TestCachingUpstream.Cached<>(result, expiresAt)); pending.set(false); downstream.accept(result); tryDrain(); } static class Cached<T> { final ExecResult<T> result; final Instant expireAt; Cached(ExecResult<T> result, Instant expireAt) { this.result = result; this.expireAt = expireAt; } } } } 所以这个对象应该在方法执行之间正确传递。

这是我在实验中使用的完整测试类:

JavaMailSender

希望它有所帮助。