Skip to content

How to test deserialization #187

@jorgheymans

Description

@jorgheymans

We recently ran into an issue with this class:

/** Tracks the number of times a page is routed to in a counter. */
public class PageViewMeter implements VaadinAfterNavigationListener, MeterBinder {

  // 2 or more consecutive numbers
  private static final Pattern NUMERIC_ID_PATTERN = Pattern.compile("\\d{2,}");

  // kubernetes-kit will reinject this field after deserialization
  private transient MeterRegistry meterRegistry;

  @Override
  public void bindTo(@NonNull MeterRegistry meterRegistry) {
    this.meterRegistry = meterRegistry;
  }

  @Override
  public void afterNavigation(AfterNavigationEvent event) {
    Counter.builder(PAGE_VIEWS.getName())
        .description(PAGE_VIEWS.getDescription())
        .withRegistry(meterRegistry)
        .withTag("page", sanitize(event.getLocation().getPath()))
        .increment();
  }

  protected String sanitize(String url) {
    if (url == null || url.isEmpty()) {
      return "/";
    }
    return NUMERIC_ID_PATTERN.matcher(url).replaceAll("id");
  }
}

This meter will work fine, until the moment where the session is deserialized from the session store (redis):

java.lang.NullPointerException: Cannot invoke "io.micrometer.core.instrument.MeterRegistry.counter(io.micrometer.core.instrument.Meter$Id)" because "registry" is null
	at io.micrometer.core.instrument.Counter$Builder.register(Counter.java:150)
	at io.micrometer.core.instrument.Counter$Builder.lambda$withRegistry$0(Counter.java:134)
	at io.micrometer.core.instrument.Meter$MeterProvider.withTag(Meter.java:518)
	at ....internal.metrics.PageViewMeter.afterNavigation(PageViewMeter.java:32)

Refactoring the class to use constructor injection fixed the issue:

@Slf4j
public class PageViewMeter implements VaadinAfterNavigationListener {

  // vaadin kubernetes-kit will reinject this field after deserialization
  private final transient MeterRegistry meterRegistry;

  /**
   * Constructor injection purposely. If we implement MeterBinder instead, reinjection after
   * deserialization does not work
   */
  public PageViewMeter(@NonNull MeterRegistry meterRegistry) {
    this.meterRegistry = meterRegistry;
  }

  @Override
  public void afterNavigation(AfterNavigationEvent event) {
    if (meterRegistry == null) {
      log.warn("MeterRegistry is not set, unable to meter PageViews");
      return;
    }

    final Meter.MeterProvider<Counter> page =
        Counter.builder(PAGE_VIEWS.getName())
            .description(PAGE_VIEWS.getDescription())
            .withRegistry(meterRegistry);
.....
.....

This was not obvious to spot. So my question is how can we write a test for this? So that we know that the mechanism of kubernetes-kit reinjecting spring dependencies after deserialization works.

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions