diff --git a/assets/diagram.jpg b/assets/diagram.jpg new file mode 100644 index 0000000..fa65205 Binary files /dev/null and b/assets/diagram.jpg differ diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..fa6c58d --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,45 @@ +version: "3.8" +networks: + k6: + grafana: +services: + influxdb: + image: influxdb:latest + networks: + - k6 + - grafana + ports: + - "8086:8086" + environment: + - INFLUXDB_DB=k6 + grafana: + image: grafana/grafana:latest + networks: + - grafana + ports: + - "3000:3000" + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./shared/grafana:/etc/grafana/provisioning/ + simulado: + image : ldabiralai/simulado:latest + networks: + - k6 + ports: + - "3001:80" + volumes: + - ./shared/simulado:/app + command: ./bin/simulado -f /app/mocks.json + k6: + image: loadimpact/k6:latest + networks: + - k6 + ports: + - "6565:6565" + volumes: + - ./shared/k6:/scripts + environment: + - K6_OUT=influxdb=http://influxdb:8086/k6 \ No newline at end of file diff --git a/existingApis.yaml b/existingApis.yaml new file mode 100644 index 0000000..c6090ba --- /dev/null +++ b/existingApis.yaml @@ -0,0 +1,65 @@ +openapi: 3.0.0 +info: + title: existingApis + version: '1.0' +servers: + - url: 'http://localhost:3001' +paths: + '/product/{productId}/similarids': + parameters: + - schema: + type: string + name: productId + in: path + required: true + get: + operationId: get-product-similarids + summary: Gets the ids of the similar products + description: Returns the similar products to a given one ordered by similarity + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + description: List of productIds similar to a given one ordered by similarity + minItems: 0 + uniqueItems: true + items: + type: integer + '/product/{productId}': + parameters: + - schema: + type: string + name: productId + in: path + required: true + get: + operationId: get-product-productId + summary: Gets a product detail + description: Returns the product detail for a given productId + responses: + '200': + description: OK + content: + application/json: + schema: + description: 'Product detail' + type: object + properties: + id: + type: string + minLength: 1 + name: + type: string + minLength: 1 + price: + type: number + availability: + type: boolean + required: + - id + - name + - price + - availability \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..dfac1af --- /dev/null +++ b/readme.md @@ -0,0 +1,33 @@ +# Backend dev technical test +We want to offer a new feature to our customers showing similar products to the one they are browsing. To do this we agreed with out front end applications to create a new REST API that will provide them the product detail of the similar products to a given one. [Here](./similarProducts.yaml) is the contract we agreed. + +We already have an endpoint that provides the product Ids similar to a given one. We also have another endpoint that returns the product detail by productId. [Here](./existingApis.yaml) is the documentation of the existing APIs. + +**Create a Spring boot application that exposes the agreed REST API on port 5000.** + +![Diagram](./assets/diagram.jpg "Diagram") + +Note that _Test_ and _Mocks_ components are given, you must only implement _yourApp_. + +## Testing and Self-evaluation +You can run the same test we will put through your application. You just need to have docker installed. + +First of all you may need to enable file sharing for the `shared` folder on your docker dashboard -> settings -> resources -> file sharing. + +Then you can start the mocks and other needed infrastructure with the following command. +``` +docker-compose up -d simulado influxdb grafana +``` +Check that mocks are working with a sample request to [http://localhost:3001/product/1/similarids](http://localhost:3001/product/1/similarids). + +To execute the test run: +``` +docker-compose run --rm k6 run scripts/test.js +``` +Browse [http://localhost:3000/d/Le2Ku9NMk/k6-performance-test](http://localhost:3000/d/Le2Ku9NMk/k6-performance-test) to view the results. + +## Evaluation +The following topics will be considered: +- Code clarity and maintainability +- Performance +- Resilence \ No newline at end of file diff --git a/shared/grafana/dashboards/dashboard.yml b/shared/grafana/dashboards/dashboard.yml new file mode 100644 index 0000000..2cb21b0 --- /dev/null +++ b/shared/grafana/dashboards/dashboard.yml @@ -0,0 +1,12 @@ + +apiVersion: 1 + +providers: +- name: 'myinfluxdb' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/shared/grafana/dashboards/performance-test-dasboard.json b/shared/grafana/dashboards/performance-test-dasboard.json new file mode 100644 index 0000000..7b227d2 --- /dev/null +++ b/shared/grafana/dashboards/performance-test-dasboard.json @@ -0,0 +1,444 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.6", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "groupBy": [ + { + "params": [ + "1s" + ], + "type": "time" + }, + { + "params": [ + "scenario" + ], + "type": "tag" + }, + { + "params": [ + "status" + ], + "type": "tag" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "measurement": "http_reqs", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "count" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Requests", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:671", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:672", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "myinfluxdb", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 8 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.6", + "pointradius": 1, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "groupBy": [ + { + "params": [ + "1s" + ], + "type": "time" + } + ], + "measurement": "http_req_duration", + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT mean(\"value\") FROM \"http_req_duration\" WHERE $timeFilter GROUP BY time($__interval) fill(null)", + "rawQuery": false, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "http-req duration", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:543", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:544", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "myinfluxdb", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 3, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 15 + }, + "hiddenSeries": false, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.6", + "pointradius": 1, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "groupBy": [ + { + "params": [ + "1s" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "measurement": "vus", + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT mean(\"value\") FROM \"http_req_duration\" WHERE $timeFilter GROUP BY time($__interval) fill(null)", + "rawQuery": false, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "vus", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:309", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:310", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 26, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "k6 performance test", + "uid": "Le2Ku9NMk", + "version": 1 +} \ No newline at end of file diff --git a/shared/grafana/datasources/datasource.yml b/shared/grafana/datasources/datasource.yml new file mode 100644 index 0000000..4199582 --- /dev/null +++ b/shared/grafana/datasources/datasource.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +datasources: + - name: myinfluxdb + type: influxdb + access: proxy + database: k6 + orgId: 1 + url: http://influxdb:8086 + isDefault: true diff --git a/shared/k6/test.js b/shared/k6/test.js new file mode 100644 index 0000000..f74d90e --- /dev/null +++ b/shared/k6/test.js @@ -0,0 +1,73 @@ +import http from 'k6/http'; +import { sleep } from 'k6'; + +export let options = { + scenarios: { + normal: { + executor: 'constant-vus', + vus: 200, + duration: '10s', + exec: "normal", + gracefulStop: '0s', + }, + notFound: { + executor: 'constant-vus', + vus: 200, + duration: '10s', + exec: "notFound", + gracefulStop: '0s', + startTime: '10s' + }, + error: { + executor: 'constant-vus', + vus: 200, + duration: '10s', + exec: "error", + gracefulStop: '0s', + startTime: '20s' + }, + slow: { + executor: 'constant-vus', + vus: 200, + duration: '10s', + exec: "slow", + gracefulStop: '10s', + startTime: '30s' + }, + verySlow: { + executor: 'constant-vus', + vus: 200, + duration: '10s', + exec: "verySlow", + gracefulStop: '30s', + startTime: '50s' + } + } +}; + +const host = "http://host.docker.internal:5000"; + +export function normal() { + http.get(host + "/product/1/similar"); + sleep(0.5); +} + +export function slow() { + http.get(host + "/product/2/similar"); + sleep(0.5); +} + +export function verySlow() { + http.get(host + "/product/3/similar"); + sleep(0.5); +} + +export function notFound() { + http.get(host + "/product/4/similar"); + sleep(0.5); +} + +export function error() { + http.get(host + "/product/5/similar"); + sleep(0.5); +} \ No newline at end of file diff --git a/shared/simulado/mocks.json b/shared/simulado/mocks.json new file mode 100644 index 0000000..31f63bc --- /dev/null +++ b/shared/simulado/mocks.json @@ -0,0 +1,77 @@ +[ + { + "path": "/product/1/similarids", + "body": "[2,3,4]", + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/2/similarids", + "body": "[3,100,1000]", + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/3/similarids", + "body": "[100,1000,10000]", + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/4/similarids", + "body": "[1,2,5]", + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/5/similarids", + "body": "[1,2,6]", + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/1", + "body": "{\"id\":\"1\",\"name\":\"Shirt\",\"price\":9.99,\"availability\":true}", + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/2", + "body": "{\"id\":\"2\",\"name\":\"Dress\",\"price\":19.99,\"availability\":true}", + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/3", + "body": "{\"id\":\"3\",\"name\":\"Blazer\",\"price\":29.99,\"availability\":false}", + "headers": {"Content-Type": "application/json"}, + "delay": 100 + }, + { + "path": "/product/4", + "body": "{\"id\":\"4\",\"name\":\"Boots\",\"price\":39.99,\"availability\":true}", + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/5", + "body": "{\"message\":\"Product not found\"}", + "status": 404, + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/6", + "status": 500, + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/100", + "body": "{\"id\":\"100\",\"name\":\"Trousers\",\"price\":49.99,\"availability\":false}", + "delay": 1000, + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/1000", + "body": "{\"id\":\"1000\",\"name\":\"Coat\",\"price\":89.99,\"availability\":true}", + "delay": 5000, + "headers": {"Content-Type": "application/json"} + }, + { + "path": "/product/10000", + "body": "{\"id\":\"10000\",\"name\":\"Leather jacket\",\"price\":89.99,\"availability\":true}", + "delay": 50000, + "headers": {"Content-Type": "application/json"} + } +] \ No newline at end of file diff --git a/similarProducts.yaml b/similarProducts.yaml new file mode 100644 index 0000000..7a0d668 --- /dev/null +++ b/similarProducts.yaml @@ -0,0 +1,45 @@ +openapi: 3.0.0 +info: + title: SimilarProducts + version: '1.0' +servers: + - url: 'http://localhost:5000' +paths: + '/product/{productId}/similar': + parameters: + - schema: + type: string + name: productId + in: path + required: true + get: + operationId: get-product-similar + summary: Similar products + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + description: 'List of similar products to a given one ordered by similarity' + minItems: 0 + uniqueItems: true + items: + type: object + required: + - id + - name + - price + - availability + properties: + id: + type: string + minLength: 1 + name: + type: string + minLength: 1 + price: + type: number + availability: + type: boolean \ No newline at end of file