Skip to content

Conversation

jexp
Copy link
Contributor

@jexp jexp commented Mar 21, 2016

No description provided.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 21, 2016
@jexp
Copy link
Contributor Author

jexp commented Mar 21, 2016

@snicoll @philwebb this is the Spring Data Neo4j integration PR, we were just waiting for the Hopper-RC1 release.

Please have a look and let us know what you think.

Todos for after Spring-Data-Hopper goes GA:

  • Remove Neo4j M2 repository declaration

@snicoll snicoll changed the title DATAGRAPH-751 - Add Spring Data Neo4j to Spring Boot. Add Spring Data Neo4j to Spring Boot. Mar 22, 2016
@snicoll snicoll changed the title Add Spring Data Neo4j to Spring Boot. Add Spring Data Neo4j support Mar 22, 2016
@snicoll snicoll added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Mar 22, 2016
@snicoll snicoll added this to the 1.4.0.M2 milestone Mar 22, 2016
@snicoll
Copy link
Member

snicoll commented Mar 22, 2016

Can you clarify the TODO please? I am not sure I get it. Hopper RC1 is already out.

@vince-bickers
Copy link

@snicoll The TODO is done - there are no longer any references to any Neo4j snapshot repositories.

@snicoll snicoll self-assigned this Mar 22, 2016
@snicoll
Copy link
Member

snicoll commented Mar 22, 2016

Alright, I've started to review the changes and I have a few questions:

  • spring.data.neo4j.session.lifetime bugs me a bit. Is "lifetime" a concept in Neo4j or did you pick that name for the Spring Boot support? I am more than tempted to name that spring.data.neo4j.session.scope. I see in the tests that you use prototype. What are the supported scopes for the feature? Is that session, request and prototype?
  • The domain packages property confuses me. What is it? Is it the same thing as scanning the classpath for JPA entities for JPA? If so, why don't you use the same mechanism (i.e. default to application package, can be customized via @EntityScan)
  • The doc does not seem up to date with the code. If I understand well, there are two modes: one is to embed neo4j in the app and you can either be in memory or write to a file. The other mode is to connect to a remote server (in which case you need to provide the url and an optional username/password. I would like to separate that and detect the driver automatically. Is there a reason to expose the driver? Is there a third case?
  • testJpaRepositoryConfigurationWithNeo4jOverlap is ignored. Is that a temporary limitation? Can you explain?

I will push a first polish later today. Please do not commit on this branch anymore as I've changed quite a lot of things already.

snicoll pushed a commit to snicoll/spring-boot that referenced this pull request Mar 22, 2016
@vince-bickers
Copy link

  • Spring Data Neo4j is backed by an object-graph-mapping library, the Neo4j-OGM, which works in a similar way to Hibernate for JPA. Like Hibernate, the OGM has the concept of a Session, whose lifetime is ultimately determined by application code. The OGM can be used independently of Spring Data Neo4j, but in a Spring Data Neo4j application, the Session object is created as a bean instance, and the lifetime of that bean must be one of the Spring bean scopes.
  • Spring Data Neo4j does not do entity scanning itself. It delegates the responsibility to the Neo4j OGM, which does not use any Spring annotations or libraries - by design.
  • How would you suggest auto-detecting the driver the user wishes to use? Right now, there are two available drivers, the http driver and the embedded driver. Shortly a third one will be available, a binary protocol driver called Bolt. Drivers are loaded via the service loader mechanism. In theory, anybody can write and use a driver for the OGM.
  • I'll have a look at that test. It may be related to the fact that Neo4j can't participate natively in XA transactions, so managing cross-store repository operations must be left to application code.

@snicoll
Copy link
Member

snicoll commented Mar 22, 2016

Sorry, I don't think that really answers all my questions.

You named the property spring.data.neo4j.session.lifetime and it represents a scope in "our" World. So I am suggesting to name it spring.data.neo4j.session.scope and I am asking if there is anything issue with that (looks like it's ok).

It delegates the responsibility to the Neo4j OGM

Yes but we could read that info and pass it along. it's a String after all... Having said that, I have no idea how I would do that (yet) but it feel inconsistent right now.

How would you suggest auto-detecting the driver the user wishes to use?

Via configuration, something like

spring.data.neo4j.embedded.enabled=true
spring.data.neo4j.embedded.path=/var/tmp/graph.db

And if you need to go remote you'd do

spring.data.neo4j.uri=http://my-neo4j-server:7474
spring.data.neo4j.username=user
spring.data.neo4j.password=password

In the first case, we can auto-detect the embedded driver. In the second the regular http driver. And you could still perfectly force the driver via the driver property but it should be transparent for such simple use cases.

can't participate natively in XA transactions,

The test is a copy/paste of the mongo one so I don't think that's XA related.

@vince-bickers
Copy link

No problem changing lifetime to scope as far as I'm concerned.

We could auto-detect the default drivers via the uri protocol. The default driver mode is embedded in-memory if you don't specify anything at all. So maybe:

spring.data.neo4j.uri=http://my-neo4j-server:7474

spring.data.neo4j.embedded.path=/var/tmp/graph.db

would suffice.

Though, spring.data.neo4j.uri=file:///var/tmp/graph.db is consistent with the uri-based approach for embedded, which I would prefer.

@snicoll
Copy link
Member

snicoll commented Mar 22, 2016

Thanks for the reply. Yes, it's a bit harder to get things right. I'll give it some more thoughts.

@snicoll
Copy link
Member

snicoll commented Mar 22, 2016

Spring Data Neo4j does not do entity scanning itself. It delegates the responsibility to the Neo4j OGM, which does not use any Spring annotations or libraries - by design.

I've worked on a proposal that defaults to the package of the @SpringBootApplication and it can be overridden by a new @NodeEntityScan (similar to the JPA's @EntityScan). This completely removes the need for that property. I still need to polish the code and then I can move on to the driver configuration auto-detection. If you have any further ideas, please share them. Thanks!

In your `application properties`, you can supply any domain packages to be scanned by the OGM at startup
as well as the lifetime of the OGM session that will be established for web clients.

By default your application will be configured to use an in-process embedded instance of Neo4j that will not persist any data when your application shuts down. You can also connect to a remote Neo4j server, or to an embedded instance that persists data between restarts of your application.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that true? The starter does not bring the embedded driver so you need to add it yourself. What is the plan here? Should we add the embedded driver ourself? I think we should and that would be consistent with other areas where we support embedding.

In this case, the documentation is wrong. Again, no need to act on the code but please explain the intend.

ping @jexp @vince-bickers

@snicoll
Copy link
Member

snicoll commented Mar 22, 2016

A fix for DATAGRAPH-843 could improve our programming model though I managed to work-around it with a fake SessionFactoryProvider. If we can't fix that for hopper we'll probably have to live with that.

@snicoll
Copy link
Member

snicoll commented Mar 23, 2016

Team, I need some feedback.

If I add the embedded-driver and I configure it, this is what I get on startup:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.getSession' defined in org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration$SpringBootNeo4jConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.neo4j.ogm.session.Session]: Factory method 'getSession' threw exception; nested exception is java.lang.RuntimeException: java.lang.RuntimeException: Error starting org.neo4j.kernel.impl.factory.CommunityFacadeFactory, /var/folders/7h/1slxfhv518gg5pfd9lgb50hm0000gn/T/neo4j.db4558360362132306096

    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:599)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1123)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1018)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:775)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:841)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:540)
    at org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfigurationTests.load(Neo4jAutoConfigurationTests.java:78)
    at org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfigurationTests.defaultConfiguration(Neo4jAutoConfigurationTests.java:57)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.neo4j.ogm.session.Session]: Factory method 'getSession' threw exception; nested exception is java.lang.RuntimeException: java.lang.RuntimeException: Error starting org.neo4j.kernel.impl.factory.CommunityFacadeFactory, /var/folders/7h/1slxfhv518gg5pfd9lgb50hm0000gn/T/neo4j.db4558360362132306096
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:189)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:588)
    ... 41 more
Caused by: java.lang.RuntimeException: java.lang.RuntimeException: Error starting org.neo4j.kernel.impl.factory.CommunityFacadeFactory, /var/folders/7h/1slxfhv518gg5pfd9lgb50hm0000gn/T/neo4j.db4558360362132306096
    at org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver.configure(EmbeddedDriver.java:103)
    at org.neo4j.ogm.service.DriverService.load(DriverService.java:64)
    at org.neo4j.ogm.service.Components.loadDriver(Components.java:128)
    at org.neo4j.ogm.service.Components.driver(Components.java:86)
    at org.neo4j.ogm.session.SessionFactory.openSession(SessionFactory.java:79)
    at org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration$SpringBootNeo4jConfiguration.getSession(Neo4jAutoConfiguration.java:84)
    at org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration$SpringBootNeo4jConfiguration$$EnhancerBySpringCGLIB$$5c05153.CGLIB$getSession$1(<generated>)
    at org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration$SpringBootNeo4jConfiguration$$EnhancerBySpringCGLIB$$5c05153$$FastClassBySpringCGLIB$$3a680489.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:355)
    at org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration$SpringBootNeo4jConfiguration$$EnhancerBySpringCGLIB$$5c05153.getSession(<generated>)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:162)
    ... 42 more
Caused by: java.lang.RuntimeException: Error starting org.neo4j.kernel.impl.factory.CommunityFacadeFactory, /var/folders/7h/1slxfhv518gg5pfd9lgb50hm0000gn/T/neo4j.db4558360362132306096
    at org.neo4j.kernel.impl.factory.GraphDatabaseFacadeFactory.newFacade(GraphDatabaseFacadeFactory.java:143)
    at org.neo4j.kernel.impl.factory.CommunityFacadeFactory.newFacade(CommunityFacadeFactory.java:43)
    at org.neo4j.kernel.impl.factory.GraphDatabaseFacadeFactory.newFacade(GraphDatabaseFacadeFactory.java:108)
    at org.neo4j.graphdb.factory.GraphDatabaseFactory.newDatabase(GraphDatabaseFactory.java:129)
    at org.neo4j.graphdb.factory.GraphDatabaseFactory$1.newDatabase(GraphDatabaseFactory.java:117)
    at org.neo4j.graphdb.factory.GraphDatabaseBuilder.newGraphDatabase(GraphDatabaseBuilder.java:185)
    at org.neo4j.graphdb.factory.GraphDatabaseFactory.newEmbeddedDatabase(GraphDatabaseFactory.java:79)
    at org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver.configure(EmbeddedDriver.java:99)
    ... 57 more
Caused by: org.neo4j.kernel.lifecycle.LifecycleException: Component 'org.neo4j.kernel.extension.KernelExtensions@d8119c8' failed to initialize. Please see attached cause exception.
    at org.neo4j.kernel.lifecycle.LifeSupport$LifecycleInstance.init(LifeSupport.java:434)
    at org.neo4j.kernel.lifecycle.LifeSupport.init(LifeSupport.java:66)
    at org.neo4j.kernel.lifecycle.LifeSupport.start(LifeSupport.java:102)
    at org.neo4j.kernel.impl.factory.GraphDatabaseFacadeFactory.newFacade(GraphDatabaseFacadeFactory.java:139)
    ... 64 more
Caused by: java.lang.NoClassDefFoundError: org/apache/lucene/document/Fieldable
    at org.neo4j.kernel.api.impl.index.NodeRangeDocumentLabelScanStorageStrategy.<init>(NodeRangeDocumentLabelScanStorageStrategy.java:71)
    at org.neo4j.kernel.api.impl.index.LuceneLabelScanStoreExtension.newInstance(LuceneLabelScanStoreExtension.java:72)
    at org.neo4j.kernel.api.impl.index.LuceneLabelScanStoreExtension.newInstance(LuceneLabelScanStoreExtension.java:39)
    at org.neo4j.kernel.extension.KernelExtensions.init(KernelExtensions.java:69)
    at org.neo4j.kernel.lifecycle.LifeSupport$LifecycleInstance.init(LifeSupport.java:424)
    ... 67 more
Caused by: java.lang.ClassNotFoundException: org.apache.lucene.document.Fieldable
    at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 72 more

Defaulting on session scope is not a good idea as you can't tell what the application will be. Since you have a property to change that anyway, I've changed it to singleton.

Have you tried to run that PR with the embedded driver at all? How can I fix this issue?

@snicoll
Copy link
Member

snicoll commented Mar 23, 2016

looks like we can't use lucene 4 with Neo4j though this is the one we're using indirectly from other data project.

@wilkinsona
Copy link
Member

looks like we can't use lucene 4 with Neo4j though this is the one we're using indirectly from other data project.

That's a major problem for the Platform and, I would have thought, for the Spring Data release train as well. The boms become pointless if the things they contain don't work together.

@snicoll
Copy link
Member

snicoll commented Mar 23, 2016

I agree this is a major blocker and if this is confirmed, I wish I'd know before spending so much time on this.

snicoll added a commit to snicoll/spring-boot that referenced this pull request Mar 23, 2016
This commit polihes the original Neo4j contribution in several areas.

Rather than providing the packages to scan, this commit rearranges the
`EntityScan` and `EntityScanRegistrar` so that the logic can be shared
for other components. If no package is provided, scanning now defaults to
the "auto-configured" package(s) and a `@NodeEntityScan` annotation
allows to override that.

The configuration has also been updated to detect the driver based on the
`uri` property. If the embedded driver is available we use that by
default. If it is not available, we're trying to contact a Neo4j server
running on localhost. It is possible to disable the embedded mode or set
the `uri` parameter explicitly to deviate from these defaults.

Closes spring-projectsgh-5458
@vince-bickers
Copy link

The sample starter project uses the embedded driver. It declares the following dependency:

<dependency> <groupId>org.neo4j</groupId> <artifactId>neo4j-ogm-embedded-driver</artifactId> <version>2.0.0-M04</version> <scope>test</scope> </dependency>

@snicoll
Copy link
Member

snicoll commented Mar 23, 2016

If you run the sample it blows up since the test scope is not available. In any case, I don't think that's very relevant. We need to test the embedded driver and the sample is not the place for it.

The real question is why you require Lucene 3.6? It's quite old.

@vince-bickers is there a way I can get answer to the questions above?

@vince-bickers
Copy link

@snicoll. The Neo4j kernel is currently bound to lucene 3.6. They will be upgrading it soon, but @jexp is better placed to answer that question than me.

@jexp
Copy link
Contributor Author

jexp commented Mar 23, 2016

@snicoll Hi, sorry for the confusion.

Regarding the main issue which is the Lucene dependency. After consulting with @olivergierke we would like to do it similarly as SD JPA does it with Hibernate.
I.e. SDN 4.1 works with Neo4j 2.3 and 3.0 and defaults to 2.3.

But in boot we would override it with a Neo4j 3.0 dependency which then pulls in Lucene 5.5.

For your other comment: "We need to test the embedded driver and the sample is not the place for it."

Can you point me to another project that uses their embedded/in-process test-drivers in the autoconfigure - tests? So that we can learn from it and update the PR (after pulling your changes? or what is your preferred process for us updating this PR?).

Thanks a lot, Michael

@snicoll
Copy link
Member

snicoll commented Mar 23, 2016

The code is (was) ready to be merged. Sorry, I don't know neo4j so explaining that the embedded driver is GPL would have helped. I now need to rework it to exclude the dependency on the embedded driver. Sample included which means we'll ressort to what we do for Redis. You should be able to run the sample, adding a test scope is not an option.

If you want users to use Neo4j 3.0, why haven't you done that in your original submission? In other words, is it something you do because you have to or is that the target for your users base?

@jexp
Copy link
Contributor Author

jexp commented Mar 23, 2016

I thought the licensing discussion of the test dependency (embedded driver) was sorted out on the email thread. That's why the embedded driver is only a test dependency in this PR.

I'm not sure what you mean by:

Sample included which means we'll ressort to what we do for Redis. You should be able to run the sample, adding a test scope is not an option.

Neo4j 3.0 is to be GA by April 26. Currently SDN was developed mainly against Neo4j 2.3 as default dependency but the underlying integration also works with Neo4j 3.0. We didn't default to 3.0 because SD-Hopper GA is due before Neo4j 3.0 will be GA. But as Boot 1.4 GA is scheduled for end of May, Neo4j will be final by then. (I had not checked the boot release plan).

The main target for SDN 4.1 is Neo4j 2.3 (Server) but it will also work with Neo4j 3.0 for users that want to use it.

@wilkinsona
Copy link
Member

Regarding the main issue which is the Lucene dependency. After consulting with @olivergierke we would like to do it similarly as SD JPA does it with Hibernate.
I.e. SDN 4.1 works with Neo4j 2.3 and 3.0 and defaults to 2.3.

That doesn't sound like a good solution to me. It means that users of Spring Data Hopper's bom who want to use Spring Data Elasticsearch and Spring Data Neo4J at the same time will not be able to do so with the out-of-the-box configuration. What's the point of a release train if its contents don't work together?

@odrotbohm
Copy link
Member

They will. If they use Neo4j server, which users actually use usually. The embedded mode is really just a crutch to avoid the dependency to a real server. So this has been traded here in favor of adding yet another infrastructure component to the Boot build. But it's not the default way you'd use SDN 4.

@jexp
Copy link
Contributor Author

jexp commented Mar 25, 2016

I confirm that all the compile and runtime dependencies of

are all Apache License v 2.0 (or compatible) licensed.

snicoll pushed a commit that referenced this pull request Mar 25, 2016
@snicoll snicoll closed this in fd43779 Mar 25, 2016
snicoll added a commit that referenced this pull request Mar 25, 2016
* pr/5458:
  Polish contribution
  Add Neo4j support
philwebb added a commit that referenced this pull request Jun 24, 2016
Move Neo4J auto-configuration from `autoconfigure.neo4j` to
`autoconfigure.data.neo4j` since it's intrinsically linked to Spring
Data.

See gh-5458
See gh-6142
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants