Skip to content

Commit

Permalink
Update cache resolving logic for records
Browse files Browse the repository at this point in the history
  • Loading branch information
DimuthuMadushan committed Feb 9, 2024
1 parent 337d332 commit 735ba14
Show file tree
Hide file tree
Showing 18 changed files with 305 additions and 14 deletions.
25 changes: 25 additions & 0 deletions ballerina-tests/tests/44_server_caches.bal
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,28 @@ isolated function testServerCacheEvictionWithTTL() returns error? {
expectedPayload = check getJsonContentFromFile("server_cache_eviction_with_TTL_3");
assertJsonValuesWithOrder(actualPayload, expectedPayload);
}

@test:Config {
groups: ["server_cache", "records"],
dataProvider: dataProviderServerSideCacheWithDynamicResponse
}
isolated function testServerSideCacheWithDynamicResponse(string documentFile, string[] resourceFileNames, json variables = (), string[] operationNames = []) returns error? {
string url = "http://localhost:9091/dynamic_response";
string document = check getGraphqlDocumentFromFile(documentFile);
foreach int i in 0..< resourceFileNames.length() {
json actualPayload = check getJsonPayloadFromService(url, document, variables, operationNames[i]);
json expectedPayload = check getJsonContentFromFile(resourceFileNames[i]);
assertJsonValuesWithOrder(actualPayload, expectedPayload);
}
}

function dataProviderServerSideCacheWithDynamicResponse() returns map<[string, string[], json, string[]]> {
map<[string, string[], json, string[]]> dataSet = {
"1": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_1", "server_cache_with_dynamic_responses_2", "server_cache_with_dynamic_responses_3"], (), ["A", "B", "C"]],
"2": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_2", "server_cache_with_dynamic_responses_4", "server_cache_with_dynamic_responses_2"], (), ["B", "D", "B"]],
"3": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_3", "server_cache_with_dynamic_responses_4", "server_cache_with_dynamic_responses_3"], (), ["C", "D", "C"]],
"4": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_3", "server_cache_with_dynamic_responses_4", "server_cache_with_dynamic_responses_5"], (), ["C", "E", "C"]],
"5": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_6", "server_cache_with_dynamic_responses_7", "server_cache_with_dynamic_responses_8"], (), ["B", "F", "B"]]
};
return dataSet;
}
6 changes: 6 additions & 0 deletions ballerina-tests/tests/records.bal
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,9 @@ type Associate record {|
|};

public type Relationship FriendService|AssociateService;

type User record {|
int id?;
string name?;
int age?;
|};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
query A {
user(id:1) {
id
}
}

query B {
user(id:1) {
id
name
}
}

query C {
user(id:1) {
id
name
age
}
}

mutation D {
updateUser(id:1, name:"White", age:45, enableEvict:false) {
id
name
age
}
}

mutation E {
updateUser(id:1, name:"White", age:45, enableEvict:true) {
id
name
age
}
}

mutation F {
updateUser(id:1, name:"Walter", age:45, enableEvict:true) {
id
name
age
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"data": {
"user": {
"id": 1
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"data": {
"user": {
"id": 1,
"name": "John"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"user": {
"id": 1,
"name": "John",
"age": 25
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"updateUser": {
"id": 1,
"name": "White",
"age": 45
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"user": {
"id": 1,
"name": "White",
"age": 45
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"data": {
"user": {
"id": 1,
"name": "White"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"updateUser": {
"id": 1,
"name": "Walter",
"age": 45
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"data": {
"user": {
"id": 1,
"name": "Walter"
}
}
}
29 changes: 29 additions & 0 deletions ballerina-tests/tests/test_services.bal
Original file line number Diff line number Diff line change
Expand Up @@ -2971,3 +2971,32 @@ service /caching_with_interceptor_operations on basicListener {
return self.name;
}
}

service /dynamic_response on basicListener {
private User user = {id: 1, name: "John", age: 25};

@graphql:ResourceConfig {
cacheConfig: {
enabled: true,
maxAge: 600
}
}
resource function get user(graphql:Field 'field, int id) returns User {
string[] sub = 'field.getSubfieldNames();
if sub.length() == 1 {
return {id: self.user.id};
}else if sub.length() == 2 {
return {id: self.user.id, name: self.user.name};
} else {
return self.user;
}
}

remote function updateUser(graphql:Context context, int id, string name, int age, boolean enableEvict) returns User|error {
if enableEvict {
check context.invalidate("user");
}
self.user = {id:id, name:name, age:age};
return self.user;
}
}
25 changes: 23 additions & 2 deletions ballerina/common_utils.bal
Original file line number Diff line number Diff line change
Expand Up @@ -516,9 +516,30 @@ public isolated function __addError(Context context, ErrorDetail errorDetail) {
context.addError(errorDetail);
}

isolated function generateArgHash(parser:ArgumentNode[] arguments, string[] parentArgHashes = []) returns string {
any[] argValues = [...parentArgHashes];
isolated function generateArgHash(parser:ArgumentNode[] arguments, string[] parentArgHashes = [],
string[] optionalFields = []) returns string {
any[] argValues = [...parentArgHashes, ...optionalFields];
argValues.push(...arguments.'map((arg) => arg.getValue()));
byte[] hash = crypto:hashMd5(argValues.toString().toBytes());
return hash.toBase64();
}

isolated function getNullableFieldsFromType(__Type fieldType) returns string[] {
string[] nullableFields = [];
__Field[]? fields = unwrapNonNullype(fieldType).fields;
if fields is __Field[] {
foreach __Field 'field in fields {
if isNullableField('field.'type) {
nullableFields.push('field.name);
}
}
}
return nullableFields;
}

isolated function isNullableField(__Type 'type) returns boolean {
if 'type.kind == NON_NULL {
return false;
}
return true;
}
5 changes: 5 additions & 0 deletions ballerina/engine_utils.bal
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@ isolated function initCacheTable(ServerCacheConfig? operationCacheConfig, Server
}
return;
}

isolated function hasRecordReturnType(service object {} serviceObject, string[] path)
returns boolean = @java:Method {
'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine"
} external;
20 changes: 19 additions & 1 deletion ballerina/field.bal
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class Field {
private final readonly & string[] parentArgHashes;
private final boolean cacheEnabled;
private final decimal cacheMaxAge;
private boolean hasRequestedNullableFields;

isolated function init(parser:FieldNode internalNode, __Type fieldType, service object {}? serviceObject = (),
(string|int)[] path = [], parser:RootOperationType operationType = parser:OPERATION_QUERY,
Expand Down Expand Up @@ -57,6 +58,8 @@ public class Field {
self.cacheEnabled = false;
self.cacheMaxAge = 0d;
}
self.hasRequestedNullableFields = self.cacheEnabled && serviceObject is service object {}
&& hasFields(getOfType(self.fieldType)) && hasRecordReturnType(serviceObject, self.resourcePath);
}

# Returns the name of the field.
Expand Down Expand Up @@ -200,11 +203,26 @@ public class Field {
}

private isolated function generateCacheKey() returns string {
string[] requestedNullableFields = [];
if self.hasRequestedNullableFields {
requestedNullableFields = self.getRequestedNullableFields();
}
string resourcePath = "";
foreach string|int path in self.path {
resourcePath += string `${path}.`;
}
string hash = generateArgHash(self.internalNode.getArguments(), self.parentArgHashes);
string hash = generateArgHash(self.internalNode.getArguments(), self.parentArgHashes, requestedNullableFields);
return string `${resourcePath}${hash}`;
}

private isolated function getRequestedNullableFields() returns string[] {
string[] nullableFields = getNullableFieldsFromType(self.fieldType);
string[] requestedNullableFields = [];
foreach string 'field in nullableFields {
if self.getSubfieldNames().indexOf('field) is int {
requestedNullableFields.push('field);
}
}
return requestedNullableFields.sort();
}
}
68 changes: 58 additions & 10 deletions docs/spec/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1776,7 +1776,7 @@ service on new graphql:Listener(9090) {

#### 7.1.9 Operation-level Cache Configurations

The `cacheConfig` field is used to provide the GraphQL operation-level cache configuration to enable the caching for `query` operations.
The `cacheConfig` field is used to provide the operation-level cache configuration to enable the [GraphQL caching](#107-caching) for `query` operations.

###### Example: Enable Operation-level Cache with Default Values

Expand Down Expand Up @@ -1878,7 +1878,7 @@ service on new graphql:Listener(9090) {

#### 7.2.3 Field-level Cache Configuration

The `cacheConfig` field is used to provide the field-level cache configs. The fields are as same as the operation cache configs. The field configurations override the operation configurations.
The `cacheConfig` field is used to provide the field-level cache configs. The fields are as same as the operation cache configs.

###### Example: Field-level Cache Configs

Expand Down Expand Up @@ -3764,7 +3764,7 @@ Operation-level caching can be used to cache the entire operation, and this can

##### 10.7.1.2 Field-level Caching

The GraphQL field-level caching can be enabled only for a specific field. This can be done by providing the [field cache configurations](#723-field-level-cache-configuration). Once the field-level caching is enabled for a field, it will be applied to the sub-fields of that field.
The GraphQL field-level caching can be enabled only for a specific field. This can be done by providing the [field cache configurations](#723-field-level-cache-configuration). Once the field-level caching is enabled for a field, it will be applied to the sub-fields of that field. The field-level cache configuration can be used to override the operation-level cache configurations.

#### 10.7.1.3 Cache Eviction

Expand Down Expand Up @@ -3836,6 +3836,12 @@ service /graphql on new graphql:Listener(9090) {
{name: "Jesse Pinkman", age: 23, isMarried: false}
];
@graphql:ResourceConfig {
cacheConfig: {
enabled: true,
maxAge 20
}
}
isolated resource function get friends(boolean isMarried = false) returns Person[] {
if isMarried {
return from Friend friend in self.friends
Expand Down Expand Up @@ -3870,20 +3876,62 @@ public isolated distinct service class Person {
return self.name;
}
@graphql:ResourceConfig {
cacheConfig: {
enabled: true,
maxAge 20
}
}
isolated resource function get age() returns int {
return self.age;
}
@graphql:ResourceConfig {
cacheConfig: {
enabled: false
}
}
isolated resource function get isMarried() returns boolean {
return self.isMarried;
}
}
```

In this example, GraphQL field-level caching is enabled for the `age` field via the resource configurations. When the age is changed using the `updateAge` operation, the `invalidate` method is used to remove the existing cache entries related to the age field.
In this example, GraphQL field-level caching is enabled for the `friends` field via the resource configurations. The configuration applies to its subfields, the `name` and `age` fields will be cached. Since the caching is disabled for the field `isMarried`, it will not be cached. When the age is changed using the `updateAge` operation, the `invalidate` method is used to remove the existing cache entries related to the age field.

###### Example: Overrides Operation-level Cache Config

```ballerina
import ballerina/graphql;
@graphql:ServiceConfig {
cacheConfig: {
enabled: true,
maxAge: 50
}
}
service /graphql on new graphql:Listener(9090) {
private string name = "Ballerina GraphQL";
private string 'type = "code first";
private string version = "V1.11.0";
resource function get name() returns string {
return self.name;
}
resource function get 'type() returns string {
return self.'type;
}
@graphql:ServiceConfig {
cacheConfig: {
enabled: false
}
}
resource function get version() returns string {
return self.'type;
}
remote function updateName(graphql:Context context, string name) returns string|error {
check context.invalidate("name");
self.name = name
return self.name;
}
}
```

In this example, caching is enabled at the operation level. Therefore, the field `name` and `type` will be cached. Since the field-level cache configuration overrides the parent cache configurations, the field `version` will not be cached. When updating the name with a mutation, the cached values become invalid. Hence, the `invalidate` function can be used to evict the existing cache values.
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ private Object[] getArgumentsForMethod() {
return result;
}

private static Type getEffectiveType(IntersectionType intersectionType) {
public static Type getEffectiveType(IntersectionType intersectionType) {
for (Type constituentType : intersectionType.getConstituentTypes()) {
if (constituentType.getTag() != TypeTags.READONLY_TAG) {
return constituentType;
Expand Down
Loading

0 comments on commit 735ba14

Please sign in to comment.