From 18fff76fbf29ce0eab9160adb034f696f66a9ea8 Mon Sep 17 00:00:00 2001 From: Pratik Prajapati <33730817+ppratikcr7@users.noreply.github.com> Date: Wed, 15 Nov 2023 23:48:52 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9E=20=20Hotfix/Split=20getValidExperi?= =?UTF-8?q?ment=20queries=20into=203=20sub=20queries=20to=20reduce=20laten?= =?UTF-8?q?cy=20against=20Release=20V5=20(#1083)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * split getValidExperiment queries into 3 sub queries to reduce latency * added cache for validExperiment function * :zap: Fixing caching Updating caching-manager and fixing caching bug * Run all queries at once for getValidExperiments * solved caching errors * revert cache-manger to older version * fix majority of unit test cases for cached valid experiment mocks * solve one of the test suite failures with appropriate mock * fix for final test suite * remove unnecessory console.logs * solve git comments * remove promise.all for sigle promises and remove metrics query from getValidExps as its not being used in assign and mark * add CACHING_TTL to env * add omnibus-specific load-test file to the repo --------- Co-authored-by: RidhamShah Co-authored-by: Vivek Fitkariwala Co-authored-by: danoswaltCL <97542869+danoswaltCL@users.noreply.github.com> --- backend/locust/load_test_omnibus.py | 419 ++++++++++++++++++ backend/locust/load_test_upgrade_mathia.py | 71 +-- backend/packages/Upgrade/.env.docker.local | 1 + backend/packages/Upgrade/.env.example | 3 +- backend/packages/Upgrade/.env.test | 2 + backend/packages/Upgrade/package-lock.json | 16 +- .../api/repositories/ExperimentRepository.ts | 170 +++++-- .../Upgrade/src/api/services/CacheService.ts | 31 +- .../services/ExperimentAssignmentService.ts | 29 +- .../src/api/services/ExperimentService.ts | 44 +- .../src/api/services/SegmentService.ts | 6 +- backend/packages/Upgrade/src/env.ts | 1 + .../repositories/ExperimentRepository.test.ts | 33 +- .../ExperimentAssignmentService.test.ts | 92 ++-- types/src/Experiment/enums.ts | 5 + types/src/index.ts | 1 + 16 files changed, 716 insertions(+), 208 deletions(-) create mode 100644 backend/locust/load_test_omnibus.py diff --git a/backend/locust/load_test_omnibus.py b/backend/locust/load_test_omnibus.py new file mode 100644 index 0000000000..9c0c196736 --- /dev/null +++ b/backend/locust/load_test_omnibus.py @@ -0,0 +1,419 @@ +from datetime import datetime +import random +import uuid +from upgrade_mathia_data import modules, workspaces +from locust import HttpUser, SequentialTaskSet, task, tag, between +import createExperiment +import deleteExperiment +import json + +schools = { + "greenvilleWaukegaen": "greenvilleWaukegaen", + "notGreenvilleWaukegaen": "notGreenvilleWaukegaen" +} +students = {} +# allExperimentPartitionIDConditionPair = [] +# For load testing on existing prod experiments: +allExperimentPartitionIDConditionPair = [ + {"site": "SelectSection", "target" : "worksheet_grapher_a1_direct_variation", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_lin_mod_mult_rep", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_slope_intercept_decimal", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_slope_intercept_integer", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_patterns_1step_eqn_ops", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_patterns_2step_expr", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_linear_systems_dec", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_linear_systems_int", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_solving_2step_int", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_solving_2step_dec_frac", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_mod_initial_plus_point", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_mod_two_points", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_mod_init_point", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_four_quadrant_graphing_1step", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_linear_models_distrib_div", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_linear_models_distrib_frac", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_solving_1step_int", "condition" : "control"}, + {"site": "SelectSection", "target" : "worksheet_grapher_a1_solving_1step_dec", "condition" : "control"}, + {"site": "SelectSection", "target" : "linear_relations_1", "condition" : "control"}, + {"site": "SelectSection", "target" : "data_displays_comparing_populations", "condition" : "control"}, + {"site": "SelectSection", "target" : "data_displays_comparing_data_sets_using_center_and_spread", "condition" : "control"}, + {"site": "SelectSection", "target" : "direct_variation_equation", "condition" : "control"}, + {"site": "SelectSection", "target" : "direct_variation_convert", "condition" : "control"}, + {"site": "SelectSection", "target" : "volume_surface_area_right_prism_vol", "condition" : "control"}, + {"site": "SelectSection", "target" : "volume_surface_area_right_prism_vol-_backward", "condition" : "control"}, + {"site": "SelectSection", "target" : "volume_surface_area_sq_pyramid_vol", "condition" : "control"}, + {"site": "SelectSection", "target" : "factor_trees_lcm_gcf", "condition" : "control"}, + {"site": "SelectSection", "target" : "factor_trees_prime_factorization", "condition" : "control"}, + {"site": "SelectSection", "target" : "geo_transforms_tr", "condition" : "control"}, + {"site": "SelectSection", "target" : "geo_transforms_re", "condition" : "control"}, + {"site": "SelectSection", "target" : "geo_transforms_ro", "condition" : "control"}, + {"site": "SelectSection", "target" : "graphs_of_functions-1", "condition" : "control"}, + {"site": "SelectSection", "target" : "solving_angle_measures", "condition" : "control"}, +] + +# Setting host URL's: +protocol = "https" +host = "upgradeapi.qa-cli.com" + +# clear existing experiments: +option = int(input("Enter 1 for delete a single random experiment and 2 to delete all experiments, else Enter 0 for not deleting any experiment: ")) + +if option == 1: + expIds = deleteExperiment.getExperimentIds(protocol, host) + deleteExperiment.deleteExperiment(protocol, host, expIds) +elif option == 2: + expIds = deleteExperiment.getExperimentIds(protocol, host) + for i in range(len(expIds)): + expIds = deleteExperiment.getExperimentIds(protocol, host) + deleteExperiment.deleteExperiment(protocol, host, expIds) +else: + pass + +# create new experiments: +experimentCount = int(input("Enter the number of experiments to be created: ")) + +for i in range(experimentCount): + experimentType = int(input("Enter the experiment type: Enter 1 for Simple and 2 for Factorial:")) + # returning the updated partionconditionpair list: + allExperimentPartitionIDConditionPair = createExperiment.createExperiment(protocol, host, allExperimentPartitionIDConditionPair) + experimentType = "Simple" if experimentType == 1 else "Factorial" + allExperimentPartitionIDConditionPair = createExperiment.createExperiment(protocol, host, allExperimentPartitionIDConditionPair, experimentType) + +### Start enrolling students in the newly created experiment: ### +#Return a new Student +def initStudent(): + studentId = str(uuid.uuid4()) + + # 99% of the time, students are in one class, but 1% of the time they will be in two classes + # schoolCount = random.choices([1, 2], [99, 1])[0] + # if schoolCount == 1: + # numClasses = [random.choices([1, 2], [63, 37])[0]] + # else: + # numClasses = [random.choices([1, 2], [63, 37])[0], random.choices([1, 2], [63, 37])[0]] + schoolIds = getSchools(1) + # classData = getClasses(schoolIds, 1) + students[studentId] = { + "studentId": studentId, + "schools": {} + } + for schoolId in schoolIds: + students[studentId]["schools"][schoolId] = { + "classes": {}, + "instructors": [] + } + # for classObject in classData: + # students[studentId]["schools"][classObject["schoolId"]]["classes"][classObject["classId"]] = { + # "classId": classObject["classId"], + # "instructorId": classObject["instructorId"], + # "classModules": classObject["classModules"] + # } + return students[studentId] +#Return a list of schools, either new or existing +def getSchools(schoolCount): + retSchools = [] + # for i in range(schoolCount): + # # minimum of 10 school, if less always create new school + # if len(schools.keys()) < 10: + # createNew = True + # # maximum of 2140 schools, if reached don't create new school + # elif len(schools.keys()) >= 2140: + # createNew = False + # # if schoolCount is between 10 and 2140, create a new school 50% of the time + # else: + # createNew = random.choices([True, False], [50, 50])[0] + # if createNew: + # schoolId = str(uuid.uuid4()) + # while schoolId in retSchools: + # schoolId = str(uuid.uuid4()) + # instructors = [] + # for i in range(10): + # instructors.append(str(uuid.uuid4())) + # schools[schoolId] = { + # "classes": {}, + # "instructors": instructors + # } + # else: + # schoolId = random.choice(list(schools.keys())) + # while schoolId in retSchools: + # schoolId = random.choice(list(schools.keys())) + # retSchools.append(schoolId) + isGreenvilleWauekgaen = random.randint(1,100) <= 3 + if isGreenvilleWauekgaen: + retSchools.append("greenvilleWaukegaen") + else: + retSchools.append("notGreenvilleWaukegaen") + return retSchools +#Return a list of classes, either new or existing +def getClasses(schoolIds, numClasses): + retClasses = [] + retClassData = [] + for i in range(len(schoolIds)): + for j in range(numClasses[i]): + schoolId = schoolIds[i] + #Each school has at least 5 classes so create a new class if under 5 + if len(schools[schoolId]["classes"].keys()) < 5: + createNew = True + #Each school has a maximum of 50 classes so do not create a new class + elif len(schools[schoolId]["classes"].keys()) >= 50: + createNew = False + else: + #if numClasses is between 5 and 50, create a new class 50% of the time + createNew = random.choices([True, False], [50, 50])[0] + if createNew: + classId = str(uuid.uuid4()) + while classId in retClasses: + classId = str(uuid.uuid4()) + instructorId = random.choice(schools[schoolId]["instructors"]) + classModules = random.sample(list(modules.keys()), k=5) + schools[schoolId]["classes"][classId] = { + "schoolId": schoolId, + "classId": classId, + "instructorId": instructorId, + "classModules": classModules + } + else: + classId = random.choice(list(schools[schoolId]["classes"].keys())) + while classId in retClasses: + classId = random.choice(list(schools[schoolId]["classes"].keys())) + retClasses.append(classId) + retClassData.append(schools[schoolId]["classes"][classId]) + return retClassData +# Main Locust API calls for enrolling students in an experiment: +class UpgradeUserTask(SequentialTaskSet): + # each User represents one Student + def on_start(self): + self.student = initStudent() + print("Student: ", self.student) + #Portal Tasks + ## Portal calls init -> setGroupMembership in reality + # Task 1: portal calls /init + @tag("required", "portal") + @task + def init(self): + url = protocol + f"://{host}/api/init" + print("/init for userid: " + self.student["studentId"]) + data = { + "id": self.student["studentId"], + "group": {}, + "workingGroup": {} + } + with self.client.post(url, json = data, catch_response = True) as response: + if response.status_code != 200: + print(f"Init Failed with {response.status_code} for userid: " + self.student["studentId"]) + # Task 2: portal calls /groupmembership + @tag("portal") + @task + def setGroupMembership(self): + schoolIds = list(self.student["schools"].keys()) + classIds = [] + for schoolId in self.student["schools"].keys(): + classIds.extend(list(self.student["schools"][schoolId]["classes"].keys())) + instructorIds = [] + for schoolId in self.student["schools"].keys(): + for classId in self.student["schools"][schoolId]["classes"].keys(): + instructorIds.append(self.student["schools"][schoolId]["classes"][classId]["instructorId"]) + url = protocol + f"://{host}/api/groupmembership" + print("/groupmembership for userid: " + self.student["studentId"]) + data = { + "id": self.student["studentId"], + "group": { + "schoolId": schoolIds, + "classId": classIds, + "instructorId": instructorIds + } + } + with self.client.post(url, json = data, catch_response = True) as response: + if response.status_code != 200: + print(f"Group membership Failed with {response.status_code} for userid: " + self.student["studentId"]) + # Task 3: portal calls /assign + @tag("portal") + @task + def getAllExperimentConditionsPortal(self): + url = protocol + f"://{host}/api/assign" + print("/assign portal for userid: " + self.student["studentId"]) + data = { + "userId": self.student["studentId"], + "context": "portal" + } + print("user", self.student["studentId"]) + print("assignments: ", data) + with self.client.post(url, json = data, catch_response = True) as response: + if response.status_code != 200: + print(f"/assign Failed with {response.status_code} for userid: " + self.student["studentId"]) + # Launcher tasks + # Task 4: launcher calls /workinggroup + @tag("launcher") + @task + def setWorkingGroup(self): + workingSchoolId = random.choice(list(self.student["schools"].keys())) + # workingClassId = random.choice(list(self.student["schools"][workingSchoolId]["classes"].keys())) + # workingInstructorId = self.student["schools"][workingSchoolId]["classes"][workingClassId]["instructorId"] + url = protocol + f"://{host}/api/v1/workinggroup" + print("/workinggroup for userid: " + self.student["studentId"]) + data = { + "id": self.student["studentId"], + "workingGroup": { + "schoolId": workingSchoolId, + # "classId": workingClassId, + # "instructorId": workingInstructorId + } + } + with self.client.patch(url, json = data, catch_response = True) as response: + if response.status_code != 200: + print(f"setWorkingGroup Failed with {response.status_code} for userid: " + self.student["studentId"]) + # Task 5: launcher calls /useraliases + @tag("launcher") + @task + def setAltIds(self): + workingSchoolId = random.choice(list(self.student["schools"].keys())) + # workingClassId = random.choice(list(self.student["schools"][workingSchoolId]["classes"].keys())) + # classModules = self.student["schools"][workingSchoolId]["classes"][workingClassId]["classModules"] + url = protocol + f"://{host}/api/v1/useraliases" + print("/useraliases for userid: " + self.student["studentId"]) + data = { + "userId": self.student["studentId"], + "aliases": [str(uuid.uuid4()), str(uuid.uuid4()), str(uuid.uuid4())] + } + with self.client.patch(url, json = data, catch_response = True) as response: + if response.status_code != 200: + print(f"/useraliases Failed with {response.status_code} for userid: " + self.student["studentId"]) + #Assignment Progress Service + #Skipping getExperimentCondition() - Assume getAllExperimentConditionsAssignProg() has been called, so getExperimentCondition() does not hit API + # Task 6: workspace calls /assign or uses cached data + @tag("assign-prog") + @task + def getAllExperimentConditionsAssignProg(self): + url = protocol + f"://{host}/api/v1/assign" + print("/assign assign-prog for userid: " + self.student["studentId"]) + data = { + "userId": self.student["studentId"], + "context": "assign-prog" + } + with self.client.post(url, json = data, catch_response = True) as response: + if response.status_code != 200: + print(f"getAllExperimentConditions in assign-prog Failed with {response.status_code}") + # mark is called after finishing a workspace. In reality, mark is called 15-30 mins after assign + # Task 7: Student count gets incremented here on marking complete + @tag("assign-prog") + @task + def markExperimentPoint(self): + url = protocol + f"://{host}/api/v1/mark" + print("/mark for userid: " + self.student["studentId"]) + + # if(len(allExperimentPartitionIDConditionPair) == 0): + # print("No assigned conditions found") + # return + # else: + # print("allExperimentPartitionIDConditionPair: ", allExperimentPartitionIDConditionPair) + # pick a random pair of PartitionIdConditionId from allExperimentPartitionIDConditionPair + markPartitionIDConditionPair = random.choice(allExperimentPartitionIDConditionPair) + + if "greenvilleWaukegaen" in self.student["schools"]: + condition = markPartitionIDConditionPair['condition'] if "greenvilleWaukegaen" in self.student["schools"] else None + site = markPartitionIDConditionPair['site'] + target = markPartitionIDConditionPair['target'] + else: + condition_choices = ["control", "variant", None] + weights = [0.02, 0.02, 0.96] + + condition = random.choices(condition_choices, weights, k=1)[0] + site = "SelectSection" + target = random.choices(["analyzing_models_2step_integers", "analyzing_models_2step_rationals"]) + + data = { + "userId": self.student["studentId"], + "site": site, + "target": target, + "condition": condition, + } + + print("user schoolId", self.student["schools"]) + print("mark data: ", data) + + with self.client.post(url, json = data, catch_response = True) as response: + if response.status_code != 200: + print(f"/mark Failed with {response.status_code} for userid: " + self.student["studentId"]) + # Task 8: failed experiment point + # @tag("assign-prog") + # @task + # def failedExperimentPoint(self): + # url = protocol + f"://{host}/api/failed" + # print("/failed for userid: " + self.student["studentId"]) + + # if(len(allExperimentPartitionIDConditionPair) == 0): + # print("No assigned conditions found") + # return + # else: + # print("allExperimentPartitionIDConditionPair: ", allExperimentPartitionIDConditionPair) + # # pick a random pair of PartitionIdConditionId from allExperimentPartitionIDConditionPair + # markPartitionIDConditionPair = random.choice(allExperimentPartitionIDConditionPair) + + # data = { + # "userId": self.student["studentId"], + # "experimentPoint": markPartitionIDConditionPair['experimentPoint'], + # "partitionId": markPartitionIDConditionPair['partitionId'], + # # "experimentPoint": markPartitionIDConditionPair['site'], + # # "partitionId": markPartitionIDConditionPair['target'], + # "condition": markPartitionIDConditionPair['condition'] + # } + + # # pick a random assigned workspace - requires /assign response to be saved + # # markPartitionIDConditionPair = random.choice(self.assignedWorkspaces) + # # data = { + # # "reason": "locust tests", + # # "experimentPoint": markPartitionIDConditionPair['expPoint'], + # # "userId": self.student["studentId"], + # # "experimentId": markPartitionIDConditionPair['expId'] + # # } + # with self.client.post(url, json = data, catch_response = True) as response: + # if response.status_code != 200: + # print(f"/failed Failed with {response.status_code} for userid: " + self.student["studentId"]) + # Generate mock log data + def genMockLog(self): + attributes = { + "totalTimeSeconds": random.randint(1,500000), + "totalMasteryWorkspacesCompleted": random.randint(1,50), + "totalConceptBuildersCompleted": random.randint(1,50), + "totalMasteryWorkspacesGraduated": random.randint(1,50), + "totalSessions": random.randint(1,100), + "totalProblemsCompleted": random.randint(1,1000) + } + groupedMetrics = { + "groupClass": "workspace", + "groupKey": random.choice(list(workspaces.keys())), + "groupUniquifier": str(datetime.now()), + "attributes": { + "timeSeconds": random.randint(1,500), + "hintCount": random.randint(1,10), + "errorCount": random.randint(1,20), + "completionCount": 1, + "workspaceCompletionStatus": "GRADUATED", + "problemsCompleted": random.randint(1,10) + } + } + return { + "userId": self.student["studentId"], + "timestamp": str(datetime.now()), + "metrics": { + "attributes": attributes, + "groupedMetrics": [groupedMetrics] + } + } + #UpgradeForwarder + # Task 9: + @tag("logger") + @task + def logEvent(self): + url = protocol + f"://{host}/api/log" + data = { + "userId": self.student["studentId"], + "value": [ + self.genMockLog() + ] + } + with self.client.post(url, json = data, catch_response = True) as response: + if response.status_code != 200: + print(f"LogEvent Failed with {response.status_code}") +class UpgradeUser(HttpUser): + wait_time = between(0.1, 10) + host = "https://upgradeapi.qa-cli.com" + tasks = [UpgradeUserTask] \ No newline at end of file diff --git a/backend/locust/load_test_upgrade_mathia.py b/backend/locust/load_test_upgrade_mathia.py index 4b95a5dc25..5ac9eefd22 100644 --- a/backend/locust/load_test_upgrade_mathia.py +++ b/backend/locust/load_test_upgrade_mathia.py @@ -8,8 +8,22 @@ schools = {} students = {} - allExperimentPartitionIDConditionPair = [] +# For load testing on existing prod experiments: +# allExperimentPartitionIDConditionPair = [ +# {"experimentPoint": "SelectAdaptive", "partitionId" : "1_0gzegepj", "condition" : "human-robert-control"}, +# {"experimentPoint": "SelectAdaptive", "partitionId" : "1_ffi961t9", "condition" : "human-robert-control"}, +# {"experimentPoint": "SelectAdaptive", "partitionId" : "1_m7qve72i", "condition" : "joke-ai-robert-experimental"}, +# {"experimentPoint": "SelectAdaptive", "partitionId" : "1_2ee7ws2b", "condition" : "joke-ai-robert-experimental"}, +# {"experimentPoint": "SelectAdaptive", "partitionId" : "1_i7i7heqa", "condition" : "control-playback-speed"}, +# {"experimentPoint": "SelectAdaptive", "partitionId" : "1_yspx09fg", "condition" : "control-playback-speed"}, +# {"experimentPoint": "SelectAdaptive", "partitionId" : "1_fwk036l9", "condition" : "control-playback-speed"}, +# {"experimentPoint": "DisplayQuestion", "partitionId" : "AR-VST-0150-6.NS.8-1_SP_014", "condition" : "learnosity-item-control"}, +# {"experimentPoint": "DisplayQuestion", "partitionId" : "AR-VST-0150-6.NS.8-1_SP_008", "condition" : "learnosity-item-control"}, +# {"experimentPoint": "DisplayQuestion", "partitionId" : "AR-VST-0150-6.NS.8-1_SP_004", "condition" : "learnosity-item-control"}, +# {"experimentPoint": "SelectStream", "partitionId" : "HintUI", "condition" : "question-hint-tutorbot"}, +# {"experimentPoint": "SelectStream", "partitionId" : "rewindButton", "condition" : "enable-rewind-button"} +# ] # Setting host URL's: protocol = "http" @@ -48,30 +62,24 @@ def initStudent(): numClasses = [random.choices([1, 2], [63, 37])[0]] else: numClasses = [random.choices([1, 2], [63, 37])[0], random.choices([1, 2], [63, 37])[0]] - schoolIds = getSchools(schoolCount) classData = getClasses(schoolIds, numClasses) - students[studentId] = { "studentId": studentId, "schools": {} } - for schoolId in schoolIds: students[studentId]["schools"][schoolId] = { "classes": {}, "instructors": [] } - for classObject in classData: students[studentId]["schools"][classObject["schoolId"]]["classes"][classObject["classId"]] = { "classId": classObject["classId"], "instructorId": classObject["instructorId"], "classModules": classObject["classModules"] } - return students[studentId] - #Return a list of schools, either new or existing def getSchools(schoolCount): retSchools = [] @@ -85,30 +93,23 @@ def getSchools(schoolCount): # if schoolCount is between 10 and 2140, create a new school 50% of the time else: createNew = random.choices([True, False], [50, 50])[0] - if createNew: schoolId = str(uuid.uuid4()) while schoolId in retSchools: schoolId = str(uuid.uuid4()) - instructors = [] for i in range(10): instructors.append(str(uuid.uuid4())) - schools[schoolId] = { "classes": {}, "instructors": instructors } - else: schoolId = random.choice(list(schools.keys())) while schoolId in retSchools: schoolId = random.choice(list(schools.keys())) - retSchools.append(schoolId) - return retSchools - #Return a list of classes, either new or existing def getClasses(schoolIds, numClasses): retClasses = [] @@ -125,12 +126,10 @@ def getClasses(schoolIds, numClasses): else: #if numClasses is between 5 and 50, create a new class 50% of the time createNew = random.choices([True, False], [50, 50])[0] - if createNew: classId = str(uuid.uuid4()) while classId in retClasses: classId = str(uuid.uuid4()) - instructorId = random.choice(schools[schoolId]["instructors"]) classModules = random.sample(list(modules.keys()), k=5) schools[schoolId]["classes"][classId] = { @@ -139,27 +138,20 @@ def getClasses(schoolIds, numClasses): "instructorId": instructorId, "classModules": classModules } - else: classId = random.choice(list(schools[schoolId]["classes"].keys())) while classId in retClasses: classId = random.choice(list(schools[schoolId]["classes"].keys())) - retClasses.append(classId) retClassData.append(schools[schoolId]["classes"][classId]) - return retClassData - # Main Locust API calls for enrolling students in an experiment: class UpgradeUserTask(SequentialTaskSet): - # each User represents one Student def on_start(self): self.student = initStudent() - #Portal Tasks ## Portal calls init -> setGroupMembership in reality - # Task 1: portal calls /init @tag("required", "portal") @task @@ -171,26 +163,21 @@ def init(self): "group": {}, "workingGroup": {} } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"Init Failed with {response.status_code} for userid: " + self.student["studentId"]) - # Task 2: portal calls /groupmembership @tag("portal") @task def setGroupMembership(self): schoolIds = list(self.student["schools"].keys()) - classIds = [] for schoolId in self.student["schools"].keys(): classIds.extend(list(self.student["schools"][schoolId]["classes"].keys())) - instructorIds = [] for schoolId in self.student["schools"].keys(): for classId in self.student["schools"][schoolId]["classes"].keys(): instructorIds.append(self.student["schools"][schoolId]["classes"][classId]["instructorId"]) - url = protocol + f"://{host}/api/groupmembership" print("/groupmembership for userid: " + self.student["studentId"]) data = { @@ -201,12 +188,9 @@ def setGroupMembership(self): "instructorId": instructorIds } } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"Group membership Failed with {response.status_code} for userid: " + self.student["studentId"]) - - # Task 3: portal calls /assign @tag("portal") @task @@ -217,12 +201,9 @@ def getAllExperimentConditionsPortal(self): "userId": self.student["studentId"], "context": "portal" } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"/assign Failed with {response.status_code} for userid: " + self.student["studentId"]) - - # Launcher tasks # Task 4: launcher calls /workinggroup @tag("launcher") @@ -241,11 +222,9 @@ def setWorkingGroup(self): "instructorId": workingInstructorId } } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"setWorkingGroup Failed with {response.status_code} for userid: " + self.student["studentId"]) - # Task 5: launcher calls /useraliases @tag("launcher") @task @@ -253,38 +232,30 @@ def setAltIds(self): workingSchoolId = random.choice(list(self.student["schools"].keys())) workingClassId = random.choice(list(self.student["schools"][workingSchoolId]["classes"].keys())) classModules = self.student["schools"][workingSchoolId]["classes"][workingClassId]["classModules"] - url = protocol + f"://{host}/api/useraliases" print("/useraliases for userid: " + self.student["studentId"]) data = { "userId": self.student["studentId"], "aliases": [self.student["studentId"] + m for m in classModules] } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"/useraliases Failed with {response.status_code} for userid: " + self.student["studentId"]) - - #Assignment Progress Service #Skipping getExperimentCondition() - Assume getAllExperimentConditionsAssignProg() has been called, so getExperimentCondition() does not hit API - # Task 6: workspace calls /assign or uses cached data @tag("assign-prog") @task def getAllExperimentConditionsAssignProg(self): url = protocol + f"://{host}/api/assign" print("/assign assign-prog for userid: " + self.student["studentId"]) - data = { "userId": self.student["studentId"], - "context": "assign-prog" + "context": "mathstream" } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"getAllExperimentConditions in assign-prog Failed with {response.status_code}") - # mark is called after finishing a workspace. In reality, mark is called 15-30 mins after assign # Task 7: Student count gets incremented here on marking complete @tag("assign-prog") @@ -308,18 +279,15 @@ def markExperimentPoint(self): # pick a random assigned workspace - requires /assign response to be saved # markPartitionIDConditionPair = random.choice(self.assignedWorkspaces) - # data = { # "userId": self.student["studentId"], # "experimentPoint": markPartitionIDConditionPair['expPoint'], # "partitionId": markPartitionIDConditionPair['expId'], # "condition": markPartitionIDConditionPair['assignedCondition']['conditionCode'] # } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"/mark Failed with {response.status_code} for userid: " + self.student["studentId"]) - # Task 8: failed experiment point @tag("assign-prog") @task @@ -343,18 +311,15 @@ def failedExperimentPoint(self): # pick a random assigned workspace - requires /assign response to be saved # markPartitionIDConditionPair = random.choice(self.assignedWorkspaces) - # data = { # "reason": "locust tests", # "experimentPoint": markPartitionIDConditionPair['expPoint'], # "userId": self.student["studentId"], # "experimentId": markPartitionIDConditionPair['expId'] # } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"/failed Failed with {response.status_code} for userid: " + self.student["studentId"]) - # Generate mock log data def genMockLog(self): attributes = { @@ -386,7 +351,6 @@ def genMockLog(self): "groupedMetrics": [groupedMetrics] } } - #UpgradeForwarder # Task 9: @tag("logger") @@ -399,12 +363,9 @@ def logEvent(self): self.genMockLog() ] } - with self.client.post(url, json = data, catch_response = True) as response: if response.status_code != 200: print(f"LogEvent Failed with {response.status_code}") - - class UpgradeUser(HttpUser): wait_time = between(0.1, 10) host = "localhost:3030" diff --git a/backend/packages/Upgrade/.env.docker.local b/backend/packages/Upgrade/.env.docker.local index 0d7d553229..0086e8c78a 100644 --- a/backend/packages/Upgrade/.env.docker.local +++ b/backend/packages/Upgrade/.env.docker.local @@ -9,6 +9,7 @@ APP_ROUTE_PREFIX=/api APP_BANNER=true APP_DEMO=false CACHING_ENABLED=true +CACHING_TTL=10 # # LOGGING diff --git a/backend/packages/Upgrade/.env.example b/backend/packages/Upgrade/.env.example index 84cfe77639..4d0c408d62 100644 --- a/backend/packages/Upgrade/.env.example +++ b/backend/packages/Upgrade/.env.example @@ -8,7 +8,8 @@ APP_PORT=3030 APP_ROUTE_PREFIX=/api APP_BANNER=true APP_DEMO=false - +CACHING_ENABLED=true +CACHING_TTL=10 # # LOGGING # diff --git a/backend/packages/Upgrade/.env.test b/backend/packages/Upgrade/.env.test index 3658d9c6b5..12819bc03d 100644 --- a/backend/packages/Upgrade/.env.test +++ b/backend/packages/Upgrade/.env.test @@ -8,6 +8,8 @@ APP_PORT=3030 APP_ROUTE_PREFIX=/api APP_BANNER=true APP_DEMO=false +CACHING_ENABLED=true +CACHING_TTL=10 # # LOGGING diff --git a/backend/packages/Upgrade/package-lock.json b/backend/packages/Upgrade/package-lock.json index 7e4f3bb119..f528fab7ea 100644 --- a/backend/packages/Upgrade/package-lock.json +++ b/backend/packages/Upgrade/package-lock.json @@ -1,12 +1,12 @@ { "name": "ab_testing_backend", - "version": "5.0.1", + "version": "5.0.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ab_testing_backend", - "version": "5.0.1", + "version": "5.0.11", "license": "ISC", "dependencies": { "@golevelup/ts-jest": "^0.3.2", @@ -1786,9 +1786,9 @@ } }, "node_modules/@types/cache-manager": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.1.tgz", - "integrity": "sha512-w4Gm7qg4ohvk0k4CLhOoqnMohWEyeyAOTovPgkguhuDCfVEV1wN/HWEd1XzB1S9/NV9pUcZcc498qU4E15ck6A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.4.tgz", + "integrity": "sha512-Kyk9uF54w5/JQWLDKr5378euWUPvebknZut6UpsKhO3R7vE5a5o71QxTR2uev1niBgVAoXAR+BCNMU1lipjxWQ==", "dev": true }, "node_modules/@types/compression": { @@ -17524,9 +17524,9 @@ } }, "@types/cache-manager": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.1.tgz", - "integrity": "sha512-w4Gm7qg4ohvk0k4CLhOoqnMohWEyeyAOTovPgkguhuDCfVEV1wN/HWEd1XzB1S9/NV9pUcZcc498qU4E15ck6A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.4.tgz", + "integrity": "sha512-Kyk9uF54w5/JQWLDKr5378euWUPvebknZut6UpsKhO3R7vE5a5o71QxTR2uev1niBgVAoXAR+BCNMU1lipjxWQ==", "dev": true }, "@types/compression": { diff --git a/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts b/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts index 5df5c76da4..53d4da6f7b 100644 --- a/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/ExperimentRepository.ts @@ -37,11 +37,21 @@ export class ExperimentRepository extends Repository { const [experimentData, experimentSegmentData] = await Promise.all([ experiment.getMany().catch((errorMsg: any) => { - const errorMsgString = repositoryError('ExperimentRepository', 'find', {}, errorMsg); + const errorMsgString = repositoryError( + 'ExperimentRepository', + 'findAllExperiments-experimentData', + {}, + errorMsg + ); throw errorMsgString; }), experimentSegment.getMany().catch((errorMsg: any) => { - const errorMsgString = repositoryError('ExperimentRepository', 'find', {}, errorMsg); + const errorMsgString = repositoryError( + 'ExperimentRepository', + 'findAllExperiments-experimentSegmentData', + {}, + errorMsg + ); throw errorMsgString; }), ]); @@ -62,25 +72,36 @@ export class ExperimentRepository extends Repository { .select(['experiment.id', 'experiment.name']) .getMany() .catch((errorMsg: any) => { - const errorMsgString = repositoryError('ExperimentRepository', 'find', {}, errorMsg); + const errorMsgString = repositoryError('ExperimentRepository', 'findAllName', {}, errorMsg); throw errorMsgString; }); } public async getValidExperiments(context: string): Promise { - const experiment = this.createQueryBuilder('experiment') + const experimentConditionLevelPayloadQuery = this.createQueryBuilder('experiment') .leftJoinAndSelect('experiment.conditions', 'conditions') + .leftJoinAndSelect('conditions.levelCombinationElements', 'levelCombinationElements') + .leftJoinAndSelect('levelCombinationElements.level', 'level') + .leftJoinAndSelect('conditions.conditionPayloads', 'conditionPayload') + .where( + new Brackets((qb) => { + qb.where( + '(experiment.state = :enrolling OR experiment.state = :enrollmentComplete) AND :context ILIKE ANY (ARRAY[experiment.context])', + { + enrolling: 'enrolling', + enrollmentComplete: 'enrollmentComplete', + context, + } + ); + }) + ); + + const experimentFactorPartitionLevelPayloadQuery = this.createQueryBuilder('experiment') .leftJoinAndSelect('experiment.partitions', 'partitions') - .leftJoinAndSelect('experiment.queries', 'queries') - .leftJoinAndSelect('experiment.stateTimeLogs', 'stateTimeLogs') - .leftJoinAndSelect('experiment.factors', 'factors') - .leftJoinAndSelect('factors.levels', 'levels') - .leftJoinAndSelect('queries.metric', 'metric') .leftJoinAndSelect('partitions.conditionPayloads', 'conditionPayloads') .leftJoinAndSelect('conditionPayloads.parentCondition', 'parentCondition') - .leftJoinAndSelect('conditions.levelCombinationElements', 'levelCombinationElements') - .leftJoinAndSelect('conditions.conditionPayloads', 'conditionPayload') - .leftJoinAndSelect('levelCombinationElements.level', 'level') + .leftJoinAndSelect('experiment.factors', 'factors') + .leftJoinAndSelect('factors.levels', 'levels') .where( new Brackets((qb) => { qb.where( @@ -94,7 +115,7 @@ export class ExperimentRepository extends Repository { }) ); - const experimentSegment = this.createQueryBuilder('experiment') + const experimentSegmentQuery = this.createQueryBuilder('experiment') // making small queries .select('experiment.id') .leftJoinAndSelect('experiment.experimentSegmentInclusion', 'experimentSegmentInclusion') @@ -120,16 +141,41 @@ export class ExperimentRepository extends Repository { }) ); - const [experimentData, experimentSegmentData] = await Promise.all([ - experiment.getMany().catch((errorMsg: any) => { - const errorMsgString = repositoryError('ExperimentRepository', 'find', {}, errorMsg); - throw errorMsgString; - }), - experimentSegment.getMany().catch((errorMsg: any) => { - const errorMsgString = repositoryError('ExperimentRepository', 'find', {}, errorMsg); - throw errorMsgString; - }), - ]); + const [experimentConditionLevelPayloadData, experimentFactorPartitionLevelPayloadData, experimentSegmentData] = + await Promise.all([ + experimentConditionLevelPayloadQuery.getMany().catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'ExperimentRepository', + 'getValidExperiments-experimentConditionLevelPayloadQuery', + {}, + errorMsg + ); + throw errorMsgString; + }), + experimentFactorPartitionLevelPayloadQuery.getMany().catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'ExperimentRepository', + 'getValidExperiments-experimentFactorPartitionLevelPayloadQuery', + {}, + errorMsg + ); + throw errorMsgString; + }), + experimentSegmentQuery.getMany().catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'ExperimentRepository', + 'getValidExperiments-experimentSegmentQuery', + {}, + errorMsg + ); + throw errorMsgString; + }), + ]); + + const experimentData = experimentConditionLevelPayloadData.map((data) => { + const data2 = experimentFactorPartitionLevelPayloadData.find((i) => i.id === data.id); + return { ...data, ...data2 }; + }); const mergedData = experimentData.map((data) => { const { id } = data; @@ -143,19 +189,31 @@ export class ExperimentRepository extends Repository { } public async getValidExperimentsWithPreview(context: string): Promise { - const experiment = this.createQueryBuilder('experiment') + const experimentConditionLevelPayloadQuery = this.createQueryBuilder('experiment') .leftJoinAndSelect('experiment.conditions', 'conditions') + .leftJoinAndSelect('conditions.levelCombinationElements', 'levelCombinationElements') + .leftJoinAndSelect('levelCombinationElements.level', 'level') + .leftJoinAndSelect('conditions.conditionPayloads', 'conditionPayload') + .where( + new Brackets((qb) => { + qb.where( + '(experiment.state = :enrolling OR experiment.state = :enrollmentComplete OR experiment.state = :preview) AND :context ILIKE ANY (ARRAY[experiment.context])', + { + enrolling: 'enrolling', + enrollmentComplete: 'enrollmentComplete', + preview: 'preview', + context, + } + ); + }) + ); + + const experimentFactorPartitionLevelPayloadQuery = this.createQueryBuilder('experiment') .leftJoinAndSelect('experiment.partitions', 'partitions') - .leftJoinAndSelect('experiment.queries', 'queries') - .leftJoinAndSelect('experiment.stateTimeLogs', 'stateTimeLogs') - .leftJoinAndSelect('experiment.factors', 'factors') - .leftJoinAndSelect('factors.levels', 'levels') - .leftJoinAndSelect('queries.metric', 'metric') .leftJoinAndSelect('partitions.conditionPayloads', 'conditionPayloads') .leftJoinAndSelect('conditionPayloads.parentCondition', 'parentCondition') - .leftJoinAndSelect('conditions.levelCombinationElements', 'levelCombinationElements') - .leftJoinAndSelect('conditions.conditionPayloads', 'conditionPayload') - .leftJoinAndSelect('levelCombinationElements.level', 'level') + .leftJoinAndSelect('experiment.factors', 'factors') + .leftJoinAndSelect('factors.levels', 'levels') .where( new Brackets((qb) => { qb.where( @@ -170,7 +228,8 @@ export class ExperimentRepository extends Repository { }) ); - const experimentSegment = this.createQueryBuilder('experiment') + const experimentSegmentQuery = this.createQueryBuilder('experiment') + // making small queries .select('experiment.id') .leftJoinAndSelect('experiment.experimentSegmentInclusion', 'experimentSegmentInclusion') .leftJoinAndSelect('experimentSegmentInclusion.segment', 'segmentInclusion') @@ -196,16 +255,41 @@ export class ExperimentRepository extends Repository { }) ); - const [experimentData, experimentSegmentData] = await Promise.all([ - experiment.getMany().catch((errorMsg: any) => { - const errorMsgString = repositoryError('ExperimentRepository', 'find', {}, errorMsg); - throw errorMsgString; - }), - experimentSegment.getMany().catch((errorMsg: any) => { - const errorMsgString = repositoryError('ExperimentRepository', 'find', {}, errorMsg); - throw errorMsgString; - }), - ]); + const [experimentConditionLevelPayloadData, experimentFactorPartitionLevelPayloadData, experimentSegmentData] = + await Promise.all([ + experimentConditionLevelPayloadQuery.getMany().catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'ExperimentRepository', + 'getValidExperiments-experimentConditionLevelPayloadQuery', + {}, + errorMsg + ); + throw errorMsgString; + }), + experimentFactorPartitionLevelPayloadQuery.getMany().catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'ExperimentRepository', + 'getValidExperiments-experimentFactorPartitionLevelPayloadQuery', + {}, + errorMsg + ); + throw errorMsgString; + }), + experimentSegmentQuery.getMany().catch((errorMsg: any) => { + const errorMsgString = repositoryError( + 'ExperimentRepository', + 'getValidExperiments-experimentSegmentQuery', + {}, + errorMsg + ); + throw errorMsgString; + }), + ]); + + const experimentData = experimentConditionLevelPayloadData.map((data) => { + const data2 = experimentFactorPartitionLevelPayloadData.find((i) => i.id === data.id); + return { ...data, ...data2 }; + }); const mergedData = experimentData.map((data) => { const { id } = data; diff --git a/backend/packages/Upgrade/src/api/services/CacheService.ts b/backend/packages/Upgrade/src/api/services/CacheService.ts index 7448a83cb7..d46999ad7e 100644 --- a/backend/packages/Upgrade/src/api/services/CacheService.ts +++ b/backend/packages/Upgrade/src/api/services/CacheService.ts @@ -1,10 +1,13 @@ import { env } from './../../env'; import { Service } from 'typedi'; import cacheManager from 'cache-manager'; +import { CACHE_PREFIX } from 'upgrade_types'; @Service() export class CacheService { private memoryCache: cacheManager.Cache; + private ttl = env.caching.ttl || 900; + constructor() { // read from the environment variable for initializing caching let store: 'memory' | 'none'; @@ -13,29 +16,37 @@ export class CacheService { } else { store = 'none'; } - this.memoryCache = cacheManager.caching({ store, max: 100, ttl: 900 }); + + this.memoryCache = cacheManager.caching({ store, max: 100, ttl: this.ttl }); } public setCache(id: string, value: T): Promise { - return this.memoryCache.set(id, value); + return this.memoryCache ? this.memoryCache.set(id, value) : Promise.resolve(null); } public getCache(id: string): Promise { - return this.memoryCache.get(id); + return this.memoryCache ? this.memoryCache.get(id) : Promise.resolve(null); + } + + public delCache(id: string): Promise { + return this.memoryCache ? this.memoryCache.del(id) : Promise.resolve(); } - public delCache(id: string): Promise { - return this.memoryCache.del(id); + public async resetPrefixCache(prefix: string): Promise { + const keys = this.memoryCache ? await this.memoryCache.store.keys() : []; + const filteredKeys = keys.filter((str) => str.startsWith(prefix)); + this.memoryCache.store.del(filteredKeys); } - public resetCache(): void { - this.memoryCache.reset(); + // Use this to wrap the function that you want to cache + public wrap(key: string, fn: () => Promise): Promise { + return this.memoryCache ? this.memoryCache.wrap(key, fn) : fn(); } - public async wrapFunction(keys: string[], functionToCall: () => Promise): Promise { + public async wrapFunction(prefix: CACHE_PREFIX, keys: string[], functionToCall: () => Promise): Promise { const cachedData = await Promise.all( keys.map(async (key) => { - return this.getCache(key); + return this.getCache(prefix + key); }) ); @@ -48,7 +59,7 @@ export class CacheService { await Promise.all( keys.map((key, index) => { - return this.setCache(key, data[index]); + return this.setCache(prefix + key, data[index]); }) ); return data; diff --git a/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts b/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts index 8da064a7ce..97c06c21d6 100644 --- a/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts +++ b/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts @@ -203,7 +203,7 @@ export class ExperimentAssignmentService { if (previewUser) { experiments = await this.experimentRepository.getValidExperimentsWithPreview(context); } else { - experiments = await this.experimentRepository.getValidExperiments(context); + experiments = await this.experimentService.getCachedValidExperiments(context); } // Experiment has assignment type as GROUP_ASSIGNMENT @@ -458,19 +458,19 @@ export class ExperimentAssignmentService { // query all experiment and sub experiment // check if user or group is excluded - let experiments: Experiment[] = [], - userExcluded: string, - groupExcluded: string[]; + let experiments: Experiment[] = []; + + const [userExcluded, groupExcluded] = await this.checkUserOrGroupIsGloballyExcluded(experimentUser); + + if (userExcluded || groupExcluded.length > 0) { + // return null if the user or group is excluded from the experiment + return []; + } + if (previewUser) { - [experiments, [userExcluded, groupExcluded]] = await Promise.all([ - this.experimentRepository.getValidExperimentsWithPreview(context), - this.checkUserOrGroupIsGloballyExcluded(experimentUser), - ]); + experiments = await this.experimentRepository.getValidExperimentsWithPreview(context); } else { - [experiments, [userExcluded, groupExcluded]] = await Promise.all([ - this.experimentRepository.getValidExperiments(context), - this.checkUserOrGroupIsGloballyExcluded(experimentUser), - ]); + experiments = await this.experimentService.getCachedValidExperiments(context); } experiments = experiments.map((exp) => this.experimentService.formatingConditionPayload(exp)); @@ -518,11 +518,6 @@ export class ExperimentAssignmentService { return []; } - if (userExcluded || groupExcluded.length > 0) { - // return null if the user or group is excluded from the experiment - return []; - } - const globalFilteredExperiments: Experiment[] = [...experiments]; const experimentIds = globalFilteredExperiments.map((experiment) => experiment.id); diff --git a/backend/packages/Upgrade/src/api/services/ExperimentService.ts b/backend/packages/Upgrade/src/api/services/ExperimentService.ts index 95c6fbaebe..24732477dd 100644 --- a/backend/packages/Upgrade/src/api/services/ExperimentService.ts +++ b/backend/packages/Upgrade/src/api/services/ExperimentService.ts @@ -27,6 +27,7 @@ import { EXCLUSION_CODE, SEGMENT_TYPE, EXPERIMENT_TYPE, + CACHE_PREFIX, } from 'upgrade_types'; import { IndividualExclusionRepository } from '../repositories/IndividualExclusionRepository'; import { GroupExclusionRepository } from '../repositories/GroupExclusionRepository'; @@ -62,10 +63,17 @@ import { Level } from '../models/Level'; import { LevelRepository } from '../repositories/LevelRepository'; import { LevelCombinationElement } from '../models/LevelCombinationElement'; import { LevelCombinationElementRepository } from '../repositories/LevelCombinationElements'; -import { ConditionValidator, ExperimentDTO, FactorValidator, PartitionValidator, ParticipantsValidator } from '../DTO/ExperimentDTO'; +import { + ConditionValidator, + ExperimentDTO, + FactorValidator, + PartitionValidator, + ParticipantsValidator, +} from '../DTO/ExperimentDTO'; import { ConditionPayloadDTO } from '../DTO/ConditionPayloadDTO'; import { FactorDTO } from '../DTO/FactorDTO'; import { LevelDTO } from '../DTO/LevelDTO'; +import { CacheService } from './CacheService'; @Service() export class ExperimentService { @@ -90,7 +98,8 @@ export class ExperimentService { public previewUserService: PreviewUserService, public segmentService: SegmentService, public scheduledJobService: ScheduledJobService, - public errorService: ErrorService + public errorService: ErrorService, + public cacheService: CacheService ) {} public async find(logger?: UpgradeLogger): Promise { @@ -227,6 +236,15 @@ export class ExperimentService { }; } + public async getCachedValidExperiments(context: string) { + const cacheKey = CACHE_PREFIX.EXPERIMENT_KEY_PREFIX + context; + return this.cacheService + .wrap(cacheKey, this.experimentRepository.getValidExperiments.bind(this.experimentRepository, context)) + .then((validExperiment) => { + return JSON.parse(JSON.stringify(validExperiment)); + }); + } + public create( experiment: ExperimentDTO, currentUser: User, @@ -234,6 +252,7 @@ export class ExperimentService { createType?: string ): Promise { logger.info({ message: 'Create a new experiment =>', details: experiment }); + this.cacheService.delCache(CACHE_PREFIX.EXPERIMENT_KEY_PREFIX + experiment.context[0]); // order for condition let newConditionId; @@ -265,12 +284,15 @@ export class ExperimentService { } public createMultipleExperiments( - experiment: ExperimentDTO[], + experiments: ExperimentDTO[], user: User, logger: UpgradeLogger ): Promise { - logger.info({ message: `Generating test experiments`, details: experiment }); - return this.addBulkExperiments(experiment, user, logger); + logger.info({ message: `Generating test experiments`, details: experiments }); + experiments.forEach((experiment) => { + this.cacheService.delCache(CACHE_PREFIX.EXPERIMENT_KEY_PREFIX + experiment.context[0]); + }); + return this.addBulkExperiments(experiments, user, logger); } public async delete( @@ -283,6 +305,7 @@ export class ExperimentService { } return getConnection().transaction(async (transactionalEntityManager) => { const experiment = await this.findOne(experimentId, logger); + await this.cacheService.delCache(CACHE_PREFIX.EXPERIMENT_KEY_PREFIX + experiment.context[0]); if (experiment) { const deletedExperiment = await this.experimentRepository.deleteById(experimentId, transactionalEntityManager); @@ -335,6 +358,7 @@ export class ExperimentService { if (logger) { logger.info({ message: `Update the experiment`, details: experiment }); } + this.cacheService.delCache(CACHE_PREFIX.EXPERIMENT_KEY_PREFIX + experiment.context[0]); return this.updateExperimentInDB(experiment as ExperimentDTO, currentUser, logger); } @@ -380,6 +404,7 @@ export class ExperimentService { { id: experimentId }, { relations: ['stateTimeLogs'] } ); + this.cacheService.delCache(CACHE_PREFIX.EXPERIMENT_KEY_PREFIX + oldExperiment.context[0]); if ( (state === EXPERIMENT_STATE.ENROLLING || state === EXPERIMENT_STATE.PREVIEW) && @@ -438,6 +463,7 @@ export class ExperimentService { logger: UpgradeLogger ): Promise { for (const experiment of experiments) { + this.cacheService.delCache(CACHE_PREFIX.EXPERIMENT_KEY_PREFIX + experiment.context[0]); const duplicateExperiment = await this.experimentRepository.findOne(experiment.id); if (duplicateExperiment && experiment.id) { const error = new Error('Duplicate experiment'); @@ -757,7 +783,7 @@ export class ExperimentService { this.checkConditionCodeDefault(conditions); // creating condition docs - const conditionDocToSave: Array>> = + const conditionDocToSave: Array>> = (conditions && conditions.length > 0 && conditions.map((condition: ConditionValidator) => { @@ -796,7 +822,7 @@ export class ExperimentService { }) ); decisionPoint.id = decisionPoint.id || uuid(); - return { ...decisionPoint, experiment: experimentDoc } + return { ...decisionPoint, experiment: experimentDoc }; })) || []; @@ -1517,7 +1543,7 @@ export class ExperimentService { return { ...experiment, factors: updatedFactors, conditionPayloads: updatedConditionPayloads }; } - private includeExcludeSegmentCreation ( + private includeExcludeSegmentCreation( experimentSegment: ParticipantsValidator, experimentDocSegmentData: ExperimentSegmentInclusion | ExperimentSegmentExclusion, experimentId: string, @@ -1618,7 +1644,7 @@ export class ExperimentService { const array = condition.levelCombinationElements.map((elements) => { elements.id = elements.id || uuid(); // elements.condition = condition; - return { ...elements, condition: condition } + return { ...elements, condition: condition }; }); allLevelCombinationElements.push(...array); }); diff --git a/backend/packages/Upgrade/src/api/services/SegmentService.ts b/backend/packages/Upgrade/src/api/services/SegmentService.ts index 14ceb02b78..55ee3c905a 100644 --- a/backend/packages/Upgrade/src/api/services/SegmentService.ts +++ b/backend/packages/Upgrade/src/api/services/SegmentService.ts @@ -5,7 +5,7 @@ import { IndividualForSegmentRepository } from '../repositories/IndividualForSeg import { GroupForSegmentRepository } from '../repositories/GroupForSegmentRepository'; import { Segment } from '../models/Segment'; import { UpgradeLogger } from '../../lib/logger/UpgradeLogger'; -import { EXPERIMENT_STATE, SEGMENT_TYPE, SERVER_ERROR, SEGMENT_STATUS } from 'upgrade_types'; +import { EXPERIMENT_STATE, SEGMENT_TYPE, SERVER_ERROR, SEGMENT_STATUS, CACHE_PREFIX } from 'upgrade_types'; import { getConnection } from 'typeorm'; import uuid from 'uuid'; import { ErrorWithType } from '../errors/ErrorWithType'; @@ -61,7 +61,7 @@ export class SegmentService { } public async getSegmentByIds(ids: string[]): Promise { - return this.cacheService.wrapFunction(ids, async () => { + return this.cacheService.wrapFunction(CACHE_PREFIX.SEGMENT_KEY_PREFIX, ids, async () => { const result = await this.segmentRepository .createQueryBuilder('segment') .leftJoinAndSelect('segment.individualForSegment', 'individualForSegment') @@ -311,7 +311,7 @@ export class SegmentService { } // reset caching - this.cacheService.resetCache(); + await this.cacheService.resetPrefixCache(CACHE_PREFIX.SEGMENT_KEY_PREFIX); return transactionalEntityManager .getRepository(Segment) diff --git a/backend/packages/Upgrade/src/env.ts b/backend/packages/Upgrade/src/env.ts index 30143f29dc..0fa86237eb 100644 --- a/backend/packages/Upgrade/src/env.ts +++ b/backend/packages/Upgrade/src/env.ts @@ -99,6 +99,7 @@ export const env = { tokenSecretKey: getOsEnv('TOKEN_SECRET_KEY'), caching: { enabled: toBool(getOsEnvOptional('CACHING_ENABLED')), + ttl: toNumber(getOsEnvOptional('CACHING_TTL')), }, clientApi: { secret: getOsEnv('CLIENT_API_SECRET'), diff --git a/backend/packages/Upgrade/test/unit/repositories/ExperimentRepository.test.ts b/backend/packages/Upgrade/test/unit/repositories/ExperimentRepository.test.ts index 2a30f2ffc1..0daede3b59 100644 --- a/backend/packages/Upgrade/test/unit/repositories/ExperimentRepository.test.ts +++ b/backend/packages/Upgrade/test/unit/repositories/ExperimentRepository.test.ts @@ -60,7 +60,6 @@ describe('ExperimentRepository Testing', () => { generatedMaps: [experiment], raw: [experiment], }; - insertMock.expects('insert').once().returns(insertQueryBuilder); insertMock.expects('into').once().returns(insertQueryBuilder); @@ -249,13 +248,13 @@ describe('ExperimentRepository Testing', () => { .returns(selectQueryBuilder); const result = [experiment]; - selectMock.expects('leftJoinAndSelect').exactly(22).returns(selectQueryBuilder); - selectMock.expects('where').twice().returns(selectQueryBuilder); - selectMock.expects('getMany').twice().returns(Promise.resolve(result)); + selectMock.expects('leftJoinAndSelect').exactly(19).returns(selectQueryBuilder); + selectMock.expects('where').exactly(3).returns(selectQueryBuilder); + selectMock.expects('getMany').exactly(3).returns(Promise.resolve(result)); const res = await repo.getValidExperiments('context'); - sinon.assert.calledTwice(createQueryBuilderStub); + expect(createQueryBuilderStub.callCount).toBe(3); selectMock.verify(); expect(res).toEqual(result); @@ -266,15 +265,15 @@ describe('ExperimentRepository Testing', () => { .stub(ExperimentRepository.prototype, 'createQueryBuilder') .returns(selectQueryBuilder); - selectMock.expects('leftJoinAndSelect').exactly(22).returns(selectQueryBuilder); - selectMock.expects('where').twice().returns(selectQueryBuilder); - selectMock.expects('getMany').twice().returns(Promise.reject(err)); + selectMock.expects('leftJoinAndSelect').exactly(19).returns(selectQueryBuilder); + selectMock.expects('where').exactly(3).returns(selectQueryBuilder); + selectMock.expects('getMany').exactly(3).returns(Promise.reject(err)); expect(async () => { await repo.getValidExperiments('context'); }).rejects.toThrow(err); - sinon.assert.calledTwice(createQueryBuilderStub); + expect(createQueryBuilderStub.callCount).toBe(3); selectMock.verify(); }); @@ -284,13 +283,13 @@ describe('ExperimentRepository Testing', () => { .returns(selectQueryBuilder); const result = [experiment]; - selectMock.expects('leftJoinAndSelect').exactly(22).returns(selectQueryBuilder); - selectMock.expects('where').twice().returns(selectQueryBuilder); - selectMock.expects('getMany').twice().returns(Promise.resolve(result)); + selectMock.expects('leftJoinAndSelect').exactly(19).returns(selectQueryBuilder); + selectMock.expects('where').exactly(3).returns(selectQueryBuilder); + selectMock.expects('getMany').exactly(3).returns(Promise.resolve(result)); const res = await repo.getValidExperimentsWithPreview('context'); - sinon.assert.calledTwice(createQueryBuilderStub); + expect(createQueryBuilderStub.callCount).toBe(3); selectMock.verify(); expect(res).toEqual(result); @@ -301,15 +300,15 @@ describe('ExperimentRepository Testing', () => { .stub(ExperimentRepository.prototype, 'createQueryBuilder') .returns(selectQueryBuilder); - selectMock.expects('leftJoinAndSelect').exactly(22).returns(selectQueryBuilder); - selectMock.expects('where').twice().returns(selectQueryBuilder); - selectMock.expects('getMany').twice().returns(Promise.reject(err)); + selectMock.expects('leftJoinAndSelect').exactly(19).returns(selectQueryBuilder); + selectMock.expects('where').exactly(3).returns(selectQueryBuilder); + selectMock.expects('getMany').exactly(3).returns(Promise.reject(err)); expect(async () => { await repo.getValidExperimentsWithPreview('context'); }).rejects.toThrow(err); - sinon.assert.calledTwice(createQueryBuilderStub); + expect(createQueryBuilderStub.callCount).toBe(3); selectMock.verify(); }); diff --git a/backend/packages/Upgrade/test/unit/services/ExperimentAssignmentService.test.ts b/backend/packages/Upgrade/test/unit/services/ExperimentAssignmentService.test.ts index 4bf9c405db..19eb13ca18 100644 --- a/backend/packages/Upgrade/test/unit/services/ExperimentAssignmentService.test.ts +++ b/backend/packages/Upgrade/test/unit/services/ExperimentAssignmentService.test.ts @@ -26,32 +26,32 @@ import { ConditionPayloadRepository } from '../../../src/api/repositories/Condit import { GroupEnrollment } from '../../../src/api/models/GroupEnrollment'; import { MARKED_DECISION_POINT_STATUS } from 'upgrade_types'; -describe('Expeirment Assignment Service Test', () => { +describe('Experiment Assignment Service Test', () => { let sandbox; let testedModule; const experimentRepositoryMock = sinon.createStubInstance(ExperimentRepository); - const decisionPointRepositoryMock = sinon.createStubInstance(DecisionPointRepository); - const individualExclusionRepositoryMock = sinon.createStubInstance(IndividualExclusionRepository); - const groupExclusionRepositoryMock = sinon.createStubInstance(GroupExclusionRepository); - const groupEnrollmentRepositoryMock = sinon.createStubInstance(GroupEnrollmentRepository); - const individualEnrollmentRepositoryMock = sinon.createStubInstance(IndividualEnrollmentRepository); - const monitoredDecisionPointLogRepositoryMock = sinon.createStubInstance(MonitoredDecisionPointLogRepository); - const monitoredDecisionPointRepositoryMock = sinon.createStubInstance(MonitoredDecisionPointRepository); - const errorRepositoryMock = sinon.createStubInstance(ErrorRepository); - const logRepositoryMock = sinon.createStubInstance(LogRepository); - const metricRepositoryMock = sinon.createStubInstance(MetricRepository); - const stateTimeLogsRepositoryMock = sinon.createStubInstance(StateTimeLogsRepository); - const analyticsRepositoryMock = sinon.createStubInstance(AnalyticsRepository); - const conditionPayloadRepositoryMock = sinon.createStubInstance(ConditionPayloadRepository); - const previewUserServiceMock = sinon.createStubInstance(PreviewUserService); - const experimentUserServiceMock = sinon.createStubInstance(ExperimentUserService); - const scheduledJobServiceMock = sinon.createStubInstance(ScheduledJobService); - const errorServiceMock = sinon.createStubInstance(ErrorService); - const settingServiceMock = sinon.createStubInstance(SettingService); - const segmentServiceMock = sinon.createStubInstance(SegmentService); - const experimentServiceMock = sinon.createStubInstance(ExperimentService); - experimentServiceMock.formatingConditionPayload.restore() - experimentServiceMock.formatingPayload.restore() + const decisionPointRepositoryMock = sinon.createStubInstance(DecisionPointRepository); + const individualExclusionRepositoryMock = sinon.createStubInstance(IndividualExclusionRepository); + const groupExclusionRepositoryMock = sinon.createStubInstance(GroupExclusionRepository); + const groupEnrollmentRepositoryMock = sinon.createStubInstance(GroupEnrollmentRepository); + const individualEnrollmentRepositoryMock = sinon.createStubInstance(IndividualEnrollmentRepository); + const monitoredDecisionPointLogRepositoryMock = sinon.createStubInstance(MonitoredDecisionPointLogRepository); + const monitoredDecisionPointRepositoryMock = sinon.createStubInstance(MonitoredDecisionPointRepository); + const errorRepositoryMock = sinon.createStubInstance(ErrorRepository); + const logRepositoryMock = sinon.createStubInstance(LogRepository); + const metricRepositoryMock = sinon.createStubInstance(MetricRepository); + const stateTimeLogsRepositoryMock = sinon.createStubInstance(StateTimeLogsRepository); + const analyticsRepositoryMock = sinon.createStubInstance(AnalyticsRepository); + const conditionPayloadRepositoryMock = sinon.createStubInstance(ConditionPayloadRepository); + const previewUserServiceMock = sinon.createStubInstance(PreviewUserService); + const experimentUserServiceMock = sinon.createStubInstance(ExperimentUserService); + const scheduledJobServiceMock = sinon.createStubInstance(ScheduledJobService); + const errorServiceMock = sinon.createStubInstance(ErrorService); + const settingServiceMock = sinon.createStubInstance(SettingService); + const segmentServiceMock = sinon.createStubInstance(SegmentService); + const experimentServiceMock = sinon.createStubInstance(ExperimentService); + experimentServiceMock.formatingConditionPayload.restore() + experimentServiceMock.formatingPayload.restore() beforeEach(() => { sandbox = sinon.createSandbox(); @@ -158,10 +158,10 @@ describe('Expeirment Assignment Service Test', () => { const requestContext = { logger: loggerMock, userDoc: { id: 'user123', group: 'group', workingGroup: {} } }; const userId = '12345'; const context = 'context'; - const experimentRepositoryMock = { getValidExperiments: sandbox.stub().resolves([]) }; const experimentUserServiceMock = { getOriginalUserDoc: sandbox.stub().resolves({ id: 'user123', createdAt: new Date(), group: 'group', workingGroup: {} }) }; - testedModule.experimentRepository = experimentRepositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([]); testedModule.experimentUserService = experimentUserServiceMock; testedModule.segmentService.getSegmentByIds.resolves([{id: '77777777-7777-7777-7777-777777777777', subSegments: [], individualForSegment: [], groupForSegment: []} ]) @@ -177,13 +177,14 @@ describe('Expeirment Assignment Service Test', () => { const context = 'context'; const requestContext = { logger: loggerMock, userDoc: { id: userId, group: {'schoolId': ['school1']}, workingGroup: {} } }; const exp = simpleIndividualAssignmentExperiment; - const experimentRepositoryMock = { getValidExperiments: sandbox.stub().resolves([exp]) }; const experimentUserServiceMock = { getOriginalUserDoc: sandbox.stub().resolves(requestContext.userDoc) }; const previewUserServiceMock = { findOne: sandbox.stub().resolves(undefined) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } const individualExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } - testedModule.experimentRepository = experimentRepositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([exp]); + testedModule.experimentUserService = experimentUserServiceMock; testedModule.previewUserServiceMock = previewUserServiceMock; testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; @@ -205,13 +206,13 @@ describe('Expeirment Assignment Service Test', () => { const context = 'context'; const requestContext = { logger: loggerMock, userDoc: { id: userId, group: {'schoolId': ['school1']}, workingGroup: {} } }; const exp = factorialIndividualAssignmentExperiment; - const experimentRepositoryMock = { getValidExperiments: sandbox.stub().resolves([exp]) }; const experimentUserServiceMock = { getOriginalUserDoc: sandbox.stub().resolves(requestContext.userDoc) }; const previewUserServiceMock = { findOne: sandbox.stub().resolves(undefined) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } const individualExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } - testedModule.experimentRepository = experimentRepositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([exp]); testedModule.experimentUserService = experimentUserServiceMock; testedModule.previewUserServiceMock = previewUserServiceMock; testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; @@ -247,14 +248,14 @@ describe('Expeirment Assignment Service Test', () => { const context = 'context'; const requestContext = { logger: loggerMock, userDoc: { id: userId, group: {'schoolId': ['school1']}, workingGroup: {} } }; const exp = simpleWithinSubjectOrderedRoundRobinExperiment; - const experimentRepositoryMock = { getValidExperiments: sandbox.stub().resolves([exp]) }; const experimentUserServiceMock = { getOriginalUserDoc: sandbox.stub().resolves(requestContext.userDoc) }; const previewUserServiceMock = { findOne: sandbox.stub().resolves(undefined) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } const individualExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } const monitoredDecisionPointLogRepositoryMock = { find: sandbox.stub().resolves(0), getAllMonitoredDecisionPointLog: sandbox.stub().resolves([])} - testedModule.experimentRepository = experimentRepositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([exp]); testedModule.experimentUserService = experimentUserServiceMock; testedModule.previewUserServiceMock = previewUserServiceMock; testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; @@ -281,7 +282,6 @@ describe('Expeirment Assignment Service Test', () => { groupEnrollment.experiment = exp; groupEnrollment.condition = exp.conditions[0]; groupEnrollment.groupId = 'add-group1'; - const experimentRepositoryMock = { getValidExperiments: sandbox.stub().resolves([exp]) }; const experimentUserServiceMock = { getOriginalUserDoc: sandbox.stub().resolves(requestContext.userDoc) }; const previewUserServiceMock = { findOne: sandbox.stub().resolves(undefined) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } @@ -289,7 +289,8 @@ describe('Expeirment Assignment Service Test', () => { const groupEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([groupEnrollment]) } const groupExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } - testedModule.experimentRepository = experimentRepositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([exp]); testedModule.experimentUserService = experimentUserServiceMock; testedModule.previewUserServiceMock = previewUserServiceMock; testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; @@ -315,7 +316,6 @@ describe('Expeirment Assignment Service Test', () => { const context = 'context'; const requestContext = { logger: loggerMock, userDoc: { id: userId, group: {'add-group1': ['school1']}, workingGroup: {'add-group1': 'school1'} } }; const exp = factorialGroupAssignmentExperiment; - const experimentRepositoryMock = { getValidExperiments: sandbox.stub().resolves([exp]) }; const experimentUserServiceMock = { getOriginalUserDoc: sandbox.stub().resolves(requestContext.userDoc) }; const previewUserServiceMock = { findOne: sandbox.stub().resolves(undefined) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } @@ -328,7 +328,9 @@ describe('Expeirment Assignment Service Test', () => { const groupEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([groupEnrollment]) } const groupExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } - testedModule.experimentRepository = experimentRepositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([exp]); + testedModule.experimentUserService = experimentUserServiceMock; testedModule.previewUserServiceMock = previewUserServiceMock; testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; @@ -385,7 +387,6 @@ describe('Expeirment Assignment Service Test', () => { const clientError = 'clientError'; const loggerMock = { info: sandbox.stub(), error: sandbox.stub() }; const decisionPointRespositoryMock = { find: sandbox.stub().resolves([]) }; - const experimentRespositoryMock = { getValidExperiments: sandbox.stub().resolves([]) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } const individualExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } const groupEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } @@ -393,7 +394,8 @@ describe('Expeirment Assignment Service Test', () => { const monitoredDecisionPointRepositoryMock = { saveRawJson: sandbox.stub().callsFake((args) => {return args}) }; testedModule.decisionPointRepository = decisionPointRespositoryMock; - testedModule.experimentRepository = experimentRespositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([]); testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; testedModule.individualExclusionRepository = individualExclusionRepositoryMock; testedModule.groupEnrollmentRepository = groupEnrollmentRepositoryMock; @@ -416,7 +418,6 @@ describe('Expeirment Assignment Service Test', () => { const condition = 'testCondition'; const loggerMock = { info: sandbox.stub(), error: sandbox.stub() }; const decisionPointRespositoryMock = { find: sandbox.stub().resolves([]) }; - const experimentRespositoryMock = { getValidExperiments: sandbox.stub().resolves([]) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } const individualExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } const groupEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } @@ -424,7 +425,8 @@ describe('Expeirment Assignment Service Test', () => { const monitoredDecisionPointRepositoryMock = { saveRawJson: sandbox.stub().callsFake((args) => {return args}) }; testedModule.decisionPointRepository = decisionPointRespositoryMock; - testedModule.experimentRepository = experimentRespositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([]); testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; testedModule.individualExclusionRepository = individualExclusionRepositoryMock; testedModule.groupEnrollmentRepository = groupEnrollmentRepositoryMock; @@ -447,7 +449,6 @@ describe('Expeirment Assignment Service Test', () => { const condition = 'testCondition'; const loggerMock = { info: sandbox.stub(), error: sandbox.stub() }; const decisionPointRespositoryMock = { find: sandbox.stub().resolves([simpleDPExperiment]) }; - const experimentRespositoryMock = { getValidExperiments: sandbox.stub().resolves([simpleIndividualAssignmentExperiment]) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } const individualExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } const groupEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } @@ -463,7 +464,8 @@ describe('Expeirment Assignment Service Test', () => { const monitoredDecisionPointRepositoryMock = { saveRawJson: sandbox.stub().callsFake((args) => {return args}), findOne: sandbox.stub().resolves(monitoredDocument)}; testedModule.decisionPointRepository = decisionPointRespositoryMock; - testedModule.experimentRepository = experimentRespositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([simpleIndividualAssignmentExperiment]); testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; testedModule.individualExclusionRepository = individualExclusionRepositoryMock; testedModule.groupEnrollmentRepository = groupEnrollmentRepositoryMock; @@ -482,7 +484,6 @@ describe('Expeirment Assignment Service Test', () => { const condition = 'testCondition'; const loggerMock = { info: sandbox.stub(), error: sandbox.stub() }; const decisionPointRespositoryMock = { find: sandbox.stub().resolves([simpleDPExperiment]) }; - const experimentRespositoryMock = { getValidExperiments: sandbox.stub().resolves([simpleGroupAssignmentExperiment]) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } const individualExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } const groupEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } @@ -498,7 +499,8 @@ describe('Expeirment Assignment Service Test', () => { const monitoredDecisionPointRepositoryMock = { saveRawJson: sandbox.stub().callsFake((args) => {return args}), findOne: sandbox.stub().resolves(monitoredDocument)}; testedModule.decisionPointRepository = decisionPointRespositoryMock; - testedModule.experimentRepository = experimentRespositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([simpleIndividualAssignmentExperiment]); testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; testedModule.individualExclusionRepository = individualExclusionRepositoryMock; testedModule.groupEnrollmentRepository = groupEnrollmentRepositoryMock; @@ -517,7 +519,6 @@ describe('Expeirment Assignment Service Test', () => { const condition = 'testCondition'; const loggerMock = { info: sandbox.stub(), error: sandbox.stub() }; const decisionPointRespositoryMock = { find: sandbox.stub().resolves([withinSubjectDPExperiment]) }; - const experimentRespositoryMock = { getValidExperiments: sandbox.stub().resolves([]) }; const individualEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } const individualExclusionRepositoryMock = { findExcluded: sandbox.stub().resolves([]) } const groupEnrollmentRepositoryMock = { findEnrollments: sandbox.stub().resolves([]) } @@ -533,7 +534,8 @@ describe('Expeirment Assignment Service Test', () => { const monitoredDecisionPointRepositoryMock = { saveRawJson: sandbox.stub().callsFake((args) => {return args}), findOne: sandbox.stub().resolves(monitoredDocument)}; testedModule.decisionPointRepository = decisionPointRespositoryMock; - testedModule.experimentRepository = experimentRespositoryMock; + testedModule.experimentService = experimentServiceMock; + testedModule.experimentService.getCachedValidExperiments = sandbox.stub().resolves([simpleIndividualAssignmentExperiment]); testedModule.individualEnrollmentRepository = individualEnrollmentRepositoryMock; testedModule.individualExclusionRepository = individualExclusionRepositoryMock; testedModule.groupEnrollmentRepository = groupEnrollmentRepositoryMock; diff --git a/types/src/Experiment/enums.ts b/types/src/Experiment/enums.ts index 95eb419e6d..631aafae36 100644 --- a/types/src/Experiment/enums.ts +++ b/types/src/Experiment/enums.ts @@ -193,3 +193,8 @@ export enum SUPPORTED_CALIPER_PROFILES { export enum SUPPORTED_CALIPER_EVENTS { GRADE = 'GradeEvent', } + +export enum CACHE_PREFIX { + EXPERIMENT_KEY_PREFIX = 'validExperiments-', + SEGMENT_KEY_PREFIX = 'segments-', +} diff --git a/types/src/index.ts b/types/src/index.ts index ac99cca9d8..f97584622e 100644 --- a/types/src/index.ts +++ b/types/src/index.ts @@ -26,6 +26,7 @@ export { SUPPORTED_CALIPER_EVENTS, PAYLOAD_TYPE, CONDITION_ORDER, + CACHE_PREFIX, } from './Experiment/enums'; export { IEnrollmentCompleteCondition,