diff --git a/.gitignore b/.gitignore index 05cec97..ffce103 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,7 @@ graphql_api_assignment-*.tar .cursor/ +.elixir_ls + +.env* + diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 0000000..caee60b --- /dev/null +++ b/.iex.exs @@ -0,0 +1,3 @@ +alias GraphqlApiAssignment.HashringCounter + +name = HashringCounter.hash_ring_name() diff --git a/config/config.exs b/config/config.exs index 2e8e964..0bca631 100644 --- a/config/config.exs +++ b/config/config.exs @@ -11,7 +11,10 @@ config :graphql_api_assignment, GraphqlApiAssignment.Repo, database: "graphql_api_assignment_repo", username: "user", password: "password", - hostname: "localhost" + hostname: "localhost", + show_sensitive_data_on_connection_error: true, + log: :debug, + stacktrace: true config :graphql_api_assignment, ecto_repos: [GraphqlApiAssignment.Repo] @@ -30,6 +33,18 @@ config :prometheus_telemetry, ecto_max_query_length: 150, ecto_known_query_module: nil +config :request_cache_plug, + enabled?: true, + verbose?: false, + graphql_paths: ["/graphiql", "/graphql"], + request_cache_module: GraphqlApiAssignment.RedixPool + +config :libring, + rings: [ + ring_a: [monitor_nodes: true], + ring_b: [monitor_nodes: true] + ] + # Configures the endpoint config :graphql_api_assignment, GraphqlApiAssignmentWeb.Endpoint, url: [host: "localhost"], diff --git a/config/test.exs b/config/test.exs index 6b63443..96b7f6b 100644 --- a/config/test.exs +++ b/config/test.exs @@ -12,7 +12,11 @@ config :graphql_api_assignment, GraphqlApiAssignment.Repo, username: "user", password: "password", hostname: "localhost", - pool: Ecto.Adapters.SQL.Sandbox + pool: Ecto.Adapters.SQL.Sandbox, + show_sensitive_data_on_connection_error: true, + log: :debug, + stacktrace: true, + pool_size: 10 # In test we don't send emails config :graphql_api_assignment, GraphqlApiAssignment.Mailer, adapter: Swoosh.Adapters.Test diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6bbf4dc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +networks: + monitoring: + driver: bridge + +services: + prometheus: + image: prom/prometheus:v3.1.0 + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prom_config.yml:/etc/prometheus/prometheus.yml + networks: + - monitoring + + + grafana: + image: grafana/grafana:11.6.0 + container_name: grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + networks: + - monitoring + depends_on: + - prometheus + + postgres: + image: postgres:14 + container_name: postgres + restart: always + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: graphql_api_assignment_repo + ports: + - "5432:5432" + networks: + - monitoring diff --git a/grafana/custom/hash_ring_export.json b/grafana/custom/hash_ring_export.json new file mode 100644 index 0000000..8645762 --- /dev/null +++ b/grafana/custom/hash_ring_export.json @@ -0,0 +1,232 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 8, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "editorMode": "code", + "expr": "rate(grapql_api_hash_ring_get_duration_millisecond_bucket[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Hash Ring Get Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "editorMode": "code", + "expr": "rate(grapql_api_hash_ring_put_duration_millisecond_bucket[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Hash Ring Put Duration", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Hash Ring", + "uid": "feifig66c1n9cb", + "version": 1 + } \ No newline at end of file diff --git a/grafana/custom/token_pipeline_export.json b/grafana/custom/token_pipeline_export.json index 8daa8ef..6f7bcd2 100644 --- a/grafana/custom/token_pipeline_export.json +++ b/grafana/custom/token_pipeline_export.json @@ -1,209 +1,206 @@ { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 3, - "links": [], - "panels": [ + "annotations": { + "list": [ { + "builtIn": 1, "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" + "type": "grafana", + "uid": "-- Grafana --" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 5, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": true, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false }, - "custom": { - "axisBorderShow": true, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": [ - "sum" - ], - "displayMode": "table", - "placement": "right", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] } }, - "pluginVersion": "11.5.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" - }, - "editorMode": "code", - "expr": "rate(grapql_api_token_generation_duration_millisecond_bucket[1m])", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Token Generation Duration", - "type": "timeseries" + "overrides": [] }, - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "rate(grapql_api_token_generation_duration_millisecond_bucket[1m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Token Generation Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" }, - "overrides": [] + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 1, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false }, - "id": 1, - "options": { - "minVizHeight": 75, - "minVizWidth": 75, - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto" - }, - "pluginVersion": "11.5.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" - }, - "editorMode": "code", - "expr": "grapql_api_token_generation_total_count", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Total Session Tokens Generated", - "type": "gauge" - } - ], - "preload": false, - "refresh": "", - "schemaVersion": 40, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": {}, - "timezone": "browser", - "title": "Token Pipeline", - "uid": "aedynclbhdg5cc", - "version": 2, - "weekStart": "" - } \ No newline at end of file + "editorMode": "code", + "expr": "grapql_api_token_generation_total_count", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total Session Tokens Generated", + "type": "gauge" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Token Pipeline", + "uid": "ceifdsnsks2kgb", + "version": 2 +} \ No newline at end of file diff --git a/grafana/ecto_export.json b/grafana/ecto_export.json index d315600..be78395 100644 --- a/grafana/ecto_export.json +++ b/grafana/ecto_export.json @@ -18,13 +18,13 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 2, + "id": 6, "links": [], "panels": [ { "datasource": { "type": "prometheus", - "uid": "cedvl3fku3chsc" + "uid": "eeifd9qqffev4c" }, "fieldConfig": { "defaults": { @@ -69,8 +69,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -126,7 +125,7 @@ "sort": "none" } }, - "pluginVersion": "11.5.1", + "pluginVersion": "11.6.0", "targets": [ { "datasource": { @@ -147,7 +146,7 @@ { "datasource": { "type": "prometheus", - "uid": "cedvl3fku3chsc" + "uid": "eeifd9qqffev4c" }, "fieldConfig": { "defaults": { @@ -192,8 +191,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -249,7 +247,7 @@ "sort": "none" } }, - "pluginVersion": "11.5.1", + "pluginVersion": "11.6.0", "targets": [ { "datasource": { @@ -270,7 +268,7 @@ { "datasource": { "type": "prometheus", - "uid": "cedvl3fku3chsc" + "uid": "eeifd9qqffev4c" }, "fieldConfig": { "defaults": { @@ -282,8 +280,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -332,7 +329,7 @@ "showThresholdMarkers": true, "sizing": "auto" }, - "pluginVersion": "11.5.1", + "pluginVersion": "11.6.0", "targets": [ { "datasource": { @@ -353,7 +350,7 @@ { "datasource": { "type": "prometheus", - "uid": "cedvl3fku3chsc" + "uid": "eeifd9qqffev4c" }, "fieldConfig": { "defaults": { @@ -365,8 +362,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -415,7 +411,7 @@ "showThresholdMarkers": true, "sizing": "auto" }, - "pluginVersion": "11.5.1", + "pluginVersion": "11.6.0", "targets": [ { "datasource": { @@ -436,7 +432,7 @@ ], "preload": false, "refresh": "", - "schemaVersion": 40, + "schemaVersion": 41, "tags": [], "templating": { "list": [] @@ -449,6 +445,5 @@ "timezone": "browser", "title": "Ecto", "uid": "fedy9ix6qgo3kc", - "version": 1, - "weekStart": "" + "version": 2 } \ No newline at end of file diff --git a/grafana/graphql_export.json b/grafana/graphql_export.json index fec2e14..ef0e62a 100644 --- a/grafana/graphql_export.json +++ b/grafana/graphql_export.json @@ -1,372 +1,367 @@ { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 1, - "links": [], - "panels": [ + "annotations": { + "list": [ { + "builtIn": 1, "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" + "type": "grafana", + "uid": "-- Grafana --" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 100 - } - ] - } + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 7, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" }, - "overrides": [] + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 100 + } + ] + } }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false }, - "id": 2, - "options": { - "minVizHeight": 75, - "minVizWidth": 75, - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto" - }, - "pluginVersion": "11.5.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" - }, - "editorMode": "code", - "expr": "histogram_quantile(0.95, rate(graphql_execute_duration_milliseconds_bucket[1m]))", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "GraphQL Execute Duration Latency", - "type": "gauge" + "editorMode": "code", + "expr": "histogram_quantile(0.95, rate(graphql_execute_duration_milliseconds_bucket[1m]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GraphQL Execute Duration Latency", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" }, - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] + "thresholdsStyle": { + "mode": "off" } }, - "overrides": [] + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "id": 1, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" + }, + "editorMode": "code", + "expr": "rate(graphql_execute_duration_milliseconds_count[1m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GraphQL Execute Duration RPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 100 + } + ] } }, - "pluginVersion": "11.5.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" - }, - "editorMode": "code", - "expr": "rate(graphql_execute_duration_milliseconds_count[1m])", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "GraphQL Execute Duration RPS", - "type": "timeseries" + "overrides": [] }, - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 100 - } - ] - } + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 8 - }, - "id": 3, - "options": { - "minVizHeight": 75, - "minVizWidth": 75, - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false + "editorMode": "code", + "expr": "histogram_quantile(0.95, rate(graphql_resolve_duration_milliseconds_bucket[1m]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GraphQL Resolve Duration Latency", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "eeifd9qqffev4c" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto" - }, - "pluginVersion": "11.5.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false }, - "editorMode": "code", - "expr": "histogram_quantile(0.95, rate(graphql_resolve_duration_milliseconds_bucket[1m]))", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "GraphQL Resolve Duration Latency", - "type": "gauge" - }, - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] + "thresholdsStyle": { + "mode": "off" } }, - "overrides": [] + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 8 + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "id": 4, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "cedvl3fku3chsc" }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "11.5.1", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "cedvl3fku3chsc" - }, - "editorMode": "code", - "expr": "rate(graphql_resolve_duration_milliseconds_count[1m])", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "GraphQL Resolve Duration RPS", - "type": "timeseries" - } - ], - "preload": false, - "refresh": "", - "schemaVersion": 40, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": {}, - "timezone": "browser", - "title": "GraphQL", - "uid": "cedy6nr1zn85cc", - "version": 1, - "weekStart": "" - } \ No newline at end of file + "editorMode": "code", + "expr": "rate(graphql_resolve_duration_milliseconds_count[1m])", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "GraphQL Resolve Duration RPS", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "GraphQL", + "uid": "cedy6nr1zn85cc", + "version": 2 +} \ No newline at end of file diff --git a/lib/graphql_api_assignment/application.ex b/lib/graphql_api_assignment/application.ex index 14a9d99..b556c22 100644 --- a/lib/graphql_api_assignment/application.ex +++ b/lib/graphql_api_assignment/application.ex @@ -10,7 +10,12 @@ defmodule GraphqlApiAssignment.Application do topologies = [ example: [ strategy: Cluster.Strategy.Epmd, - config: [hosts: [:"node_a@localhost", :"node_b@localhost"]], + config: [ + hosts: [ + :node1@localhost, + :node2@localhost + ] + ] ] ] @@ -23,25 +28,33 @@ defmodule GraphqlApiAssignment.Application do {Finch, name: GraphqlApiAssignment.Finch}, # Start a worker by calling: GraphqlApiAssignment.Worker.start_link(arg) # {GraphqlApiAssignment.Worker, arg}, + GraphqlApiAssignment.RedixPool.child_spec(), # Start to serve requests, typically the last entry GraphqlApiAssignmentWeb.Endpoint, {Absinthe.Subscription, GraphqlApiAssignmentWeb.Endpoint}, GraphqlApiAssignment.Repo, - GraphqlApiAssignment.ResolverBucket, GraphqlApiAssignment.TokenCache, {PrometheusTelemetry, - exporter: [enabled?: true], - metrics: [ - PrometheusTelemetry.Metrics.Ecto.metrics_for_repo(GraphqlApiAssignment.Repo), - PrometheusTelemetry.Metrics.GraphQL.metrics(), - GraphqlApiAssignment.Metrics.TokenPipeline.metrics() - ] - }, + exporter: [enabled?: true, opts: [port: get_port("PROMETHEUS_PORT")]], + metrics: [ + PrometheusTelemetry.Metrics.Ecto.metrics_for_repo(GraphqlApiAssignment.Repo), + PrometheusTelemetry.Metrics.GraphQL.metrics(), + GraphqlApiAssignment.Metrics.TokenPipeline.metrics(), + GraphqlApiAssignment.Metrics.HashringCounter.metrics() + ]}, GraphqlApiAssignment.SecurityClearanceQueue, GraphqlApiAssignment.ResourceScheduler, - {GraphqlApiAssignment.TokenPipeline.TokenProducer, []}, - {GraphqlApiAssignment.TokenPipeline.TokenProducerConsumer, []}, - {GraphqlApiAssignment.TokenPipeline.TokenConsumer, []} + GraphqlApiAssignment.TokenPipelineSupervisor, + %{ + id: :hashring_counter, + start: + {HashRing.Managed, :new, + [ + GraphqlApiAssignment.HashringCounter.hash_ring_name(), + [monitor_nodes: true, node_type: :visible] + ]} + }, + GraphqlApiAssignment.HashringCounter ] # See https://hexdocs.pm/elixir/Supervisor.html @@ -50,6 +63,10 @@ defmodule GraphqlApiAssignment.Application do Supervisor.start_link(children, opts) end + defp get_port(k) do + String.to_integer(System.get_env(k, "5000")) + end + # Tell Phoenix to update the endpoint configuration # whenever the application is updated. @impl true diff --git a/lib/graphql_api_assignment/hashring_counter.ex b/lib/graphql_api_assignment/hashring_counter.ex new file mode 100644 index 0000000..696c009 --- /dev/null +++ b/lib/graphql_api_assignment/hashring_counter.ex @@ -0,0 +1,80 @@ +defmodule GraphqlApiAssignment.HashringCounter do + @doc """ + I opted for the Hasring as I prefered avalaibility and partition tolerance + over consistency (eventual consistency is fine for my use case). I wanted to + avoid system downtime or refusal to serve requests. + """ + use Task, restart: :permanent + + @hash_ring_name :default_hash_counter + @ets_options [:public, :set, :named_table] + @replica_count 2 + + def hash_ring_name, do: @hash_ring_name + + def start_link(opts) do + opts = Keyword.put_new(opts, :name, @hash_ring_name) + + Task.start_link(fn -> + setup(opts) + end) + end + + defp setup(opts) do + :ets.new(table_name(opts[:name]), @ets_options) + Process.hibernate(Function, :identity, []) + end + + def put(hash_ring \\ @hash_ring_name, key, _value \\ nil) do + {duration, res} = + :timer.tc(fn -> + hash_ring + |> key_to_node(key) + |> Enum.each( + &:erpc.cast(&1, fn -> + hash_ring + |> table_name() + |> :ets.update_counter(key, {2, 1}, {key, 0}) + end) + ) + end) + + GraphqlApiAssignment.Metrics.HashringCounter.inc_hash_ring_put_duration(duration) + res + end + + def get(hash_ring \\ @hash_ring_name, key) do + {duration, res} = + :timer.tc(fn -> + hash_ring + |> key_to_node(key) + |> Enum.random() + |> :erpc.call(fn -> + res = + hash_ring + |> table_name() + |> :ets.lookup(key) + + case res do + [{_, value}] -> value + _ -> 0 + end + end) + end) + + GraphqlApiAssignment.Metrics.HashringCounter.inc_hash_ring_get_duration(duration) + res + end + + def key_to_node(hash_ring \\ @hash_ring_name, key) do + HashRing.Managed.key_to_nodes(hash_ring, key, @replica_count) + end + + def table_name(hash_ring), do: :"#{hash_ring}_ets" + + @spec get_key_count(key :: atom()) :: integer() + def get_key_count(key), do: get(hash_ring_name(), key) + + @spec increment_key(key :: atom()) :: :ok + def increment_key(key), do: put(hash_ring_name(), key) +end diff --git a/lib/graphql_api_assignment/metrics/hashring_counter.ex b/lib/graphql_api_assignment/metrics/hashring_counter.ex new file mode 100644 index 0000000..d1d89a1 --- /dev/null +++ b/lib/graphql_api_assignment/metrics/hashring_counter.ex @@ -0,0 +1,37 @@ +defmodule GraphqlApiAssignment.Metrics.HashringCounter do + @moduledoc false + import Telemetry.Metrics, only: [distribution: 2] + + @event_prefix [:grapql_api] + @distribution_event_name @event_prefix ++ [:hash_ring] + @get_event_name @distribution_event_name ++ [:get] + @put_event_name @distribution_event_name ++ [:put] + @buckets PrometheusTelemetry.Config.default_millisecond_buckets() + + def metrics do + [ + distribution( + "grapql_api.hash_ring.get.duration.millisecond", + event_name: @get_event_name, + measurement: :duration, + description: "Time to process counter get requests", + reporter_options: [buckets: @buckets] + ), + distribution( + "grapql_api.hash_ring.put.duration.millisecond", + event_name: @put_event_name, + measurement: :duration, + description: "Time to process counter get requests", + reporter_options: [buckets: @buckets] + ) + ] + end + + def inc_hash_ring_get_duration(duration) do + :telemetry.execute(@get_event_name, %{duration: duration}) + end + + def inc_hash_ring_put_duration(duration) do + :telemetry.execute(@put_event_name, %{duration: duration}) + end +end diff --git a/lib/graphql_api_assignment/redix_pool.ex b/lib/graphql_api_assignment/redix_pool.ex new file mode 100644 index 0000000..d6bb5dc --- /dev/null +++ b/lib/graphql_api_assignment/redix_pool.ex @@ -0,0 +1,86 @@ +defmodule GraphqlApiAssignment.RedixPool do + @default_name :redix_pool + @default_size 10 + @default_overflow 10 + + def child_spec(opts \\ []) do + name = Keyword.get(opts, :name, @default_name) + + :poolboy.child_spec(name, + name: {:local, name}, + worker_module: Redix, + size: opts[:pool_size] || @default_size, + max_overflow: opts[:max_overflow] || @default_overflow + ) + end + + def put(key, ttl \\ nil, value, name \\ @default_name) + + @doc """ + Stores a value in the Redis database with an optional time-to-live (TTL). + + ## Parameters + + - `key`: The key under which the value will be stored (binary). + - `ttl`: The time-to-live for the key in seconds (integer). If not provided, the key will not expire. + - `value`: The value to be stored (term). + - `name`: The name of the pool to use (atom). Defaults to `:redix_pool`. + + ## Examples + + iex> GraphqlApiAssignment.RedixPool.put("my_key", nil, "my_value") + :ok + + iex> GraphqlApiAssignment.RedixPool.put("my_key", 3600, "my_value") + :ok + + """ + @spec put(binary(), nil | integer(), term(), term()) :: :ok + def put(key, nil, value, name) do + :poolboy.transaction(name, fn pid -> + with {:ok, "OK"} <- Redix.command(pid, ["SET", key, :erlang.term_to_binary(value)]) do + :ok + end + end) + end + + @spec put(key :: binary(), ttl:: integer(), value :: term(), pool_name :: term()) :: :ok + def put(key, ttl, value, name) do + :poolboy.transaction(name, fn pid -> + with {:ok, "OK"} <- Redix.command(pid, ["SETEX", key, ttl, :erlang.term_to_binary(value)]) do + :ok + end + end) + end + + @doc """ + Retrieves a value from the Redis database. + + ## Parameters + + - `key`: The key for which the value will be retrieved (binary). + - `name`: The name of the pool to use (atom). Defaults to `:redix_pool`. + + ## Examples + + iex> GraphqlApiAssignment.RedixPool.get("my_key") + {:ok, "my_value"} + + iex> GraphqlApiAssignment.RedixPool.get("non_existing_key") + {:ok, nil} + + """ + @spec get(key :: binary()) :: {:ok, term()} + @spec get(key :: binary(), pool_name :: term()) :: {:ok, term()} + def get(key, name \\ @default_name) do + :poolboy.transaction(name, fn pid -> + with {:ok, value} <- Redix.command(pid, ["GET", key]) do + if is_binary(value) do + {:ok, :erlang.binary_to_term(value)} + else + {:ok, value} + end + end + end) + end +end diff --git a/lib/graphql_api_assignment/resolver_bucket.ex b/lib/graphql_api_assignment/resolver_bucket.ex deleted file mode 100644 index b444277..0000000 --- a/lib/graphql_api_assignment/resolver_bucket.ex +++ /dev/null @@ -1,41 +0,0 @@ -defmodule GraphqlApiAssignment.ResolverBucket do - use GenServer - - @default_name __MODULE__ - @table_name :bucket - - def start_link(opts \\ []) do - table_name = Keyword.get(opts, :table_name, @table_name) - opts = Keyword.put_new(opts, :name, @default_name) - GenServer.start(__MODULE__, %{table_name: table_name}, opts) - end - - # API - def increment_key(key, name \\ @default_name) do - GenServer.cast(name, key) - end - - def get_key_count(key, name \\ @default_name) do - GenServer.call(name, key) - end - - @impl true - def init(%{table_name: table_name}) do - :ets.new(table_name, [:named_table, read_concurrency: true]) - {:ok, %{table_name: table_name}} - end - - @impl true - def handle_call(key, _from, state) do - case :ets.lookup(state.table_name, key) do - [{^key, count}] -> {:reply, count, state} - [] -> {:reply, 0, state} - end - end - - @impl true - def handle_cast(key, state) do - :ets.update_counter(state.table_name, key, {2, 1}, {key, 0}) - {:noreply, state} - end -end diff --git a/lib/graphql_api_assignment/resource_scheduler.ex b/lib/graphql_api_assignment/resource_scheduler.ex index 213dd1b..462ec23 100644 --- a/lib/graphql_api_assignment/resource_scheduler.ex +++ b/lib/graphql_api_assignment/resource_scheduler.ex @@ -9,8 +9,7 @@ defmodule GraphqlApiAssignment.ResourceScheduler do def start_link(opts \\ []) do opts = Keyword.put_new(opts, :name, @default_name) - interval = Keyword.get(opts, :interval, @deault_interval) - opts = Keyword.delete(opts, :interval) + {interval, opts} = Keyword.pop(opts, :interval, @deault_interval) GenServer.start(__MODULE__, %{interval: interval}, opts) end diff --git a/lib/graphql_api_assignment/token_cache.ex b/lib/graphql_api_assignment/token_cache.ex index 02398fb..498110f 100644 --- a/lib/graphql_api_assignment/token_cache.ex +++ b/lib/graphql_api_assignment/token_cache.ex @@ -25,7 +25,7 @@ defmodule GraphqlApiAssignment.TokenCache do def handle_call({:get, user_id}, _from, state) do case :ets.lookup(state.table_name, user_id) do [{^user_id, token}] -> {:reply, token, state} - [] -> {:reply, nil, state} + [] -> {:reply, "Token creation is processing. Token will be distributed once processing is done.", state} end end diff --git a/lib/graphql_api_assignment/token_pipeline/token_consumer.ex b/lib/graphql_api_assignment/token_pipeline/token_consumer.ex index a37b033..b7d0819 100644 --- a/lib/graphql_api_assignment/token_pipeline/token_consumer.ex +++ b/lib/graphql_api_assignment/token_pipeline/token_consumer.ex @@ -6,12 +6,13 @@ defmodule GraphqlApiAssignment.TokenPipeline.TokenConsumer do def start_link(opts \\ []) do opts = Keyword.put_new(opts, :name, @default_name) - GenStage.start_link(__MODULE__, [], opts) + {subscribe_to, opts} = Keyword.pop(opts, :subscribe_to, TokenPipeline.TokenProducer) + GenStage.start_link(__MODULE__, [subscribe_to: subscribe_to], opts) end def init(state) do {:consumer, state, - subscribe_to: [{TokenPipeline.TokenProducerConsumer, min_demand: 0, max_demand: 10}]} + subscribe_to: [{Keyword.get(state, :subscribe_to), min_demand: 0, max_demand: 10}]} end def handle_events(user_tokens, _from, state) do diff --git a/lib/graphql_api_assignment/token_pipeline/token_producer_consumer.ex b/lib/graphql_api_assignment/token_pipeline/token_producer_consumer.ex index c1e2744..b62cff6 100644 --- a/lib/graphql_api_assignment/token_pipeline/token_producer_consumer.ex +++ b/lib/graphql_api_assignment/token_pipeline/token_producer_consumer.ex @@ -7,12 +7,13 @@ defmodule GraphqlApiAssignment.TokenPipeline.TokenProducerConsumer do def start_link(opts \\ []) do opts = Keyword.put_new(opts, :name, @default_name) - GenStage.start_link(__MODULE__, [], opts) + {subscribe_to, opts} = Keyword.pop(opts, :subscribe_to, TokenPipeline.TokenProducer) + GenStage.start_link(__MODULE__, [subscribe_to: subscribe_to], opts) end def init(state) do {:producer_consumer, state, - subscribe_to: [{TokenPipeline.TokenProducer, min_demand: 0, max_demand: 10}]} + subscribe_to: [{Keyword.get(state, :subscribe_to), min_demand: 0, max_demand: 10}]} end def handle_events(users, _from, state) do diff --git a/lib/graphql_api_assignment/token_pipeline_supervisor.ex b/lib/graphql_api_assignment/token_pipeline_supervisor.ex new file mode 100644 index 0000000..9aae666 --- /dev/null +++ b/lib/graphql_api_assignment/token_pipeline_supervisor.ex @@ -0,0 +1,55 @@ +defmodule GraphqlApiAssignment.TokenPipelineSupervisor do + use Supervisor + + @default_name __MODULE__ + + def start_link(opts \\ []) do + {prefix, opts} = Keyword.pop(opts, :prefix, :default) + options = Keyword.put_new(opts, :name, @default_name) + Supervisor.start_link(__MODULE__, prefix, options) + end + + # @impl true + def init(prefix) do + prefix + |> children() + |> Supervisor.init(strategy: :rest_for_one) + end + + def child_spec(args) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, [args]}, + type: :supervisor + } + end + + defp children(prefix) do + [ + {GraphqlApiAssignment.TokenPipeline.TokenProducer, + [name: get_name(prefix, :token_producer)]}, + {GraphqlApiAssignment.TokenPipeline.TokenProducerConsumer, + [ + name: get_name(prefix, :token_producer_consumer), + subscribe_to: get_name(prefix, :token_producer) + ]}, + {GraphqlApiAssignment.TokenPipeline.TokenConsumer, + [ + name: get_name(prefix, :token_consumer), + subscribe_to: get_name(prefix, :token_producer_consumer) + ]} + ] + end + + defp get_name(prefix, postfix) when is_atom(prefix) do + String.to_atom(Atom.to_string(prefix) <> "_" <> Atom.to_string(postfix)) + end + + defp get_name(prefix, postfix) when is_binary(prefix) do + String.to_atom(prefix <> "_" <> Atom.to_string(postfix)) + end + + defp get_name(_prefix, _postfix) do + raise "Prefix must be either atom or binary" + end +end diff --git a/lib/graphql_api_assignment_web/endpoint.ex b/lib/graphql_api_assignment_web/endpoint.ex index 4eb223a..02acc57 100644 --- a/lib/graphql_api_assignment_web/endpoint.ex +++ b/lib/graphql_api_assignment_web/endpoint.ex @@ -40,6 +40,9 @@ defmodule GraphqlApiAssignmentWeb.Endpoint do plug Plug.RequestId plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + # Request Cache + plug RequestCache.Plug + plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], @@ -49,4 +52,7 @@ defmodule GraphqlApiAssignmentWeb.Endpoint do plug Plug.Head plug Plug.Session, @session_options plug GraphqlApiAssignmentWeb.Router + + plug Absinthe.Plug, before_send: {RequestCache, :connect_absinthe_context_to_conn} + end diff --git a/lib/graphql_api_assignment_web/resolvers/bucket_resolver.ex b/lib/graphql_api_assignment_web/resolvers/bucket_resolver.ex index 560b7c5..a670453 100644 --- a/lib/graphql_api_assignment_web/resolvers/bucket_resolver.ex +++ b/lib/graphql_api_assignment_web/resolvers/bucket_resolver.ex @@ -1,13 +1,13 @@ defmodule GraphqlApiAssignmentWeb.Resolvers.BucketResolver do - alias GraphqlApiAssignment.ResolverBucket + alias GraphqlApiAssignment.HashringCounter def get_resolver_hits(_, %{key: key}, _) do case key do - "create_user" -> {:ok, ResolverBucket.get_key_count(:create_user)} - "get_user" -> {:ok, ResolverBucket.get_key_count(:get_user)} - "get_users" -> {:ok, ResolverBucket.get_key_count(:get_users)} - "update_user" -> {:ok, ResolverBucket.get_key_count(:update_user)} - "update_user_preferences" -> {:ok, ResolverBucket.get_key_count(:update_user_preferences)} + "create_user" -> {:ok, HashringCounter.get_key_count(:create_user)} + "get_user" -> {:ok, HashringCounter.get_key_count(:get_user)} + "get_users" -> {:ok, HashringCounter.get_key_count(:get_users)} + "update_user" -> {:ok, HashringCounter.get_key_count(:update_user)} + "update_user_preferences" -> {:ok, HashringCounter.get_key_count(:update_user_preferences)} end end end diff --git a/lib/graphql_api_assignment_web/resolvers/user_resolver.ex b/lib/graphql_api_assignment_web/resolvers/user_resolver.ex index 1d06920..949112c 100644 --- a/lib/graphql_api_assignment_web/resolvers/user_resolver.ex +++ b/lib/graphql_api_assignment_web/resolvers/user_resolver.ex @@ -1,29 +1,29 @@ defmodule GraphqlApiAssignmentWeb.Resolvers.UserResolver do alias GraphqlApiAssignment.UserService - alias GraphqlApiAssignment.ResolverBucket + alias GraphqlApiAssignment.HashringCounter def get_user_by_id(_, %{id: id}, _) do - ResolverBucket.increment_key(:get_user) + HashringCounter.increment_key(:get_user) UserService.get_user_by_id(id) end def create_user(_, args, _) do - ResolverBucket.increment_key(:create_user) + HashringCounter.increment_key(:create_user) UserService.create_user(args) end def get_users_by_preferences(_, args, _) do - ResolverBucket.increment_key(:get_users) + HashringCounter.increment_key(:get_users) UserService.get_users(args) end def update_a_user(_, args, _) do - ResolverBucket.increment_key(:update_user) + HashringCounter.increment_key(:update_user) UserService.update_a_user(args) end def update_user_preference(_, args, _) do - ResolverBucket.increment_key(:update_user_preferences) + HashringCounter.increment_key(:update_user_preferences) UserService.update_user_preference(args) end diff --git a/lib/graphql_api_assignment_web/schema/queries/bucket_query.ex b/lib/graphql_api_assignment_web/schema/queries/bucket_query.ex index 9da90dd..33b408e 100644 --- a/lib/graphql_api_assignment_web/schema/queries/bucket_query.ex +++ b/lib/graphql_api_assignment_web/schema/queries/bucket_query.ex @@ -7,6 +7,9 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.BucketQuery do @desc "Get a resolver's total hits" field :resolver_hits, :integer do arg :key, :bucket_action + + middleware RequestCache.Middleware, ttl: :timer.seconds(60) + resolve &BucketResolver.get_resolver_hits/3 end end diff --git a/lib/graphql_api_assignment_web/schema/queries/user_query.ex b/lib/graphql_api_assignment_web/schema/queries/user_query.ex index 05d24a9..26f1827 100644 --- a/lib/graphql_api_assignment_web/schema/queries/user_query.ex +++ b/lib/graphql_api_assignment_web/schema/queries/user_query.ex @@ -7,6 +7,9 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.UserQuery do @desc "Get a user by ID" field :user, :user_response do arg :id, non_null(:integer) + + middleware RequestCache.Middleware, ttl: :timer.seconds(60) + resolve &UserResolver.get_user_by_id/3 end @@ -16,6 +19,9 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.UserQuery do arg :after, :integer arg :first, :integer arg :preferences, :preference_input + + middleware RequestCache.Middleware, ttl: :timer.seconds(60) + resolve &UserResolver.get_users_by_preferences/3 end end diff --git a/lib/graphql_api_assignment_web/types/user_response_type.ex b/lib/graphql_api_assignment_web/types/user_response_type.ex index 0aa0246..c2451bf 100644 --- a/lib/graphql_api_assignment_web/types/user_response_type.ex +++ b/lib/graphql_api_assignment_web/types/user_response_type.ex @@ -9,6 +9,8 @@ defmodule GraphqlApiAssignmentWeb.Types.UserResponseType do field :preferences, :preference_response field :auth_token, :string do + middleware RequestCache.Middleware, ttl: :timer.seconds(60) + resolve &UserResolver.get_user_auth_token/3 end end diff --git a/mix.exs b/mix.exs index 86223e1..6a49806 100644 --- a/mix.exs +++ b/mix.exs @@ -53,7 +53,12 @@ defmodule GraphqlApiAssignment.MixProject do {:dataloader, "~> 1.0.0"}, {:gen_stage, "~> 1.2.1"}, {:libcluster, "~> 3.3"}, - {:prometheus_telemetry, "~> 0.4"} + {:prometheus_telemetry, "~> 0.4"}, + {:redix, "~> 1.5.2"}, + {:castore, "~> 1.0"}, + {:poolboy, "~> 1.5.2"}, + {:request_cache_plug, "~> 1.0"}, + {:libring, "~> 1.0"} ] end @@ -67,7 +72,8 @@ defmodule GraphqlApiAssignment.MixProject do [ setup: ["deps.get", "ecto.setup"], "ecto.setup": ["ecto.create", "ecto.migrate"], - "ecto.reset": ["ecto.drop", "ecto.setup"] + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: ["ecto.drop", "ecto.create --quiet", "ecto.migrate --quiet", "test"] ] end end diff --git a/mix.lock b/mix.lock index e7260e3..77b045f 100644 --- a/mix.lock +++ b/mix.lock @@ -3,7 +3,7 @@ "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.3", "74e0862f280424b7bc290f6f69e133268bce0b4e7db0218c7e129c5c2b1d3fd4", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "caffaea03c17ea7419fe07e4bc04c2399c47f0d8736900623dbf4749a826fd2c"}, "absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"}, "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, - "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, + "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "con_cache": {:hex, :con_cache, "1.1.1", "9f47a68dfef5ac3bbff8ce2c499869dbc5ba889dadde6ac4aff8eb78ddaf6d82", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1def4d1bec296564c75b5bbc60a19f2b5649d81bfa345a2febcc6ae380e8ae15"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, @@ -12,7 +12,7 @@ "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, - "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, "ecto_shorts": {:hex, :ecto_shorts, "2.4.0", "7bbd9eaf151d73fb0f73121c563aa5f548877a71696798bbfa3f69404b9302e7", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:error_message, "~> 0.1", [hex: :error_message, repo: "hexpm", optional: false]}], "hexpm", "7460c0bb50ac4db5561e29bd145286aa0ba7344d5ed9cc22b7b86e081b60ece4"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, "elixir_cache": {:hex, :elixir_cache, "0.3.3", "ba90617426e866709596a477b2c433f6dddf17e136f4595a20049f4c5c3105b3", [:mix], [{:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:error_message, "~> 0.3", [hex: :error_message, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.2", [hex: :redix, repo: "hexpm", optional: false]}, {:sandbox_registry, "~> 0.1", [hex: :sandbox_registry, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "6d92c8e2716c4254dad5dd76d4188bcf0ca3f540c706644fbed733ceaf2e710c"}, @@ -26,16 +26,17 @@ "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"}, + "libring": {:hex, :libring, "1.7.0", "4f245d2f1476cd7ed8f03740f6431acba815401e40299208c7f5c640e1883bda", [:mix], [], "hexpm", "070e3593cb572e04f2c8470dd0c119bc1817a7a0a7f88229f43cf0345268ec42"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "paper_trail": {:hex, :paper_trail, "0.14.5", "af0f8b2d38929cd3fe9001c4b5607c7ebf01d9ef1db168db1202b38a6db015c3", [:mix], [{:ecto, ">= 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, ">= 3.9.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "9a3efa7bbf0fd619b5eb8d766a009c1e475c848e9e40f2e6a2dbe7e4579235d2"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, @@ -43,10 +44,11 @@ "prometheus_telemetry": {:hex, :prometheus_telemetry, "0.4.12", "2c480a2329e3058636e728c92e42d9e8a88bc0e64088a4d19c6fb91ebaf7ef4d", [:mix], [{:absinthe, ">= 0.0.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:ecto, ">= 0.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, ">= 0.12.0", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, ">= 0.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:oban, ">= 0.0.0", [hex: :oban, repo: "hexpm", optional: true]}, {:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:swoosh, ">= 0.0.0", [hex: :swoosh, repo: "hexpm", optional: true]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "ed217ea59fde5b51af8a7901920169235930996d20ab060d536a442c22d54510"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "redix": {:hex, :redix, "1.5.2", "ab854435a663f01ce7b7847f42f5da067eea7a3a10c0a9d560fa52038fd7ab48", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "78538d184231a5d6912f20567d76a49d1be7d3fca0e1aaaa20f4df8e1142dcb8"}, + "request_cache_plug": {:hex, :request_cache_plug, "1.0.2", "3e25e2a0cf7d729ec276e20ff7cfbdda0775a54e3dd6c6d07fee86a24227a536", [:mix], [{:absinthe, "~> 1.4", [hex: :absinthe, repo: "hexpm", optional: true]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: true]}, {:con_cache, "~> 1.0", [hex: :con_cache, repo: "hexpm", optional: true]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "9236eb6069ba7683854b698b8cedcd6c373381e9929b02b68d3b01eb71f55a12"}, "sandbox_registry": {:hex, :sandbox_registry, "0.1.1", "db6a116bf8e9553111820274701e48d1971185e89b53044f47c2a8f8e74956d7", [:mix], [], "hexpm", "88e12dc6be1bb98a05439e2b1de87db4bc0c043222b4fe4b46b5da7a0cc44948"}, "swoosh": {:hex, :swoosh, "1.17.2", "73611f08fc7cb9fa15f4909db36eeb12b70727d5c8b6a7fa0d4a31c6575db29e", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de914359f0ddc134dc0d7735e28922d49d0503f31e4bd66b44e26039c2226d39"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, diff --git a/prom_config.yml b/prom_config.yml index e32e3a8..3f458ab 100644 --- a/prom_config.yml +++ b/prom_config.yml @@ -4,4 +4,4 @@ global: scrape_configs: - job_name: "prometheus" static_configs: - - targets: ["host.docker.internal:4050"] + - targets: ["host.docker.internal:5000"] diff --git a/test/graphql_api_assignment/hashring_counter_test.exs b/test/graphql_api_assignment/hashring_counter_test.exs new file mode 100644 index 0000000..519d4bd --- /dev/null +++ b/test/graphql_api_assignment/hashring_counter_test.exs @@ -0,0 +1,52 @@ +defmodule GraphqlApiAssignment.HashringCounterTest do + use ExUnit.Case, async: true + + alias GraphqlApiAssignment.HashringCounter + + describe "incrementing user action counts" do + test "increments create_user count" do + assert HashringCounter.get_key_count(:create_user) === 0 + assert HashringCounter.increment_key(:create_user) === :ok + # Delay for 1 second + Process.sleep(1000) + + assert HashringCounter.get_key_count(:create_user) === 1 + end + + test "increments update_user count" do + assert HashringCounter.get_key_count(:update_user) === 0 + assert HashringCounter.increment_key(:update_user) === :ok + # Delay for 1 second + Process.sleep(1000) + + assert HashringCounter.get_key_count(:update_user) === 1 + end + + test "increments update_user_preferences count" do + assert HashringCounter.get_key_count(:update_user_preferences) === 0 + assert HashringCounter.increment_key(:update_user_preferences) === :ok + # Delay for 1 second + Process.sleep(1000) + + assert HashringCounter.get_key_count(:update_user_preferences) === 1 + end + + test "increments get_user count" do + assert HashringCounter.get_key_count(:get_user) === 0 + assert HashringCounter.increment_key(:get_user) === :ok + # Delay for 1 second + Process.sleep(1000) + + assert HashringCounter.get_key_count(:get_user) === 1 + end + + test "increments get_users count" do + assert HashringCounter.get_key_count(:get_users) === 0 + assert HashringCounter.increment_key(:get_users) === :ok + # Delay for 1 second + Process.sleep(1000) + + assert HashringCounter.get_key_count(:get_users) === 1 + end + end +end diff --git a/test/graphql_api_assignment/redix_pool_test.exs b/test/graphql_api_assignment/redix_pool_test.exs new file mode 100644 index 0000000..aa83bc7 --- /dev/null +++ b/test/graphql_api_assignment/redix_pool_test.exs @@ -0,0 +1,62 @@ +defmodule GraphqlApiAssignment.RedixPoolTest do + use ExUnit.Case, async: true + alias GraphqlApiAssignment.RedixPool + + setup do + # Clean up any test keys before each test + :poolboy.transaction(:redix_pool, fn pid -> + Redix.command(pid, ["FLUSHDB"]) + end) + :ok + end + + describe "put/get operations" do + test "successfully stores and retrieves string value" do + assert :ok = RedixPool.put("test_key", "test_value") + assert {:ok, "test_value"} = RedixPool.get("test_key") + end + + test "handles complex data types" do + complex_value = %{a: 1, b: [1, 2, 3], c: "test"} + assert :ok = RedixPool.put("complex_key", complex_value) + assert {:ok, ^complex_value} = RedixPool.get("complex_key") + end + + test "returns nil for non-existent key" do + assert {:ok, nil} = RedixPool.get("non_existent_key") + end + end + + describe "TTL functionality" do + test "value expires after TTL" do + assert :ok = RedixPool.put("ttl_key", 1, "will_expire") + assert {:ok, "will_expire"} = RedixPool.get("ttl_key") + + # Wait for expiration + Process.sleep(1100) + assert {:ok, nil} = RedixPool.get("ttl_key") + end + + test "value persists without TTL" do + assert :ok = RedixPool.put("persistent_key", "will_stay") + Process.sleep(1100) + assert {:ok, "will_stay"} = RedixPool.get("persistent_key") + end + end + + describe "concurrent operations" do + test "handles multiple concurrent puts and gets" do + tasks = for i <- 1..10 do + Task.async(fn -> + key = "key_#{i}" + value = "value_#{i}" + assert :ok = RedixPool.put(key, value) + assert {:ok, return_value} = RedixPool.get(key) + assert value === return_value + end) + end + + Enum.each(tasks, &Task.await/1) + end + end +end diff --git a/test/graphql_api_assignment/resolver_bucket_test.exs b/test/graphql_api_assignment/resolver_bucket_test.exs deleted file mode 100644 index 85ffb23..0000000 --- a/test/graphql_api_assignment/resolver_bucket_test.exs +++ /dev/null @@ -1,54 +0,0 @@ -defmodule GraphqlApiAssignment.ResolverBucketTest do - use ExUnit.Case, async: true - - alias GraphqlApiAssignment.ResolverBucket - - describe "incrementing user action counts" do - test "increments create_user count" do - {:ok, pid} = - ResolverBucket.start_link(name: :create_user_bucket, table_name: :create_user_table) - - assert ResolverBucket.get_key_count(:create_user, pid) === 0 - ResolverBucket.increment_key(:create_user, pid) - assert ResolverBucket.get_key_count(:create_user, pid) === 1 - end - - test "increments update_user count" do - {:ok, pid} = - ResolverBucket.start_link(name: :update_user_bucket, table_name: :update_user_table) - - assert ResolverBucket.get_key_count(:update_user, pid) === 0 - ResolverBucket.increment_key(:update_user, pid) - assert ResolverBucket.get_key_count(:update_user, pid) === 1 - end - - test "increments update_user_preferences count" do - {:ok, pid} = - ResolverBucket.start_link( - name: :update_user_preferences_bucket, - table_name: :update_user_preferences_table - ) - - assert ResolverBucket.get_key_count(:update_user_preferences, pid) === 0 - ResolverBucket.increment_key(:update_user_preferences, pid) - assert ResolverBucket.get_key_count(:update_user_preferences, pid) === 1 - end - - test "increments get_user count" do - {:ok, pid} = ResolverBucket.start_link(name: :get_user_bucket, table_name: :get_user_table) - - assert ResolverBucket.get_key_count(:get_user, pid) === 0 - ResolverBucket.increment_key(:get_user, pid) - assert ResolverBucket.get_key_count(:get_user, pid) === 1 - end - - test "increments get_users count" do - {:ok, pid} = - ResolverBucket.start_link(name: :get_users_bucket, table_name: :get_users_table) - - assert ResolverBucket.get_key_count(:get_users, pid) === 0 - ResolverBucket.increment_key(:get_users, pid) - assert ResolverBucket.get_key_count(:get_users, pid) === 1 - end - end -end diff --git a/test/graphql_api_assignment_web/schema/queries/bucket_query_test.exs b/test/graphql_api_assignment_web/schema/queries/bucket_query_test.exs index ae95870..5b5c20c 100644 --- a/test/graphql_api_assignment_web/schema/queries/bucket_query_test.exs +++ b/test/graphql_api_assignment_web/schema/queries/bucket_query_test.exs @@ -1,7 +1,7 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.BucketQueryTest do use GraphqlApiAssignment.Support.Datacase - alias GraphqlApiAssignment.ResolverBucket + alias GraphqlApiAssignment.HashringCounter alias GraphqlApiAssignmentWeb.Schema @resolver_hits_query """ @@ -19,7 +19,7 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.BucketQueryTest do Absinthe.run(@resolver_hits_query, Schema, variables: variables) # Simulate fetching a user - ResolverBucket.increment_key(:get_user) + HashringCounter.increment_key(:get_user) new_amount = starting_amount + 1 assert {:ok, %{data: %{"resolverHits" => ^new_amount}}} = @@ -33,7 +33,7 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.BucketQueryTest do Absinthe.run(@resolver_hits_query, Schema, variables: variables) # Simulate fetching users - ResolverBucket.increment_key(:get_users) + HashringCounter.increment_key(:get_users) new_amount = starting_amount + 1 assert {:ok, %{data: %{"resolverHits" => ^new_amount}}} = @@ -47,7 +47,7 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.BucketQueryTest do Absinthe.run(@resolver_hits_query, Schema, variables: variables) # Simulate creating a user - ResolverBucket.increment_key(:create_user) + HashringCounter.increment_key(:create_user) new_amount = starting_amount + 1 assert {:ok, %{data: %{"resolverHits" => ^new_amount}}} = @@ -61,7 +61,7 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.BucketQueryTest do Absinthe.run(@resolver_hits_query, Schema, variables: variables) # Simulate updating user preferences - ResolverBucket.increment_key(:update_user_preferences) + HashringCounter.increment_key(:update_user_preferences) new_amount = starting_amount + 1 assert {:ok, %{data: %{"resolverHits" => ^new_amount}}} = @@ -75,7 +75,7 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.BucketQueryTest do Absinthe.run(@resolver_hits_query, Schema, variables: variables) # Simulate updating user - ResolverBucket.increment_key(:update_user) + HashringCounter.increment_key(:update_user) new_amount = starting_amount + 1 assert {:ok, %{data: %{"resolverHits" => ^new_amount}}} = diff --git a/test/graphql_api_assignment_web/schema/queries/user_query_test.exs b/test/graphql_api_assignment_web/schema/queries/user_query_test.exs index 5fed2b6..fc46643 100644 --- a/test/graphql_api_assignment_web/schema/queries/user_query_test.exs +++ b/test/graphql_api_assignment_web/schema/queries/user_query_test.exs @@ -4,6 +4,7 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.UserQuerTest do import Support.HelperFunctions, only: [setup_mock_accounts: 1] alias GraphqlApiAssignmentWeb.Schema + alias GraphqlApiAssignment.Support.Factory.SchemasPG.AccountManagement.{ PreferenceFactory, UserFactory @@ -42,6 +43,36 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.UserQuerTest do assert user["preferences"]["userId"] === context.preference.user_id end + test "fetches a user by ID, is faster on second request due to cache", %{user: %{id: user_id}} do + variables = %{"id" => user_id} + + assert {first_call_duration, result} = + :timer.tc(fn -> Absinthe.run(@query_user, Schema, variables: variables) end) + + assert {:ok, + %{ + data: %{ + "user" => %{ + "id" => ^user_id + } + } + }} = result + + assert {second_call_duration, result} = + :timer.tc(fn -> Absinthe.run(@query_user, Schema, variables: variables) end) + + assert {:ok, + %{ + data: %{ + "user" => %{ + "id" => ^user_id + } + } + }} = result + + assert first_call_duration > second_call_duration + end + test "returns an error when ID does not exist" do variables = %{"id" => -1} @@ -81,6 +112,42 @@ defmodule GraphqlApiAssignmentWeb.Schema.Queries.UserQuerTest do assert Enum.any?(users, fn user -> user["id"] === context.user.id end) end + test "fetches users with no arguments, is faster on second request due to cache", %{ + user: %{id: user_id} + } do + variables = %{} + + assert {first_call_duration, result} = + :timer.tc(fn -> Absinthe.run(@query_users, Schema, variables: variables) end) + + assert {:ok, + %{ + data: %{ + "users" => [ + %{ + "id" => ^user_id + } + ] + } + }} = result + + assert {second_call_duration, result} = + :timer.tc(fn -> Absinthe.run(@query_users, Schema, variables: variables) end) + + assert {:ok, + %{ + data: %{ + "users" => [ + %{ + "id" => ^user_id + } + ] + } + }} = result + + assert first_call_duration > second_call_duration + end + test "fetches users with preferences", context do variables = %{"first" => 1} diff --git a/test/graphql_api_assignment_web/schema/subscriptions/user_subscription_test.exs b/test/graphql_api_assignment_web/schema/subscriptions/user_subscription_test.exs index f9771e7..231335b 100644 --- a/test/graphql_api_assignment_web/schema/subscriptions/user_subscription_test.exs +++ b/test/graphql_api_assignment_web/schema/subscriptions/user_subscription_test.exs @@ -176,16 +176,11 @@ defmodule GraphqlApiAssignmentWeb.Schema.Subscriptions.UserSubscriptionTest do ) assert {:ok, _pid} = - GraphqlApiAssignment.TokenPipeline.TokenProducer.start_link(name: :token_prod) - - assert {:ok, _pid} = - GraphqlApiAssignment.TokenPipeline.TokenProducerConsumer.start_link( - name: :token_prod_con + GraphqlApiAssignment.TokenPipelineSupervisor.start_link( + name: :token_test, + prefix: :test ) - assert {:ok, _pid} = - GraphqlApiAssignment.TokenPipeline.TokenConsumer.start_link(name: :token_con) - assert_push "subscription:data", data, :timer.seconds(5) assert %{