Skip to content

Commit

Permalink
Merge pull request #1359 from folio-org/OKAPI-1191-timer-NumberFormat…
Browse files Browse the repository at this point in the history
…Exception

OKAPI-1191: NumberFormatException in timerId halts Okapi
  • Loading branch information
julianladisch authored Sep 14, 2024
2 parents 752cab2 + 2755bd6 commit 5d3dedc
Show file tree
Hide file tree
Showing 8 changed files with 539 additions and 105 deletions.
2 changes: 1 addition & 1 deletion okapi-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<artifactId>log4j-slf4j2-impl</artifactId>
</dependency>
<dependency> <!-- for log4j2 asynchronous loggers -->
<groupId>com.lmax</groupId>
Expand Down
11 changes: 11 additions & 0 deletions okapi-core/src/main/java/org/folio/okapi/bean/TimerDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,15 @@ public boolean isModified() {
public void setModified(boolean modified) {
this.modified = modified;
}

/**
* Shallow copy.
*/
public TimerDescriptor copy() {
var timerDescriptor = new TimerDescriptor();
timerDescriptor.setId(id);
timerDescriptor.setRoutingEntry(routingEntry);
timerDescriptor.setModified(modified);
return timerDescriptor;
}
}
254 changes: 150 additions & 104 deletions okapi-core/src/main/java/org/folio/okapi/managers/TimerManager.java

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions okapi-core/src/main/java/org/folio/okapi/util/TenantProductSeq.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.folio.okapi.util;

import java.util.Collection;
import org.folio.okapi.bean.TimerDescriptor;

/**
* [tenant]_[product]_[seq] (like test_tenant_mod-foo_2) or [product]_[seq] (like mod-foo_2).
*
* <p>Used as {@link TimerDescriptor} id.
*/
public class TenantProductSeq {
private static final String TIMER_ENTRY_SEP = "_";
private final String tenantId;
private final String product;
private final int seq;

/**
* Constructor using the three components.
*/
public TenantProductSeq(String tenantId, String product, int seq) {
this.tenantId = tenantId;
this.product = product;
this.seq = seq;
}

/**
* Like {@link #TenantProductSeq(String tenantProductSeq)} but the parameter
* tenantId replaces the value from tenantProductSeq String.
*
* @param tenantId the replacement value; if null the value from
* tenantProductSeq String is taken
*/
public TenantProductSeq(String tenantId, String tenantProductSeq) {
int pos2 = tenantProductSeq.lastIndexOf(TIMER_ENTRY_SEP);
seq = Integer.parseInt(tenantProductSeq.substring(pos2 + 1));
int pos1 = tenantProductSeq.lastIndexOf(TIMER_ENTRY_SEP, pos2 - 1);
product = tenantProductSeq.substring(pos1 + 1, pos2);
if (tenantId != null) {
this.tenantId = tenantId;
return;
}
if (pos1 == -1) {
this.tenantId = null;
} else {
this.tenantId = tenantProductSeq.substring(0, pos1);
}
}

/**
* Parse a String [tenant]_[product]_[seq] like test_tenant_mod-foo_2
* or [product]_[seq] like mod-foo_2.
*/
public TenantProductSeq(String tenantProductSeq) {
this(null, tenantProductSeq);
}

/**
* The tenant id.
*/
public String getTenantId() {
return tenantId;
}

/**
* The product, like mod-foo.
*/
public String getProduct() {
return product;
}

/**
* The timer number in the timer array in the module descriptor, starting with 0.
*/
public int getSeq() {
return seq;
}

/**
* Concatenation of the components, like mod-foo_2 or test_tenant_mod-foo_2.
*/
public String toString() {
if (tenantId == null) {
return product + TIMER_ENTRY_SEP + seq;
} else {
return tenantId + TIMER_ENTRY_SEP + product + TIMER_ENTRY_SEP + seq;
}
}

/**
* For each TimerDescriptor alter the id by removing the tenant id from the String.
*
* <p>For example test_tenant_mod-foo_2 becomes mod-foo_2.
*/
public static Collection<TimerDescriptor> stripTenantIdFromTimerId(
Collection<TimerDescriptor> collection) {

collection.forEach(timerDescriptor -> {
var old = new TenantProductSeq(timerDescriptor.getId());
timerDescriptor.setId(old.getProduct() + TIMER_ENTRY_SEP + old.getSeq());
});
return collection;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
package org.folio.okapi.managers;

import static io.vertx.core.Future.succeededFuture;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.junit5.Timeout;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.folio.okapi.bean.InterfaceDescriptor;
import org.folio.okapi.bean.ModuleDescriptor;
import org.folio.okapi.bean.RoutingEntry;
import org.folio.okapi.bean.Schedule;
import org.folio.okapi.bean.TimerDescriptor;
import org.folio.okapi.service.impl.TimerStoreMemory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

@Timeout(5000)
@ExtendWith(VertxExtension.class)
class TimerManagerTest {

@Test
Expand Down Expand Up @@ -37,4 +55,94 @@ void isPatchReset() {
Assertions.assertThat(TimerManager.isPatchReset(routingEntry)).isFalse();
}

@Test
void getModuleForTimer() {
var timerManager = new TimerManager(null, true);
var x = moduleDescriptor("mod-x-1.0.0", 11);
var y = moduleDescriptor("mod-y-2.3.4", 11);
var mds = List.of(x, y);
Assertions.assertThat(timerManager.getModuleForTimer(mds, "tenant_mod-x_0")).isEqualTo(x);
Assertions.assertThat(timerManager.getModuleForTimer(mds, "tenant_mod-x_11")).isNull();
Assertions.assertThat(timerManager.getModuleForTimer(mds, "tenant_mod-y_3")).isEqualTo(y);
Assertions.assertThat(timerManager.getModuleForTimer(mds, "the_test_tenant_mod-y_3")).isEqualTo(y);
Assertions.assertThat(timerManager.getModuleForTimer(mds, "invalid")).isNull();
}

ModuleDescriptor moduleDescriptor(String moduleId, int timerEntryCount) {
var routingEntries = new RoutingEntry[timerEntryCount];
for (int i = 0; i < routingEntries.length; i++) {
var routingEntry = new RoutingEntry();
routingEntry.setDelay("1");
routingEntry.setUnit("second");
routingEntries[i] = routingEntry;
}
var timers = new InterfaceDescriptor("_timer", "1.0");
timers.setHandlers(routingEntries);
timers.setInterfaceType("system");
InterfaceDescriptor [] provides = { timers };
var moduleDescriptor = new ModuleDescriptor(moduleId);
moduleDescriptor.setProvides(provides);
return moduleDescriptor;
}

TimerDescriptor timerDescriptor(String id, Integer seconds) {
var routingEntry = new RoutingEntry();
if (seconds != null) {
routingEntry.setDelay("" + seconds);
routingEntry.setUnit("second");
}
var timerDescriptor = new TimerDescriptor();
timerDescriptor.setId(id);
timerDescriptor.setRoutingEntry(routingEntry);
timerDescriptor.setModified(true);
return timerDescriptor;
}

Future<Void> testPatchTimer(TimerManager timerManager, String productSeq, Integer seconds) {
return timerManager.patchTimer("test_tenant", timerDescriptor(productSeq, seconds))
.compose(x -> timerManager.getTimer("test_tenant", productSeq))
.map(timerDescriptor -> {
var delay = timerDescriptor.getRoutingEntry().getDelay();
if (seconds == null) {
Assertions.assertThat(delay).isEqualTo("" + 1);
} else {
Assertions.assertThat(delay).isEqualTo("" + seconds);
}
return null;
});
}

@Test
void init(Vertx vertx, VertxTestContext vtc) {
var timerStore = new TimerStoreMemory(timerDescriptor("mod-y_0", 2));
var mds = List.of(moduleDescriptor("mod-x-1.0.0", 1), moduleDescriptor("mod-y-2.3.4", 2));
var tenantManager = mock(TenantManager.class);
when(tenantManager.allTenants()).thenReturn(succeededFuture(List.of("test_tenant")));
when(tenantManager.getEnabledModules("test_tenant")).thenReturn(succeededFuture(mds));
var discoveryManager = mock(DiscoveryManager.class);
var proxyService = mock(ProxyService.class);
var timerManager = new TimerManager(timerStore, true);
timerManager.init(vertx, tenantManager, discoveryManager, proxyService)
.compose(x -> testPatchTimer(timerManager, "mod-y_0", 0))
.compose(x -> testPatchTimer(timerManager, "mod-y_0", null))
.compose(x -> testPatchTimer(timerManager, "mod-y_0", 1))
.compose(x -> testPatchTimer(timerManager, "mod-y_0", 2))
.compose(x -> testPatchTimer(timerManager, "mod-y_0", 2))
.compose(x -> testPatchTimer(timerManager, "mod-x_0", 2))
.andThen(vtc.succeedingThenComplete());
}

@ParameterizedTest
@CsvSource(textBlock = """
, , false
foo, foo, false
foo_0, foo, false
t_foo_0, t, true
t_foo, t, false
""")
void belongs(String timerId, String tenantId, boolean expected) {
var timerDescriptor = new TimerDescriptor();
timerDescriptor.setId(timerId);
Assertions.assertThat(TimerManager.belongs(timerDescriptor, tenantId)).isEqualTo(expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.folio.okapi.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.folio.okapi.bean.TimerDescriptor;
import org.folio.okapi.service.TimerStore;

import io.vertx.core.Future;

public class TimerStoreMemory implements TimerStore {

private Map<String, TimerDescriptor> map = new HashMap<>();

public TimerStoreMemory() {
}

public TimerStoreMemory(TimerDescriptor timerDescriptor) {
put(timerDescriptor);
}

@Override
public Future<Void> init(boolean reset) {
if (reset) {
map.clear();
}
return Future.succeededFuture();
}

@Override
public Future<List<TimerDescriptor>> getAll() {
var list = new ArrayList<>(map.values());
return Future.succeededFuture(list);
}

@Override
public Future<Void> put(TimerDescriptor timerDescriptor) {
map.put(timerDescriptor.getId(), timerDescriptor);
return Future.succeededFuture();
}

@Override
public Future<Boolean> delete(String id) {
var timerDescriptor = map.remove(id);
return Future.succeededFuture(timerDescriptor != null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.folio.okapi.service.impl;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;

import org.folio.okapi.bean.TimerDescriptor;
import org.folio.okapi.util.PgTestBase;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.junit5.Timeout;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;

@Timeout(5000)
@ExtendWith(VertxExtension.class)
class TimerStorePostgresTest extends PgTestBase {

static TimerStorePostgres timerStorePostgres;

@BeforeAll
static void beforeAll(Vertx vertx) {
var conf = new JsonObject()
.put("postgres_host", POSTGRESQL_CONTAINER.getHost())
.put("postgres_port", POSTGRESQL_CONTAINER.getFirstMappedPort() + "")
.put("postgres_database", POSTGRESQL_CONTAINER.getDatabaseName())
.put("postgres_username", POSTGRESQL_CONTAINER.getUsername())
.put("postgres_password", POSTGRESQL_CONTAINER.getPassword());
var postgresHandle = new PostgresHandle(vertx, conf);
timerStorePostgres = new TimerStorePostgres(postgresHandle);
}

@Test
void test(VertxTestContext vtc) {
timerStorePostgres.init(false)
.compose(x -> timerStorePostgres.getAll())
.onComplete(vtc.succeeding(list -> assertThat(list, is(empty()))))
.compose(x -> timerStorePostgres.put(timerDescriptor("test_tenant_mod-expire_0")))
.compose(x -> timerStorePostgres.getAll())
.onComplete(vtc.succeeding(list -> {
assertThat(list, hasSize(1));
assertThat(list.get(0).getId(), is("test_tenant_mod-expire_0"));
}))
.compose(x -> timerStorePostgres.delete("test_tenant_mod-expire_0"))
.compose(x -> timerStorePostgres.getAll())
.onComplete(vtc.succeeding(list -> {
assertThat(list, is(empty()));
vtc.completeNow();
}));
}

TimerDescriptor timerDescriptor(String id) {
var timerDescriptor = new TimerDescriptor();
timerDescriptor.setId(id);
return timerDescriptor;
}
}
Loading

0 comments on commit 5d3dedc

Please sign in to comment.