Skip to content

Commit 65d4515

Browse files
emhagmanPaul Korzhyk
authored andcommitted
Add support for readOnly and bestEffort queries (#58)
* Add support for readOnly and bestEffort queries * Make "readOnly" required if txnOpts present
1 parent 8488689 commit 65d4515

File tree

10 files changed

+83
-11
lines changed

10 files changed

+83
-11
lines changed

lib/client.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import * as grpc from "grpc";
22
import * as messages from "../generated/api_pb";
33
import { DgraphClientStub } from "./clientStub";
4-
import { Txn } from "./txn";
4+
import { Txn, TxnOptions } from "./txn";
55
import * as types from "./types";
66
export declare class DgraphClient {
77
private readonly clients;
88
private readonly linRead;
99
private debugMode;
1010
constructor(...clients: DgraphClientStub[]);
1111
alter(op: messages.Operation, metadata?: grpc.Metadata | null, options?: grpc.CallOptions | null): Promise<types.Payload>;
12-
newTxn(): Txn;
12+
newTxn(txnOpts?: TxnOptions): Txn;
1313
setDebugMode(mode?: boolean): void;
1414
debug(msg: string): void;
1515
getLinRead(): messages.LinRead;

lib/client.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ var DgraphClient = (function () {
7171
});
7272
});
7373
};
74-
DgraphClient.prototype.newTxn = function () {
75-
return new txn_1.Txn(this);
74+
DgraphClient.prototype.newTxn = function (txnOpts) {
75+
return new txn_1.Txn(this, txnOpts);
7676
};
7777
DgraphClient.prototype.setDebugMode = function (mode) {
7878
if (mode === void 0) { mode = true; }

lib/errors.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export declare const ERR_NO_CLIENTS: Error;
22
export declare const ERR_FINISHED: Error;
33
export declare const ERR_ABORTED: Error;
4+
export declare const ERR_BEST_EFFORT_REQUIRED_READ_ONLY: Error;

lib/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
33
exports.ERR_NO_CLIENTS = new Error("No clients provided in DgraphClient constructor");
44
exports.ERR_FINISHED = new Error("Transaction has already been committed or discarded");
55
exports.ERR_ABORTED = new Error("Transaction has been aborted. Please retry");
6+
exports.ERR_BEST_EFFORT_REQUIRED_READ_ONLY = new Error("Best effort only works for read-only queries");

lib/txn.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@ import * as grpc from "grpc";
22
import * as messages from "../generated/api_pb";
33
import { DgraphClient } from "./client";
44
import * as types from "./types";
5+
export declare type TxnOptions = {
6+
readOnly?: boolean;
7+
bestEffort?: boolean;
8+
};
59
export declare class Txn {
610
private readonly dc;
711
private readonly ctx;
812
private finished;
913
private mutated;
14+
private readonly useReadOnly;
15+
private readonly useBestEffort;
1016
private sequencingProp;
11-
constructor(dc: DgraphClient);
17+
constructor(dc: DgraphClient, txnOpts?: TxnOptions);
1218
sequencing(sequencing: messages.LinRead.SequencingMap[keyof messages.LinRead.SequencingMap]): void;
1319
query(q: string, metadata?: grpc.Metadata, options?: grpc.CallOptions): Promise<types.Response>;
1420
queryWithVars(q: string, vars?: {

lib/txn.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
"use strict";
2+
var __assign = (this && this.__assign) || function () {
3+
__assign = Object.assign || function(t) {
4+
for (var s, i = 1, n = arguments.length; i < n; i++) {
5+
s = arguments[i];
6+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7+
t[p] = s[p];
8+
}
9+
return t;
10+
};
11+
return __assign.apply(this, arguments);
12+
};
213
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
314
return new (P || (P = Promise))(function (resolve, reject) {
415
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
@@ -40,13 +51,22 @@ var errors_1 = require("./errors");
4051
var types = require("./types");
4152
var util_1 = require("./util");
4253
var Txn = (function () {
43-
function Txn(dc) {
54+
function Txn(dc, txnOpts) {
4455
this.finished = false;
4556
this.mutated = false;
57+
this.useReadOnly = false;
58+
this.useBestEffort = false;
4659
this.dc = dc;
4760
this.ctx = new messages.TxnContext();
4861
this.ctx.setLinRead(this.dc.getLinRead());
4962
this.sequencingProp = messages.LinRead.Sequencing.CLIENT_SIDE;
63+
var defaultedTxnOpts = __assign({ readOnly: false, bestEffort: false }, txnOpts);
64+
this.useReadOnly = defaultedTxnOpts.readOnly;
65+
this.useBestEffort = defaultedTxnOpts.bestEffort;
66+
if (this.useBestEffort && !this.useReadOnly) {
67+
this.dc.debug("Client attempted to query using best-effort without setting the transaction to read-only");
68+
throw errors_1.ERR_BEST_EFFORT_REQUIRED_READ_ONLY;
69+
}
5070
}
5171
Txn.prototype.sequencing = function (sequencing) {
5272
this.sequencingProp = sequencing;
@@ -67,6 +87,8 @@ var Txn = (function () {
6787
req = new messages.Request();
6888
req.setQuery(q);
6989
req.setStartTs(this.ctx.getStartTs());
90+
req.setReadOnly(this.useReadOnly);
91+
req.setBestEffort(this.useBestEffort);
7092
linRead = this.ctx.getLinRead();
7193
linRead.setSequencing(this.sequencingProp);
7294
req.setLinRead(linRead);

src/client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as messages from "../generated/api_pb";
44

55
import { DgraphClientStub } from "./clientStub";
66
import { ERR_NO_CLIENTS } from "./errors";
7-
import { Txn } from "./txn";
7+
import { Txn, TxnOptions } from "./txn";
88
import * as types from "./types";
99
import { mergeLinReads, stringifyMessage } from "./util";
1010

@@ -54,8 +54,8 @@ export class DgraphClient {
5454
/**
5555
* newTxn creates a new transaction.
5656
*/
57-
public newTxn(): Txn {
58-
return new Txn(this);
57+
public newTxn(txnOpts?: TxnOptions): Txn {
58+
return new Txn(this, txnOpts);
5959
}
6060

6161
/**

src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const ERR_NO_CLIENTS = new Error("No clients provided in DgraphClient constructor");
22
export const ERR_FINISHED = new Error("Transaction has already been committed or discarded");
33
export const ERR_ABORTED = new Error("Transaction has been aborted. Please retry");
4+
export const ERR_BEST_EFFORT_REQUIRED_READ_ONLY = new Error("Best effort only works for read-only queries");

src/txn.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as grpc from "grpc";
33
import * as messages from "../generated/api_pb";
44

55
import { DgraphClient } from "./client";
6-
import { ERR_ABORTED, ERR_FINISHED } from "./errors";
6+
import { ERR_ABORTED, ERR_BEST_EFFORT_REQUIRED_READ_ONLY, ERR_FINISHED } from "./errors";
77
import * as types from "./types";
88
import {
99
isAbortedError,
@@ -12,6 +12,11 @@ import {
1212
stringifyMessage,
1313
} from "./util";
1414

15+
export type TxnOptions = {
16+
readOnly?: boolean;
17+
bestEffort?: boolean;
18+
};
19+
1520
/**
1621
* Txn is a single atomic transaction.
1722
*
@@ -31,13 +36,22 @@ export class Txn {
3136
private readonly ctx: messages.TxnContext;
3237
private finished: boolean = false;
3338
private mutated: boolean = false;
39+
private readonly useReadOnly: boolean = false;
40+
private readonly useBestEffort: boolean = false;
3441
private sequencingProp: messages.LinRead.SequencingMap[keyof messages.LinRead.SequencingMap];
3542

36-
constructor(dc: DgraphClient) {
43+
constructor(dc: DgraphClient, txnOpts?: TxnOptions) {
3744
this.dc = dc;
3845
this.ctx = new messages.TxnContext();
3946
this.ctx.setLinRead(this.dc.getLinRead());
4047
this.sequencingProp = messages.LinRead.Sequencing.CLIENT_SIDE;
48+
const defaultedTxnOpts = {readOnly: false, bestEffort: false, ...txnOpts};
49+
this.useReadOnly = defaultedTxnOpts.readOnly;
50+
this.useBestEffort = defaultedTxnOpts.bestEffort;
51+
if (this.useBestEffort && !this.useReadOnly) {
52+
this.dc.debug(`Client attempted to query using best-effort without setting the transaction to read-only`);
53+
throw ERR_BEST_EFFORT_REQUIRED_READ_ONLY;
54+
}
4155
}
4256

4357
public sequencing(sequencing: messages.LinRead.SequencingMap[keyof messages.LinRead.SequencingMap]): void {
@@ -71,6 +85,8 @@ export class Txn {
7185
const req = new messages.Request();
7286
req.setQuery(q);
7387
req.setStartTs(this.ctx.getStartTs());
88+
req.setReadOnly(this.useReadOnly);
89+
req.setBestEffort(this.useBestEffort);
7490

7591
const linRead = this.ctx.getLinRead();
7692
// tslint:disable-next-line no-unsafe-any

tests/txn.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,31 @@ describe("txn", () => {
6666
const p = txn.query('{ me(func: eq(name, "Alice")) { name }}');
6767
await expect(p).rejects.toBe(dgraph.ERR_FINISHED);
6868
});
69+
70+
it("should succeed without increasing startTs (readOnly=true)", async () => {
71+
const res = await client.newTxn().query('{ me(func: eq(name, "Alice")) { name }}');
72+
const startTs = res.getTxn().getStartTs();
73+
const res2 = await client.newTxn({ readOnly: true }).query('{ me(func: eq(name, "Alice")) { name }}');
74+
const startTs2 = res2.getTxn().getStartTs();
75+
expect(startTs).not.toBe(startTs2); // we expect these to be different, the first query increases startTs
76+
const res3 = await client.newTxn({ readOnly: true }).query('{ me(func: eq(name, "Alice")) { name }}');
77+
const startTs3 = res3.getTxn().getStartTs();
78+
expect(startTs2).toBe(startTs3); // we expect these to be same, because readOnly doesn't increase startTs
79+
});
80+
81+
it("should succeed without error (readOnly=true, bestEffort=true)", async () => {
82+
const res = await client.newTxn().query('{ me(func: eq(name, "Alice")) { name }}');
83+
const startTs = res.getTxn().getStartTs();
84+
const res2 = await client.newTxn({ readOnly: true, bestEffort: true }).query('{ me(func: eq(name, "Alice")) { name }}');
85+
const startTs2 = res2.getTxn().getStartTs();
86+
expect(startTs2).toBeGreaterThanOrEqual(startTs);
87+
});
88+
89+
it("should throw error (readOnly=false, bestEffort=true)", () => {
90+
const test = () => client.newTxn({ bestEffort: true });
91+
expect(test).toThrow(dgraph.ERR_BEST_EFFORT_REQUIRED_READ_ONLY);
92+
});
93+
6994
});
7095

7196
describe("mutate", () => {

0 commit comments

Comments
 (0)