From fc4c78814f391277bd1c9a436fdc10100d0ae8e7 Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Sun, 14 Apr 2024 14:28:15 +0100 Subject: [PATCH] basic concurrency tests written using K6 and added synchronization around AutoIncrement --- .../ResetAutoIncrementWhenTooHigh.java | 39 ++++++ .../simpleapi/SimpleApiRoutes.java | 2 + .../content/practice-modes/simulation.md | 2 +- .../ChallengeCompleteTest.java | 2 +- docs/k6/script.js | 121 ++++++++++++++++++ .../core/domain/instances/AutoIncrement.java | 11 +- .../core/domain/instances/ERInstanceData.java | 2 +- .../core/domain/instances/EntityInstance.java | 4 +- .../instances/EntityInstanceCollection.java | 12 +- .../core/domain/instances/InstanceFields.java | 5 +- .../domain/instances/AutoIncrementTest.java | 27 ++-- 11 files changed, 197 insertions(+), 30 deletions(-) create mode 100644 challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/ResetAutoIncrementWhenTooHigh.java create mode 100644 docs/k6/script.js diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/ResetAutoIncrementWhenTooHigh.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/ResetAutoIncrementWhenTooHigh.java new file mode 100644 index 00000000..6d3e2a27 --- /dev/null +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/ResetAutoIncrementWhenTooHigh.java @@ -0,0 +1,39 @@ +package uk.co.compendiumdev.challenge.practicemodes.simpleapi; + +import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; +import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; +import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfig; +import uk.co.compendiumdev.thingifier.application.httpapimessagehooks.HttpApiRequestHook; +import uk.co.compendiumdev.thingifier.core.EntityRelModel; +import uk.co.compendiumdev.thingifier.core.domain.instances.AutoIncrement; +import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData; +import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection; + +public class ResetAutoIncrementWhenTooHigh implements HttpApiRequestHook { + + private final EntityRelModel erModel; + + public ResetAutoIncrementWhenTooHigh(EntityRelModel eRmodel){ + this.erModel = eRmodel; + } + + @Override + public HttpApiResponse run(HttpApiRequest request, ThingifierApiConfig config) { + + ERInstanceData instanceData = erModel.getInstanceData(EntityRelModel.DEFAULT_DATABASE_NAME); + if(instanceData!=null){ + EntityInstanceCollection collection = instanceData. + getInstanceCollectionForEntityNamed("item"); + AutoIncrement idCounter = collection.getCounters().get("id"); + //if(idCounter.getCurrentValue()>2140000000){ + if(idCounter.getCurrentValue()>99999){ + // reset it + idCounter.incrementToNextAbove(0); + } + if(collection != null && collection.countInstances()<5) { + erModel.populateDatabase(EntityRelModel.DEFAULT_DATABASE_NAME); + } + } + return null; + } +} diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java index 20f9ba22..652afb66 100644 --- a/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java +++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/practicemodes/simpleapi/SimpleApiRoutes.java @@ -100,6 +100,8 @@ public void configure() { simpleApiHttpRouting = new ThingifierHttpApiRoutings(simplethings, apiDocDefn); simpleApiHttpRouting.registerHttpApiRequestHook(new AddMoreItemsIfNecessary(simplethings.getERmodel())); + simpleApiHttpRouting.registerHttpApiRequestHook(new ResetAutoIncrementWhenTooHigh(simplethings.getERmodel())); + new SimpleSparkRouteCreator("/simpleap/items").status(501, List.of("patch", "trace")); } diff --git a/challenger/src/main/resources/content/practice-modes/simulation.md b/challenger/src/main/resources/content/practice-modes/simulation.md index ea3e177d..6b7948ac 100644 --- a/challenger/src/main/resources/content/practice-modes/simulation.md +++ b/challenger/src/main/resources/content/practice-modes/simulation.md @@ -310,7 +310,7 @@ If you want to explore the tool more then you could try the experiments below, o ## Swagger OpenAPI File -You can download a simple Swagger [OpenAPI File for simulation mode](/practice-modes/simulation/swagger). +You can download a simple Swagger [OpenAPI File for simulation mode](/sim/docs/swaggercd ..). ## Simulation Mode Walkthrough - Insomnia diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/ChallengeCompleteTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/ChallengeCompleteTest.java index 312f6d3e..f0db8c95 100644 --- a/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/ChallengeCompleteTest.java +++ b/challenger/src/test/java/uk/co/compendiumdev/challenger/http/completechallenges/ChallengeCompleteTest.java @@ -622,7 +622,7 @@ public void ensureAtMostXTodoAvailable(int x){ final EntityInstanceCollection todos = ChallengeMain.getChallenger().getThingifier().getThingInstancesNamed("todo", challenger.getXChallenger()); if(todos.countInstances()>x){ for(int delCount = todos.countInstances()-x; delCount > 0; delCount--) { - todos.deleteInstance(((EntityInstance) (todos.getInstances().toArray()[0])).getInternalId()); + todos.deleteInstance((EntityInstance) (todos.getInstances().toArray()[0])); } } } diff --git a/docs/k6/script.js b/docs/k6/script.js new file mode 100644 index 00000000..9b76dc13 --- /dev/null +++ b/docs/k6/script.js @@ -0,0 +1,121 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + + scenarios: { + my_scenario1: { + executor: 'constant-arrival-rate', + duration: '60s', // total duration + preAllocatedVUs: 100, // to allocate runtime resources preAll + rate: 100, // number of constant iterations given `timeUnit` + timeUnit: '1s', + }, + }, + +// // A number specifying the number of VUs to run concurrently. +// vus: 100, +// // A string specifying the total duration of the test run. +// duration: '20s', + + + // Uncomment this section to enable the use of Browser API in your tests. + // + // See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more + // about using Browser API in your test scripts. + // + // scenarios: { + // // The scenario name appears in the result summary, tags, and so on. + // // You can give the scenario any name, as long as each name in the script is unique. + // ui: { + // // Executor is a mandatory parameter for browser-based tests. + // // Shared iterations in this case tells k6 to reuse VUs to execute iterations. + // // + // // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types. + // executor: 'shared-iterations', + // options: { + // browser: { + // // This is a mandatory parameter that instructs k6 to launch and + // // connect to a chromium-based browser, and use it to run UI-based + // // tests. + // type: 'chromium', + // }, + // }, + // }, + // } +}; + +// The function that defines VU logic. +// +// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more +// about authoring k6 scripts. +// +const origin = "http://localhost:4567"; + +export default function() { + + const itemsJson = getItems(); + + // random item id + const randomIndex = Math.floor(Math.random()*itemsJson.items.length) + const itemId = itemsJson.items[randomIndex].id; + if((Math.random()*100)<5){ + // 5% chance we will get an item + const itemJson = getItem(itemId); + } + if((Math.random()*100)<10){ + // 10% chance that we will delete something + deleteItem(itemId); + } + if(itemsJson.items.length<80){ + createRandomItem(); + } +} + +function getItems(){ + const response = http.get(`${origin}/simpleapi/items`); + check(response, { 'get items status was 200': (r) => r.status == 200 }); + if(response.status!=200){ + console.log(response.body) + } + return response.json(); +} + +function getItem(id){ + const response = http.get(`${origin}/simpleapi/items/${id}`); + check(response, { 'get item status was 200': (r) => r.status == 200 }); + if(response.status!=200){ + console.log(response.body) + } + return response.json(); +} + +function deleteItem(id){ + const response = http.del(`${origin}/simpleapi/items/${id}`); + check(response, { 'delete item status was 200': (r) => r.status == 200 }); + if(response.status!=200){ + console.log(response.body) + } +} + +function createRandomItem(){ + const types = ["cd", "book", "dvd", "blu-ray"] + const randomType = Math.floor(Math.random()*types.length) + const isbn = ("" + Math.random()).substring(2, 8) + ("" + Math.random()).substring(2, 8) + ("" + Math.random()).substring(2, 3) + const anItem = { + "type": types[randomType], + "isbn13": isbn, + "price" : ((Math.random()*100+10)+"").substring(0,5) + } + //console.log(anItem); + const response = http.post(`${origin}/simpleapi/items`, JSON.stringify(anItem), + { + headers: { 'Content-Type': 'application/json' }, + } + ) + + if(response.status==400){ + console.log(response.body) + } + check(response, { 'post item status was 201': (r) => r.status == 201 }); +} \ No newline at end of file diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/AutoIncrement.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/AutoIncrement.java index ef0e4696..dfa15263 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/AutoIncrement.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/AutoIncrement.java @@ -20,21 +20,24 @@ public String getName(){ return name; } + @Deprecated // we probably want to use getNextValueAndUpdate public int getCurrentValue(){ //TODO: have a list of free items, used prior to the nextInt // e.g. on DELETE, or if we do not create an item, or if we skip items during an increment on PUT return nextInt; } - public void update(){ + private synchronized void update(){ nextInt = nextInt + incrementBy; } - public void setNextValue(int integer) { - + public synchronized int getNextValueAndUpdate(){ + int curr = getCurrentValue(); + update(); + return curr; } - public void incrementToNextAbove(Integer integer) { + public synchronized void incrementToNextAbove(Integer integer) { nextInt = integer; update(); } diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/ERInstanceData.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/ERInstanceData.java index 84fec935..f1687cdb 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/ERInstanceData.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/ERInstanceData.java @@ -120,7 +120,7 @@ public void deleteEntityInstance(final EntityInstance anEntityInstance) { // we may also have to delete things which are mandatorily related i.e. can't exist on their own final List otherInstancesToDelete = - anInstanceCollection.deleteInstance(anEntityInstance.getInternalId()); + anInstanceCollection.deleteInstance(anEntityInstance); // TODO: Warning recursion with no 'cut off' if any cyclical relationships then this might fail for(EntityInstance deleteMe : otherInstancesToDelete){ diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstance.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstance.java index c49c6abe..541b7cac 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstance.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstance.java @@ -42,8 +42,8 @@ public void addAutoIncrementIdsToInstance(Map autos) { for(Field autoIncrementedField : entityDefinition.getFieldsOfType(FieldType.AUTO_INCREMENT)){ AutoIncrement auto = autos.get(autoIncrementedField.getName()); if(!instanceFields.hasAssignedValue(autoIncrementedField.getName())){ - instanceFields.putValue(autoIncrementedField.getName(), String.valueOf(auto.getCurrentValue())); - auto.update(); + instanceFields.putValue(autoIncrementedField.getName(), + String.valueOf(auto.getNextValueAndUpdate())); } } } diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstanceCollection.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstanceCollection.java index 3884cddb..1b1e9de9 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstanceCollection.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstanceCollection.java @@ -93,8 +93,8 @@ public EntityInstanceCollection addInstance(EntityInstance instance) { if(counter==null){ counter = createCounterFor(fieldDefn); } - instance.overrideValue(fieldDefn.getName(),String.valueOf(counter.getCurrentValue())); - counter.update(); + instance.overrideValue(fieldDefn.getName(), + String.valueOf(counter.getNextValueAndUpdate())); } }else{ if(fieldDefn.getType()==FieldType.AUTO_INCREMENT) { @@ -200,7 +200,7 @@ public List deleteInstance(String guid) { if (!instances.containsKey(guid)) { throw new IndexOutOfBoundsException( - String.format("Could not find a %s with GUID %s", + String.format("unable to delete, could not find a %s with GUID %s", definition.getName(), guid)); } @@ -214,8 +214,10 @@ public List deleteInstance(EntityInstance anInstance) { if (!instances.containsValue(anInstance)) { throw new IndexOutOfBoundsException( - String.format("Could not find a %s with Internal GUID %s", - definition.getName(), anInstance.getInternalId())); + String.format("Unable to delete, could not find a %s with %s of %s", + definition.getName(), + definition.getPrimaryKeyField().getName(), + anInstance.getPrimaryKeyValue())); } instances.remove(anInstance.getInternalId()); diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/InstanceFields.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/InstanceFields.java index ef11e4ed..a538b11a 100644 --- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/InstanceFields.java +++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/InstanceFields.java @@ -40,8 +40,9 @@ public InstanceFields addAutoIncrementIdsToInstance(AutoIncrement anAuto) { for(Field aField : idfields){ if(aField.getType()==FieldType.AUTO_INCREMENT){ if(!values.containsKey(aField.getName().toLowerCase())) { - addValue(FieldValue.is(aField, String.valueOf(anAuto.getCurrentValue()))); - anAuto.update(); + addValue( + FieldValue.is(aField, + String.valueOf(anAuto.getNextValueAndUpdate()))); } } } diff --git a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/domain/instances/AutoIncrementTest.java b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/domain/instances/AutoIncrementTest.java index b5f65553..202dd678 100644 --- a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/domain/instances/AutoIncrementTest.java +++ b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/domain/instances/AutoIncrementTest.java @@ -19,19 +19,18 @@ public void canAutoIncrement(){ @Test public void canUpdateAutoIncrement(){ AutoIncrement auto = new AutoIncrement("afield", 1); - - auto.update(); - Assertions.assertEquals(2, auto.getCurrentValue()); + Assertions.assertEquals(1, auto.getNextValueAndUpdate()); + Assertions.assertEquals(2, auto.getNextValueAndUpdate()); } @Test public void canUpdateAutoIncrementMultipleTimes(){ AutoIncrement auto = new AutoIncrement("afield", 1); - auto.update(); - auto.update(); - auto.update(); - Assertions.assertEquals(4, auto.getCurrentValue()); + Assertions.assertEquals(1, auto.getNextValueAndUpdate()); + Assertions.assertEquals(2, auto.getNextValueAndUpdate()); + Assertions.assertEquals(3, auto.getNextValueAndUpdate()); + Assertions.assertEquals(4, auto.getNextValueAndUpdate()); } @Test @@ -39,18 +38,18 @@ public void canUpdateAutoIncrementInJumps(){ AutoIncrement auto = new AutoIncrement("afield", 1); auto.by(10); - auto.update(); - Assertions.assertEquals(11, auto.getCurrentValue()); + Assertions.assertEquals(1, auto.getNextValueAndUpdate()); + Assertions.assertEquals(11, auto.getNextValueAndUpdate()); } @Test public void canUpdateAutoIncrementInMultipleJumps(){ AutoIncrement auto = new AutoIncrement("afield", 1); - auto.by(10); + auto.by(5); - auto.update(); - auto.update(); - auto.update(); - Assertions.assertEquals(31, auto.getCurrentValue()); + Assertions.assertEquals(1, auto.getNextValueAndUpdate()); + Assertions.assertEquals(6, auto.getNextValueAndUpdate()); + Assertions.assertEquals(11, auto.getNextValueAndUpdate()); + Assertions.assertEquals(16, auto.getNextValueAndUpdate()); } }