Search code examples
osgiliferay-7vaadin8declarative-services

OSGi DS Prototype Reference not released


I've create a simple vaadin portlet in a Liferay 7/DXP or osgi 6 context and I noticed that my References do not get garbage collected if I use osgi declarative services with a prototype scope, but they do if I use serviceObjects. Why?

Note: I've updated this question and put an even more simple example at the end.

My main component is a prototype component which has a prototype reference to an object. If I use the osgi declarative services to declare my dependency (the HelloPresenter in the following listing), then my dependency won't be released and stays in the heap forever:

import com.vaadin.server.VaadinRequest;
import com.vaadin.ui.UI;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceScope;
import org.osgi.service.component.annotations.ServiceScope;

/**
 * Created by marcel
 */
@Component(
    property = {
        "com.liferay.portlet.display-category=VaadinHelloMvp",
        "javax.portlet.display-name=VaadinHelloMvp",
        "javax.portlet.name=VaadinHelloMvp",
        "com.vaadin.osgi.liferay.portlet-ui=true"
    },
    service = UI.class,
    scope = ServiceScope.PROTOTYPE
)
public class VaadinHelloMvpPortlet extends UI {

  @Reference(scope = ReferenceScope.PROTOTYPE_REQUIRED)
  private HelloPresenter helloPresenter;

  @Override
  protected void init(VaadinRequest request) {
    this.setContent(helloPresenter.getViewComponent());
  }
}

So I've tried get my service instance for my HelloPresenter programmatically, which this works fine:

import com.vaadin.server.VaadinRequest;
import com.vaadin.ui.UI;

import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceObjects;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ServiceScope;

/**
 * Created by marcel
 */
@Component(
    property = {
        "com.liferay.portlet.display-category=VaadinHelloMvp",
        "javax.portlet.display-name=VaadinHelloMvp",
        "javax.portlet.name=VaadinHelloMvp",
        "com.vaadin.osgi.liferay.portlet-ui=true"
    },
    service = UI.class,
    scope = ServiceScope.PROTOTYPE
)
public class VaadinHelloMvpPortlet extends UI {

  private HelloPresenter helloPresenter;

  @Override
  protected void init(VaadinRequest request) {
    Bundle bundle = FrameworkUtil.getBundle(HelloPresenter.class);
    ServiceReference<HelloPresenter> serviceReference = bundle.getBundleContext().getServiceReference(HelloPresenter.class);
    ServiceObjects<HelloPresenter> serviceObjects = bundle.getBundleContext().getServiceObjects(serviceReference);
    helloPresenter = serviceObjects.getService();
    this.addDetachListener(event -> serviceObjects.ungetService(helloPresenter));
    helloPresenter.init();
    this.setContent(helloPresenter.getViewComponent());
  }
}

So I wonder why my HelloPresenter won't be released by the osgi framework in the first scenario, but it does in the second?

My portlet (UI) object is also created with

serviceObjects.getService();

and released with

serviceObjects.ungetService(uiObject);

and I tried other scenarios where I set another prototype reference in my HelloPresenter, which will also produce a reference which won't be released and garbage collected. So my experience was that, whenever you create a service object which contains a prototype reference, the reference won't get released and stucks in the jvm heap, after releasing the service object

So I got the idea that either I am doing something wrong or missed a param which makes my prototype reference never getting released OR there is something wrong with mixing osgi declarative service and serviceObjects ...

Do you know how I can make my first example work? I want to use the annotations and also be sure that they become garbage collected after closing my portlet ui.

UPDATE

I've created an even more example with a singleton component to execute a gogo shell command and a prototype object which also contains a prototype reference:

@Component(
    service = GogoShellService.class,
    scope = ServiceScope.SINGLETON,
    immediate = true,
    property =
        {
            "osgi.command.scope=test",
            "osgi.command.function=atest",
        }
)
public class GogoShellService {

  public String atest() {
    Bundle bundle = FrameworkUtil.getBundle(APrototypeComponent.class);
    ServiceReference<APrototypeComponent> serviceReference = bundle.getBundleContext().getServiceReference(APrototypeComponent.class);
    ServiceObjects<APrototypeComponent> serviceObjects = bundle.getBundleContext().getServiceObjects(serviceReference);
    APrototypeComponent service = serviceObjects.getService();
    String s = "Hello From: " + service.sayHello();
    serviceObjects.ungetService(service);
    return s;
  }
}

@Component(scope = ServiceScope.PROTOTYPE, service = APrototypeComponent.class, servicefactory = true)
public class APrototypeComponent {

  @Reference(scope = ReferenceScope.PROTOTYPE_REQUIRED)
  AProInAProComp aProInAProComp;

  public String sayHello() {

    String hello = "Hello From " + this.getClass().getSimpleName() + "(" + this.toString() + ") ";
    if (aProInAProComp != null) {
      hello += aProInAProComp.sayHello();
    }

    return hello;
  }
}

@Component(scope = ServiceScope.PROTOTYPE, service = AProInAProComp.class)
public class AProInAProComp {

  public String sayHello() {
    return "Hello From " + this.getClass().getSimpleName() + "(" + this.toString() + ")";
  }
}

Every time I execute the command (GogoShellService#atest) a new prototype instance is created and should also be destroyed afterwards, but I still can see this object in my heap and running the garbage collection doesn't clean this up...

osgi debug output is the following:

[org_apache_felix_scr:94] getService  {de.foo.bar.bax.gogo.GogoShellService}={osgi.command.function=atest, component.name=de.foo.bar.bax.gogo.GogoShellService, component.id=2944, osgi.command.scope=test, service.id=7827, service.bundleid=51, service.scope=bundle}: stack of references: [] 
APrototypeComponent(2942)] ServiceFactory.getService() 
AProInAProComp(2941)] ServiceFactory.getService() 
AProInAProComp(2941)] This thread collected dependencies 
AProInAProComp(2941)] getService (ServiceFactory) dependencies collected. 
AProInAProComp(2941)] Querying state active 
AProInAProComp(2941)] Changed state from active to active 
APrototypeComponent(2942)] This thread collected dependencies 
APrototypeComponent(2942)] getService (ServiceFactory) dependencies collected. 
APrototypeComponent(2942)] Querying state satisfied 
APrototypeComponent(2942)] For dependency aProInAProComp, optional: false; to bind: [[MultiplePrototypeRefPair: ref: [{de.foo.bar.bax.checkosgi.AProInAProComp}={component.name=de.foo.bar.bax.checkosgi.AProInAProComp, component.id=2941, service.id=7823, service.bundleid=51, service.scope=prototype}] has service: [true]]] 
APrototypeComponent(2942)] Changed state from satisfied to active 
APrototypeComponent(2942)] ServiceFactory.ungetService() 
APrototypeComponent(2942)] DependencyManager: aProInAProComp close component unbinding from org.apache.felix.scr.impl.manager.ComponentContextImpl@3927bc1d at tracking count 1 refpairs: [[MultiplePrototypeRefPair: ref: [{de.foo.bar.bax.checkosgi.AProInAProComp}={component.name=de.foo.bar.bax.checkosgi.AProInAProComp, component.id=2941, service.id=7823, service.bundleid=51, service.scope=prototype}] has service: [true]]] 
APrototypeComponent(2942)] Querying state active 
APrototypeComponent(2942)] Changed state from active to satisfied 

I don't see why my prototype instances cannot get garbage collected...


Solution

  • Update for new readers

    As of Apache Felix SCR 2.1.14 this problem should be fixed and the perfectly valid code from the original question will no longer result in erroneous behaviour.

    Original Answer

    Firstly, thank you for working to create a simple example to demonstrate your question!

    You are absolutely right that SCR should release all of your component's references after it is deactivated. For a ReferenceScope.PROTOTYPE_REQUIRED scoped reference this should result in the service instance being released and tidied up.

    Sadly it seems as though this feature of SCR hasn't been working for a while. I raised https://issues.apache.org/jira/browse/FELIX-5974 on your behalf, so it should be fixed soon, but for now the "easy" workaround is to take control of the lifecycle yourself.

    @Component(
      service = GogoShellService.class,
      scope = ServiceScope.SINGLETON,
      immediate = true,
      property =
        {
          "osgi.command.scope=test",
          "osgi.command.function=atest",
        }
      )
    public class GogoShellService {
    
      public String atest() {
        Bundle bundle = FrameworkUtil.getBundle(APrototypeComponent.class);
        ServiceReference<APrototypeComponent> serviceReference = bundle.getBundleContext().getServiceReference(APrototypeComponent.class);
        ServiceObjects<APrototypeComponent> serviceObjects = bundle.getBundleContext().getServiceObjects(serviceReference);
        APrototypeComponent service = serviceObjects.getService();
        String s = "Hello From: " + service.sayHello();
        serviceObjects.ungetService(service);
        return s;
      }
    }
    
    @Component(scope = ServiceScope.PROTOTYPE, service = APrototypeComponent.class, servicefactory = true)
    public class APrototypeComponent {
    
      @Reference(scope = ReferenceScope.PROTOTYPE_REQUIRED)
      ComponentServiceObjects<AProInAProComp> cso;
    
      AProInAProComp aProInAProComp
    
      @Activate
      void start() {
        aProInAProComp = cso.getService();
      }
    
      @Deactivate
      void stop() {
        cso.ungetService(aProInAProComp);
        aProInAProComp = null;
      }
    
      public String sayHello() {
    
        String hello = "Hello From " + this.getClass().getSimpleName() + "(" + this.toString() + ") ";
    
        if (aProInAProComp != null) {
          hello += aProInAProComp.sayHello();
        }
    
        return hello;
      }
    }
    
    @Component(scope = ServiceScope.PROTOTYPE, service = AProInAProComp.class)
    public class AProInAProComp {
    
      public String sayHello() {
        return "Hello From " + this.getClass().getSimpleName() + "(" + this.toString() + ")";
      }
    }