ebean icon indicating copy to clipboard operation
ebean copied to clipboard

@NotNull + @Enumerated causes instrumentation failure

Open rvowles opened this issue 2 years ago • 13 comments

Expected behavior

I have a bean with this, it happened as I upgraded from 13.10 -> 13.17

@Enumerated(value=EnumType.STRING) @NotNull private SubscriptionStatus subscriptionStatus = SubscriptionStatus.FREE_TRIAL;

If I annotate this way, it crashes with

java.lang.RuntimeException: Error reading annotations for dev.featurehub.app.cs.db.models.CsOrganisation at io.ebeaninternal.server.deploy.parse.ReadAnnotations.readInitial(ReadAnnotations.java:31) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebeaninternal.server.deploy.BeanDescriptorManager.createDeployBeanInfo(BeanDescriptorManager.java:1168) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebeaninternal.server.deploy.BeanDescriptorManager.readEntityDeploymentInitial(BeanDescriptorManager.java:641) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebeaninternal.server.deploy.BeanDescriptorManager.deploy(BeanDescriptorManager.java:290) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebeaninternal.server.core.InternalConfiguration.(InternalConfiguration.java:129) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebeaninternal.server.core.DefaultContainer.createServer(DefaultContainer.java:104) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebeaninternal.server.core.DefaultContainer.createServer(DefaultContainer.java:57) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebeaninternal.server.core.DefaultContainer.createServer(DefaultContainer.java:29) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebean.DatabaseFactory.create(DatabaseFactory.java:60) ~[ebean-api-13.17.0-jakarta.jar:13.17.0-jakarta] at io.ebean.DbContext.getWithCreate(DbContext.java:98) ~[ebean-api-13.17.0-jakarta.jar:13.17.0-jakarta] a Caused by: jakarta.persistence.PersistenceException: Error mapping property dev.featurehub.app.cs.db.models.CsOrganisation.subscriptionStatus - Error mapping Enum type:class dev.featurehub.app.common.model.SubscriptionStatus It is mapped using 2 different modes when only one is supported (ORDINAL, STRING or an Ebean mapping)

It seems to try and process the annotation twice.

If I drop the @NotNull and replace it with a @Column(nullable = false) then its ok.

rvowles avatar Apr 18 '23 08:04 rvowles

Error mapping Enum type:class dev.featurehub.app.common.model.SubscriptionStatus It is mapped using 2 different modes when only one is supported (ORDINAL, STRING or an Ebean mapping)

What does SubscriptionStatus look like? Does it have any mappings?

rbygrave avatar Apr 18 '23 09:04 rbygrave

it did look like this:

/*

  • FeatureHub SaaS Customer Service API
  • The API for accessing the customer service API
  • OpenAPI spec version: 1.1.1
  • NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  • https://openapi-generator.tech
  • Do not edit the class manually. */

package dev.featurehub.app.common.model;

import com.fasterxml.jackson.annotation.; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import jakarta.validation.constraints.; import java.util.HashMap; import java.util.Map;

/** Gets or Sets SubscriptionStatus */ public enum SubscriptionStatus { ACTIVE("ACTIVE"),

CANCELLED("CANCELLED"),

UNPAID("UNPAID"),

PAST_DUE("PAST_DUE"),

FREE_TRIAL("FREE_TRIAL"),

TRIAL_EXPIRED("TRIAL_EXPIRED");

private String value;

SubscriptionStatus(String value) { this.value = value; }

@JsonValue public String getValue() { return value; }

@Override public String toString() { return String.valueOf(value); }

public SubscriptionStatus copy() { return this; }

// can't use map.of private static Map<String, SubscriptionStatus> fromValues = new HashMap<>();

static { fromValues.put("ACTIVE", ACTIVE); fromValues.put("ACTIVE", ACTIVE); fromValues.put("CANCELLED", CANCELLED); fromValues.put("CANCELLED", CANCELLED); fromValues.put("UNPAID", UNPAID); fromValues.put("UNPAID", UNPAID); fromValues.put("PAST_DUE", PAST_DUE); fromValues.put("PAST_DUE", PAST_DUE); fromValues.put("FREE_TRIAL", FREE_TRIAL); fromValues.put("FREE_TRIAL", FREE_TRIAL); fromValues.put("TRIAL_EXPIRED", TRIAL_EXPIRED); fromValues.put("TRIAL_EXPIRED", TRIAL_EXPIRED); }

@JsonCreator public static SubscriptionStatus fromValue(String text) { return fromValues.get(text); } }

but then i found that the @Enumerated was being ignored and it was an INT (full rebuild, depoyed in docker container) - so had to put the @io.ebean.annotation.DbEnumValue back in on the class (the API generator will add ebean annotations on command!) But the `@Column while working for tests it appears, didn't work when running it up, so i have had to remove the NotNull and Column annotations.

rvowles avatar Apr 18 '23 09:04 rvowles

When i debugged into it, it hit that method in DeployUtil twice for the same field - setEnumScalarType - it seemed like it was unhappy about doing that. If I can debug any further to illuminate this I'm happy to!

rvowles avatar Apr 18 '23 09:04 rvowles

This is so weird. When the container starts in k8s, it hits this error when running and crashes. But k8s restarts the container, and it runs ok.

{"@timestamp":"2023-04-19T07:30:12.380+0000","message":"Error in deployment","priority":"ERROR","path":"io.ebean.internal","thread":"main","stack_trace":"java.lang.RuntimeException: Error reading annotations for dev.featurehub.app.cs.db.models.CsOrganisation\n\tio.ebeaninternal.server.deploy.parse.ReadAnnotations.readInitial(ReadAnnotations.java:31) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.ebeaninternal.server.deploy.BeanDescriptorManager.createDeployBeanInfo(BeanDescriptorManager.java:1168) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.ebeaninternal.server.deploy.BeanDescriptorManager.readEntityDeploymentInitial(BeanDescriptorManager.java:641) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.ebeaninternal.server.deploy.BeanDescriptorManager.deploy(BeanDescriptorManager.java:290) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.ebeaninternal.server.core.InternalConfiguration.(InternalConfiguration.java:129) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.ebeaninternal.server.core.DefaultContainer.createServer(DefaultContainer.java:104) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.ebeaninternal.server.core.DefaultContainer.createServer(DefaultContainer.java:29) ~[ebean-core-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.ebean.DatabaseFactory.createInternal(DatabaseFactory.java:136) ~[ebean-api-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.ebean.DatabaseFactory.create(DatabaseFactory.java:85) ~[ebean-api-13.17.0-jakarta.jar:13.17.0-jakarta]\n\tio.featurehub.app.db.utils.CommonDbFeature.configure(CommonDbFeature.kt:177) ~[common-db-1.1-SNAPSHOT.jar:?]\n\torg.glassfish.jersey.model.internal.CommonConfig.configureFeatures(CommonConfig.java:728) ~[jersey-common-3.1.1.jar:?]\n\t

rvowles avatar Apr 19 '23 07:04 rvowles

Ok - if i swap from JDK 11.11 -> 11.18 it happens, if i swap back it doesn't happen (tumerin).

rvowles avatar Apr 19 '23 07:04 rvowles

I just tried with Amazon's JDK as well and have the same issue.

rvowles avatar Apr 19 '23 08:04 rvowles

and now i try and debug it and i can't trigger it 🤕 fails when running, passes when debugging.

rvowles avatar Apr 19 '23 08:04 rvowles

The stack trace at the top is truncated and does not include the caused by: ... which would tell us the underlying cause. Are we able to get that?

rbygrave avatar Apr 19 '23 09:04 rbygrave

I'm still trying to figure out how to replicate it, it's changing all the time.

rvowles avatar Apr 19 '23 09:04 rvowles

The original error is suggesting that SubscriptionStatus is mapped in 2 or more places, where in one place it is a @Enumerated(value=EnumType.STRING) and in the other its a @Enumerated(value=EnumType.ORDINAL) or @Enumerated (or has no @Enumerated or has @DbEnumValue) ?

Is the enum type SubscriptionStatus used in multiple places / mapped in multiple entity beans? Is it mapped consistently in all the places it is used?

rbygrave avatar Apr 19 '23 09:04 rbygrave

Now that was the trigger I needed to find it thanks @rob-bygrave ! It was an Entity/View that just had it used. It was fine with 13.10/Java 11.11 but with 13.7/11.18 it wasn't. I still cannot reliably reproduce it either, sometimes it happens, sometimes it doesn't. Once it has gone, it has gone for a few hours and then pops up again. I will leave it correctly annotated for now, but I'm wondering if it wouldn't be worthwhile a PR rewording that error message so its more clear what to look for?

rvowles avatar Apr 20 '23 20:04 rvowles

dev.featurehub.app.common.model.SubscriptionStatus It is mapped using 2 different modes when only one is supported (ORDINAL, STRING or an Ebean mapping)

Yeah absolutely. I'll create a PR with some better wording and get you to review the new wording.

cannot reliably reproduce

It's very strange. I can imagine that different JVMs could result in different ordering when it reads the entity classes annotations etc.

I wonder if there is something subtle where it actually does not fail when it really/ideally should fail all the time regardless of the order in which things are read. Hmmm, maybe I should try and reproduce on a minimal model. It sounds like there is a bug where it really should fail with this error and somehow does not.

rob-bygrave avatar Apr 20 '23 22:04 rob-bygrave

Where it was being used was here (like this):

@Entity
@View(name = "cs_organisation", dependentTables = "cs_organisation")
public class CsOrganisationCounters {
  @Aggregation("count(subscription_status)")
  public Long count;

  @NotNull
  public SubscriptionStatus subscriptionStatus;
}

such a heisenbug!

rvowles avatar Apr 21 '23 00:04 rvowles