Skip to content

Cycle detection in a multi-thread environment leads to OutOfMemoryError #1510

Closed

Description

In some scenario the cycle detection leads the application to an OutOfMemoryError.
Specifically, this seems to happen if:

  • At least one object O involved in a cyclic dependency is a Singleton
  • O has not been initialized yet
  • Multiple threads require the injection of O at the same time

The following test reproduces the issue (not always but often)

import java.util.concurrent.CountDownLatch;

import org.junit.Test;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;

public class GuiceCircularDependenciesIT extends AbstractModule {

    static interface Store {
        void sell();
    }
    
    static interface Customer {
        void buy();
    }
    
    static class StoreImpl implements Store {
        private final Customer customer;

        @Inject public StoreImpl(Customer customer) {
            this.customer = customer;
        }

        @Override
        public void sell() { }
    }

    static class CustomerImpl implements Customer {

        private Store store;

        @Inject public CustomerImpl(Store store) {
            this.store = store;
        }
        
        @Override
        public void buy() {
            store.sell();
        }
    }
    
    @Override
    protected void configure() {
        bind(Store.class).to(StoreImpl.class);
        bind(Customer.class).to(CustomerImpl.class).in(Singleton.class);
    }
    
    @Test
    public void testCircularDependencies() {
        for (int j = 0; j < 100; ++j) {
            Injector injector = Guice.createInjector(this);
            CountDownLatch latch = new CountDownLatch(8);
            for (int i = 0; i < 8; ++i) {
                Thread thread = new Thread(() -> {
                    try {
                        Customer instance = injector.getInstance(Customer.class);
                        instance.buy();
                    } catch (Exception e) {
                        System.out.println(e);
                    } finally {
                        latch.countDown();
                    }
                });
                thread.setDaemon(true);
                thread.start();
            }
            try {
                latch.await();
                System.out.println("DONE: " + j);
            } catch (InterruptedException e) {
            }
        }

    }
}

a sample output, when the issue occurs, is

DONE: 0
DONE: 1
DONE: 2
DONE: 3
DONE: 4
Exception in thread "Thread-47" java.lang.OutOfMemoryError: Java heap space
    at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
    at java.base/java.util.ArrayList.grow(ArrayList.java:238)
    at java.base/java.util.ArrayList.grow(ArrayList.java:243)
    at java.base/java.util.ArrayList.add(ArrayList.java:486)
    at java.base/java.util.ArrayList.add(ArrayList.java:499)
    at com.google.common.collect.AbstractMapBasedMultimap.put(AbstractMapBasedMultimap.java:195)
    at com.google.common.collect.AbstractListMultimap.put(AbstractListMultimap.java:115)
    at com.google.inject.internal.CycleDetectingLock$CycleDetectingLockFactory$ReentrantCycleDetectingLock.addAllLockIdsAfter(CycleDetectingLock.java:290)
    at com.google.inject.internal.CycleDetectingLock$CycleDetectingLockFactory$ReentrantCycleDetectingLock.detectPotentialLocksCycle(CycleDetectingLock.java:257)
    at com.google.inject.internal.CycleDetectingLock$CycleDetectingLockFactory$ReentrantCycleDetectingLock.lockOrDetectPotentialLocksCycle(CycleDetectingLock.java:149)
    at com.google.inject.internal.SingletonScope$1.get(SingletonScope.java:159)
    at com.google.inject.internal.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:39)
    at com.google.inject.internal.InjectorImpl$1.get(InjectorImpl.java:1094)
    at com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1131)
    at foo.GuiceCircularDependenciesIT.lambda$0(GuiceCircularDependenciesIT.java:63)
    at foo.GuiceCircularDependenciesIT$$Lambda$30/0x00000008000b9040.run(Unknown Source)
    at java.base/java.lang.Thread.run(Thread.java:834)

I'm running version 4.2.3

Please note that, unless I'm doing something wrong, this makes Guice default settings harmful if you are running it in a multi-thread application.
As a matter of fact, the only way to fix the code above is to remove the cyclic dependency, and with the circular proxy feature enabled (and it is by default) it becomes harder to spot it. Nobody (running a multi-thread application) reasonably wants to take the risk of getting an OutOfMemory Error - which is typically fatal for the application - based on race conditions - which can be hard to find while testing.
Most likely, as long as the bug is there, whoever runs a multi-thread application may want to disable the feature completely, for example calling Binder::disableCircularProxies(), to override the default behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions