Skip to content

MappingInstantiationException for Abstract Classes using tasks ran on ForkJoinPool #1691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
cabbonizio opened this issue Mar 14, 2023 · 26 comments · Fixed by #1705
Closed

MappingInstantiationException for Abstract Classes using tasks ran on ForkJoinPool #1691

cabbonizio opened this issue Mar 14, 2023 · 26 comments · Fixed by #1705
Labels
type: bug A general bug

Comments

@cabbonizio
Copy link

cabbonizio commented Mar 14, 2023

Hi, our team has had a really tough problem we been dealing with for some time and been trying to get to root cause. We have an issue with a particular service where it begins failing with the following exception:

Exception:
org.springframework.data.mapping.model.MappingInstantiationException

Message:

Failed to instantiate com.qvc.common.couchbase.model.cart.payment.AbstractPaymentMethod using constructor public com.qvc.common.couchbase.model.cart.payment.AbstractPaymentMethod() with arguments

Stacktrace:

org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator$MappingInstantiationExceptionEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:358)
org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:102)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.read(MappingCouchbaseConverter.java:266)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.read(MappingCouchbaseConverter.java:244)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.readCollection(MappingCouchbaseConverter.java:773)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.readValue(MappingCouchbaseConverter.java:856)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.access$300(MappingCouchbaseConverter.java:86)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter$CouchbasePropertyValueProvider.getPropertyValue(MappingCouchbaseConverter.java:972)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.getValueInternal(MappingCouchbaseConverter.java:309)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter$1.doWithPersistentProperty(MappingCouchbaseConverter.java:277)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter$1.doWithPersistentProperty(MappingCouchbaseConverter.java:269)
org.springframework.data.mapping.model.BasicPersistentEntity.doWithProperties(BasicPersistentEntity.java:368)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.read(MappingCouchbaseConverter.java:269)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.read(MappingCouchbaseConverter.java:244)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.read(MappingCouchbaseConverter.java:201)
org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter.read(MappingCouchbaseConverter.java:86)
org.springframework.data.couchbase.core.CouchbaseTemplateSupport.decodeEntity(CouchbaseTemplateSupport.java:141)
org.springframework.data.couchbase.core.NonReactiveSupportWrapper.lambda$decodeEntity$1(NonReactiveSupportWrapper.java:45)
reactor.core.publisher.MonoSupplier.call(MonoSupplier.java:86)
reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:139)
reactor.core.publisher.FluxHide$SuppressFuseableSubscriber.onNext(FluxHide.java:137)
org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:89)
reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onNext(FluxDoFinally.java:113)
org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:89)
reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1839)
com.couchbase.client.core.Reactor$SilentMonoCompletionStage.lambda$subscribe$0(Reactor.java:183)
java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859)
java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837)
java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2073)
com.couchbase.client.core.msg.BaseRequest.succeed(BaseRequest.java:161)
com.couchbase.client.core.io.netty.kv.KeyValueMessageHandler.decodeAndComplete(KeyValueMessageHandler.java:412)
com.couchbase.client.core.io.netty.kv.KeyValueMessageHandler.retryOrComplete(KeyValueMessageHandler.java:368)
com.couchbase.client.core.io.netty.kv.KeyValueMessageHandler.decode(KeyValueMessageHandler.java:351)
com.couchbase.client.core.io.netty.kv.KeyValueMessageHandler.channelRead(KeyValueMessageHandler.java:282)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
com.couchbase.client.core.io.netty.kv.MemcacheProtocolVerificationHandler.channelRead(MemcacheProtocolVerificationHandler.java:85)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
com.couchbase.client.core.deps.io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:336)
com.couchbase.client.core.deps.io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:308)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
com.couchbase.client.core.deps.io.netty.handler.flush.FlushConsolidationHandler.channelRead(FlushConsolidationHandler.java:152)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
com.couchbase.client.core.deps.io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1373)
com.couchbase.client.core.deps.io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1236)
com.couchbase.client.core.deps.io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1285)
com.couchbase.client.core.deps.io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:519)
com.couchbase.client.core.deps.io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:458)
com.couchbase.client.core.deps.io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:280)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
com.couchbase.client.core.deps.io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
com.couchbase.client.core.deps.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
com.couchbase.client.core.deps.io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
com.couchbase.client.core.deps.io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:800)
com.couchbase.client.core.deps.io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:499)
com.couchbase.client.core.deps.io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:397)
com.couchbase.client.core.deps.io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
com.couchbase.client.core.deps.io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
com.couchbase.client.core.deps.io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.lang.Thread.run(Thread.java:829)

We have a standard @document with a list of an Abstract object like so:

private List<AbstractPaymentMethod<?>> paymentMethods;

The definition of the abstract class is:

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public abstract class AbstractPaymentMethod<T>   {

	private String id;
	private T data;

}

An example of a class implementing the abstract class is:

public class CreditPaymentMethod extends AbstractPaymentMethod<Credit> {
}

@Getter
@Setter
public class Credit  {
	private String id;
	private String type;
	private String description;
	private BigDecimal amount;
}

We can't quite determine when exactly it happen but some of the versions that we're on are:

  • Spring Boot 2.7.6
  • spring data commons 2.7.6
  • spring data couchbase 4.4.6

When app is initially deployed it runs fine for a while and then suddenly it begins throwing this error complaining specifically about our abstract class. Previous to these versions, we have been running fine up to 5 years so we're trying to determine if there was any change of behavior in the spring data commons area where we have to configure abstract classes differently. Any help would be greatly appreciated.

We are not using converters (@ReadingConverter / @WritingConverter) for this but simply just have an overall @document spring data couchbase entity class and use the standard PagingAndSortingRepository.

I imagine more data will be needed to help here but wanted to get convo started.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 14, 2023
@mp911de mp911de transferred this issue from spring-projects/spring-data-commons Mar 14, 2023
@mp911de
Copy link
Member

mp911de commented Mar 14, 2023

I moved the ticket into Spring Data Couchbase. The converter should honor the concrete type for properties holding values that do not exactly match the property type.

I.e. for a List of AbstractPaymentMethod, the list in the Couchbase document should contain a type hint (discriminator) that describes which subclass of AbstractPaymentMethod was saved. Also, since data is an Object, a type hint there seems appropriate.

When you mention that the application only fails after a certain time, this might indicate that the functionality isn't used right from the start or that a cache is being evicted.

I'd ask you to verify whether the affected documents contain proper type hints. Since you're not using @TypeAlias, you don't need any specific configuration of the mapping context.

@cabbonizio
Copy link
Author

Hey there @mp911de .......thx for your comment! Here's what a sample of the document looks like when I pull a sample doc from Couchbase analytics. I am guessing the spring-data / Couchbase framework has been adding the "_class" attribute when saving to Couchbase all along. This must be how the reads work to understand what type of object to de-serialize. There were no special annotations done to make this work. We have done this in other API models outside of Couchbase using Jackson annotations such as @JsonTypeInfo and @JsonSubTypes. We never had to do this for the Couchbase entity model it just worked. Thoughts?

"paymentMethods": [ { "_class": "com.****.common.couchbase.model.cart.payment.CreditCardPaymentMethod", "data": { "cvvPromptFlag": false, "expirationDate": "1999-12", "id": "1141055087", "lastFourDigits": "****", "type": "XX", "typeDescription": "XXXXXX" } } ],

@mikereiche
Copy link
Collaborator

mikereiche commented Mar 14, 2023

@cabbonizio - I'll investigate your issue.
There was an issue around the _class not being leveraged that was causing an issue for abstract entities - #1315 (and its duplicate #1364). But these were fixed before 4.4.6. (Meanwhile, can you please use the latest 4.x which is 4.4.9 so we are both looking using the same codebase? Thanks. Edit: 4.4.8 from spring-boot 2.7.9 is fine).

@BillTurnbull
Copy link

@mikereiche just to be clear you want an update to use spring-data-couchbase 4.4.9 ?

@cabbonizio
Copy link
Author

cabbonizio commented Mar 14, 2023

@cabbonizio - I'll investigate your issue. There was an issue around the _class not being leveraged that was causing an issue for abstract entities - #1315 (and its duplicate #1364). But these were fixed before 4.4.6. (Meanwhile, can you please use the lates 4.x which is 4.4.9 so we are both looking using the same codebase? Thanks).

Thank you so much for jumping in. A few other co-workers may also help contribute and answer questions. It sounds like we can force bump Spring Data Couchbase to 4.4.9 and we'll be like for like.

I made a mistake we are actually on the following currently:

  • Spring Boot 2.7.9
  • spring data commons 2.7.8
  • spring data couchbase 4.4.8

We can force bump spring data couchbase to 4.4.9 and then we're on same versions.

@ammw
Copy link

ammw commented Mar 14, 2023

And please let us know if there's anything else to check or try out

@mikereiche
Copy link
Collaborator

mikereiche commented Mar 14, 2023

I created an application from Spring Initialzr 2.7.9 (It doesn't let you pick the spring data version, it uses 4.4.8).
No issue with either the template or the repository:

package com.example.demo;

import java.math.BigDecimal;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.stereotype.Component;

/**
 * Components of the type CommandLineRunner are called right after the application start up. So the method *run* is
 * called as soon as the application starts.
 * 
 * @author Michael Reiche
 */
@Component
public class CmdRunner implements CommandLineRunner {

  @Autowired CouchbaseTemplate template;
  @Autowired PaymentRepository paymentRepository;

  @Override
  public void run(String... strings) {

    AbstractPaymentMethod paymentMethod = new CreditPaymentMethod();
    paymentMethod.setId("123");
    Credit credit = new Credit();
    credit.setType("visa_type");
    credit.setId("visa_id");
    credit.setDescription("Walmart_Purchase");
    credit.setAmount(BigDecimal.TEN);
    paymentMethod.setData(credit);

    template.upsertById(AbstractPaymentMethod.class).one(paymentMethod);

    AbstractPaymentMethod pFound = template.findById(AbstractPaymentMethod.class).one(paymentMethod.getId());
    System.err.println("payment method: " + pFound);

    paymentMethod.setId("abc");
    paymentRepository.save(paymentMethod);
    Optional<AbstractPaymentMethod> foundRepoPaymentMethod = paymentRepository.findById(paymentMethod.getId());
    System.err.println("repository payment method: " + foundRepoPaymentMethod);

  }

}
payment method: { id=123, data={ id=visa_id, type=visa_type, description='Walmart_Purchase', amount=10}}
repository payment method: Optional[{ id=abc, data={ id=visa_id, type=visa_type, description='Walmart_Purchase', amount=10}}]

id="123"
{
  "_class": "com.example.demo.CreditPaymentMethod",
  "data": {
    "amount": "10",
    "description": "Walmart_Purchase",
    "id": "visa_id",
    "type": "visa_type"
  }
}
id="abc"
{
  "_class": "com.example.demo.CreditPaymentMethod",
  "data": {
    "amount": "10",
    "description": "Walmart_Purchase",
    "id": "visa_id",
    "type": "visa_type"
  }
}

@mikereiche
Copy link
Collaborator

mikereiche commented Mar 14, 2023

package com.example.demo;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public abstract class AbstractPaymentMethod<T> {

  private String id;
  private T data;

  public String toString(){
    StringBuffer sb=new StringBuffer();
    sb.append("{ id="+id+", data="+data+"}");
    return sb.toString();
  }

}
package com.example.demo;

import com.couchbase.client.core.msg.kv.DurabilityLevel;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.transactions.config.TransactionsConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableCouchbaseRepositories({"com.example.demo"})
@EnableTransactionManagement
public class Config extends AbstractCouchbaseConfiguration {
  @Override
  public String getConnectionString() {
    return "127.0.0.1";
  }

  @Override
  public String getUserName() {
    return "Administrator";
  }

  @Override
  public String getPassword() {
    return "password";
  }

  @Override
  public String getBucketName() {
    return "my_bucket";
  }

}

package com.example.demo;

import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;

@Getter
@Setter
public class Credit  {
  private String id;
  private String type;
  private String description;
  private BigDecimal amount;

  public String toString(){
    StringBuffer sb=new StringBuffer();
    sb.append("{ id="+id+", type="+type+", description='"+description+"', amount="+amount+"}");
    return sb.toString();
  }
}
package com.example.demo;

public class CreditPaymentMethod extends AbstractPaymentMethod<Credit> {
}
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
 		SpringApplication.run( DemoApplication.class, args );
	}

}
package com.example.demo;

import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PaymentRepository
  extends CouchbaseRepository<AbstractPaymentMethod, String>{
}

@mikereiche
Copy link
Collaborator

4.4.8 is fine.

Can you show me the call that is attempting to retrieve the payment method? The definition (in the repository interface if that's how it's being called) and the actual call?

@cabbonizio
Copy link
Author

4.4.8 is fine.

Can you show me the call that is attempting to retrieve the payment method? The definition (in the repository interface if that's how it's being called) and the actual call?

There's an overall @document entity it's part of so it's retrieving the parent doc.....the payment methods are simply a list of embedded object....here's what the doc looks like:

@Getter
@Setter
@Document
@NoArgsConstructor
public class Cart {
    
    @IdPrefix
    private String keyPrefix = "carts::";
    @IdAttribute
    private String id;
    @Id @GeneratedValue(delimiter = "")
    private String key;
    @CreatedDate
    private LocalDateTime createdTimestamp;
    @LastModifiedDate
    private LocalDateTime lastUpdatedTimestamp;
    @Version
    private long versionID;
    private List<AbstractPaymentMethod<?>> paymentMethods;

I took out a lot of the attributes to keep it simple since there are a lot more.

Repo looks like this:

public interface CartRepository extends PagingAndSortingRepository<Cart, String> {
}

In code we're using the CartRepository.findById() method......

@mikereiche mikereiche added status: feedback-provided Feedback has been provided and removed status: waiting-for-triage An issue we've not yet triaged labels Mar 15, 2023
@mikereiche
Copy link
Collaborator

mikereiche commented Mar 15, 2023

I can't reproduce the problem.

I add this Cart class :

package com.example.demo;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.core.mapping.id.GeneratedValue;
import org.springframework.data.couchbase.core.mapping.id.IdAttribute;
import org.springframework.data.couchbase.core.mapping.id.IdPrefix;

import java.time.LocalDateTime;
import java.util.List;

@Getter
@Setter
@Document
@NoArgsConstructor
public class Cart {

  @IdPrefix
  private String keyPrefix = "carts::";
  @IdAttribute
  private String id;
  @Id
  @GeneratedValue(delimiter = "")
  private String key;
  @CreatedDate
  private LocalDateTime createdTimestamp;
  @LastModifiedDate
  private LocalDateTime lastUpdatedTimestamp;
  @Version
  private long versionID;
  private List<AbstractPaymentMethod<?>> paymentMethods;

  public String toString(){
    StringBuffer sb=new StringBuffer();
    sb.append("Cart{id="+id+", paymentMethods="+paymentMethods+"}");
    return sb.toString();
  }
}

And this CartRepository :

package com.example.demo;

import org.springframework.data.repository.PagingAndSortingRepository;

public interface CartRepository extends PagingAndSortingRepository<Cart, String> {
}

And use this driver class:

package com.example.demo;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.stereotype.Component;

/**
 * Components of the type CommandLineRunner are called right after the application start up. So the method *run* is
 * called as soon as the application starts.
 * 
 * @author Michael Reiche
 */
@Component
public class CmdRunner implements CommandLineRunner {

  @Autowired CouchbaseTemplate template;
  @Autowired PaymentRepository paymentRepository;
  @Autowired CartRepository cartRepository;

  @Override
  public void run(String... strings) {

    AbstractPaymentMethod paymentMethod = new CreditPaymentMethod();
    paymentMethod.setId("123");
    Credit credit = new Credit();
    credit.setType("visa_type");
    credit.setId("visa_id");
    credit.setDescription("Walmart_Purchase");
    credit.setAmount(BigDecimal.TEN);
    paymentMethod.setData(credit);

    Cart cart = new Cart();
    cart.setId("111");
    List pmtMethods = new ArrayList();
    pmtMethods.add(paymentMethod);
    cart.setPaymentMethods( pmtMethods);
    //template.upsertById(Cart.class).one(cart);  // same saved by template or repository
    cartRepository.save(cart);

    Cart pFound = template.findById(Cart.class).one("carts::"+cart.getId());
    System.err.println("template cart: " + pFound);

    Optional<Cart> repoFound = cartRepository.findById("carts::"+cart.getId());
    System.err.println("repo cart: " + repoFound);

  }

}

I get:

template cart: Cart{id=111, paymentMethods=[{ id=123, data={ id=visa_id, type=visa_type, description='Walmart_Purchase', amount=10}}]}
repo cart: Optional[Cart{id=111, paymentMethods=[{ id=123, data={ id=visa_id, type=visa_type, description='Walmart_Purchase', amount=10}}]}]

And the document looks like:

{
  "_class": "com.example.demo.Cart",
  "id": "111",
  "paymentMethods": [
    {
      "_class": "com.example.demo.CreditPaymentMethod",
      "data": {
        "amount": "10",
        "description": "Walmart_Purchase",
        "id": "visa_id",
        "type": "visa_type"
      },
      "id": "123"
    }
  ]
}

@mikereiche
Copy link
Collaborator

@ammw @BillTurnbull @cabbonizio
If you still have this issue can you attach a reproducer to the issue? You may have to rename the file after zipping it to allow it to be attached.

@mikereiche
Copy link
Collaborator

mikereiche commented Mar 15, 2023

If you set a break-point at line 231 in MappingCouchbaseConverter - you should see that type=com.example.demo.AbstractPaymentMethod<Object>
and the content of source contains _class
CouchbaseDocument{id=null, exp=0, content={_class=com.example.demo.CreditPaymentMethod, data=CouchbaseDocument{id=null, exp=0, content={amount=10, description=Walmart_Purchase, id=visa_id, type=visa_type}}, id=123}}

resulting in typeToUse=com.example.demo.CreditPaymentMethod

protected <R> R read(final TypeInformation<R> type, final CouchbaseDocument source, final Object parent) {
		if (source == null) {
			return null;
		}

231:		TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
		Class<? extends R> rawType = typeToUse.getType();

		if (conversions.hasCustomReadTarget(source.getClass(), rawType)) {
			return conversionService.convert(source, rawType);
		}

		if (typeToUse.isMap()) {
			return (R) readMap(typeToUse, source, parent);
		}

		CouchbasePersistentEntity<R> entity = (CouchbasePersistentEntity<R>) mappingContext
				.getRequiredPersistentEntity(typeToUse);
		return read(entity, source, parent);
	}

@cabbonizio
Copy link
Author

Thank you @mikereiche for all of your feedback so far. For us we can't reproduce local also only when it's deployed after some time which makes us wonder if this is self-inflicted based on a specific use-case where we maybe fail to convert the abstract to a concrete object of sorts. We're going to do some more testing and report back to this issue. Part of this is ruling out if there's any issue with the Spring components between data and Couchbase modules no evidence of that yet

@mikereiche
Copy link
Collaborator

If the concrete class (the _class=class) doesn't extend the abstract class (maybe the class definition has changed), this can happen. If the _class=class is "not more concrete" than the abstract class, then it will use the abstract class.

Another possibility is that a custom type mapper is configured, or that there is an @typealias configured.

@cabbonizio
Copy link
Author

If the concrete class (the _class=class) doesn't extend the abstract class (maybe the class definition has changed), this can happen. If the _class=class is "not more concrete" than the abstract class, then it will use the abstract class.

Another possibility is that a custom type mapper is configured, or that there is an @typealias configured.

We are looking to add some Lombok equals and hash code annotations to all of the concrete classes since something seems off about the mapping instantiators. Our Developer found they are stored as follows..........

{class-name}+"instantiator"+{hashCode}

Will report back with latest tests

@mikereiche
Copy link
Collaborator

mikereiche commented Mar 17, 2023

The error message "Failed to instantiate com.qvc.common.couchbase.model.cart.payment.AbstractPaymentMethod" indicates that DefaultTypeMapper.readType() could not determine the concrete type (documentsTargetType) based on the 'source' and was therefore trying to use the abstract type (basicType) which was used to define the repository. The issue looks more like an issue determining what the documentsTargetType is (or determining that it isMoreConcreteCustomType than the abstract type). Which points to the (sub)document not having a _class property, or the code not finding it because it is using a different @typealias than _class, or a custom TypeMapper (instead of the DefaultTypeMapper). (Or the aforementioned bug of not projecting _class, but that should be fixed in 4.4.8). The error message indicates it is trying to instantiate the wrong class - not any issue with instantiation of the correct class.

  public <T> TypeInformation<? extends T> readType(S source, TypeInformation<T> basicType) {
    Assert.notNull(source, "Source must not be null");
    Assert.notNull(basicType, "Basic type must not be null");
    Class<?> documentsTargetType = this.getDefaultedTypeToBeUsed(source);
    if (documentsTargetType == null) {
      return basicType;
    } else {
      Class<T> rawType = basicType.getType();
      boolean isMoreConcreteCustomType = rawType == null || rawType.isAssignableFrom(documentsTargetType) && !rawType.equals(documentsTargetType);
      if (!isMoreConcreteCustomType) {
        return basicType;
      } else {
        TypeInformation<?> targetType = TypeInformation.of(documentsTargetType);
        return basicType.specialize(targetType);
      }
    }
  }

@cabbonizio
Copy link
Author

@mikereiche I think we have a breakthru and I'm going to let the team member who made the discovery comment on this thread soon. Many thanks for your patience. We kind of want to comment the finding on here to save others some future pain. If all remains good as it has no issues found with Spring Data Couchbase. We'll report back soon

@mikereiche
Copy link
Collaborator

I would like to hear what you found. Closing show it doesn't show up in my open-issues list.

@BillTurnbull
Copy link

BillTurnbull commented Mar 29, 2023

@mikereiche What we found is that the particular service having the issue creates a CompletableFuture.runAsync(..) task to be run at the end of processing. That task retrieves all other cart documents belong to a user and scrubs them of data. The retrieval also uses spring-data-couchbase but does use a n1ql query rather than a key lookup to get the other carts.

When testing local I found that some of the errors were originating from that async tasks reads from couchbase rather than the read of the cart document the was part of the service request. Suspecting a threading issue I changed that async task to be just a sync method call and we could no longer reproduce the issue. It turns out that the completable future was not specifying an Executor so was using the default forkjoinpool. I then changed to use the spring created Executor bean as an executor and could also no longer reproduce the issue.

The root cause is not yet clear to me but it is definitely related to volume and perhaps the limited number of threads with forkjoinpool or the way forkjoinpool distributes work.

So, changed:

    public CompletableFuture<Void> findAndScrub(final String globalUserId, final String currentId) {
        return CompletableFuture.runAsync(() -> cartDao.findCartsByGlobalUserId(globalUserId).stream()
                .filter(someFiltering(currentId))
                .forEach(this::scrub));
    }

to:

   public CompletableFuture<Void> findAndScrub(final String globalUserId, final String currentId) {
        return CompletableFuture.runAsync(() -> cartDao.findCartsByGlobalUserId(globalUserId).stream()
                .filter(someFiltering(currentId))
                .forEach(this::scrub),applicationTaskExecutor);
    }

Any thoughts you have would be appreciated as I would prefer to have an understanding of the root cause. :-).

@mikereiche
Copy link
Collaborator

mikereiche commented Mar 29, 2023

Are you using a repository that extends DynamicProxyable? It doesn't look like it.
public interface CartRepository extends PagingAndSortingRepository<Cart, String> {

Does the error occur in findCartsByGlobalUserId(), in someFiltering() or in scrub()?

Can you show the method where the error occurs?

@mp911de
Copy link
Member

mp911de commented Mar 30, 2023

Thanks for mentioning CompletableFuture.runAsync(…). This method runs typically on the ForkJoin pool that isn't associated with a contextual class loader but rather uses the initial application class loader. Depending on your setup, the app class loader sees only the outer Spring Boot JAR and not the packaged inner jars.

The issue can be fixed by providing the bean class loader to CouchbaseTypeMapper. DefaultTypeMapper that serves as base class for Couchbase's TypeMapper is BeanClassLoaderAware accepting ClassLoader. The easiest fix would be setting the class loader via MappingCouchbaseConverter.setApplicationContext(…), see MongoDB's usage for reference.

@mp911de mp911de reopened this Mar 30, 2023
@mp911de mp911de added type: bug A general bug and removed status: feedback-provided Feedback has been provided labels Mar 30, 2023
@mp911de mp911de changed the title MappingInstantiationException with Spring Data Couchbase for Abstract Classes MappingInstantiationException for Abstract Classes using tasks ran on ForkJoinPool Mar 30, 2023
@mikereiche
Copy link
Collaborator

The fix is going to be 4.4.11 and 5.0.5

@cabbonizio
Copy link
Author

Thank you @mp911de and @mikereiche for all your help

@BillTurnbull
Copy link

The fix is going to be 4.4.11 and 5.0.5
@mikereiche I'm guessing you no longer need the stack trace you asked for? Let me know if needed.

And thanks for helping and getting a fix in!

@mikereiche
Copy link
Collaborator

Release date for those versions is April 14

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug A general bug
Projects
None yet
6 participants