Skip to content

Commit

Permalink
sql: abort queries (#172)
Browse files Browse the repository at this point in the history
Adds feature to cancel queries.
  • Loading branch information
joacoc authored Dec 13, 2023
1 parent b8415cb commit 1e65944
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 21 deletions.
50 changes: 38 additions & 12 deletions src/clients/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,36 @@ export default class SqlClient {
this.handleReconnection();
}

/**
* Reconnects the pool and creates a new private client.
*
* This is useful to reconnect on errors, or when wanting
* to abort a query.
*/
async reconnect() {
try {
const client = await this.privateClient;
client.release();
} catch (err) {
console.error("[SqlClient]", "Error aborting private client:", err);
} finally {
try {
const pool = await this.pool;
pool.end();
} catch (err) {
console.error("[SqlClient]", "Error ending pool. It is ok it the pool connection failed:", err);
} finally {
this.pool = this.buildPool();
this.privateClient = this.buildPrivateClient();
this.handleReconnection();
}
}
}

/**
* Handles the reconnection from the pool or private client
* when there is a connection issue.
*/
private async handleReconnection() {
let reconnecting = false;

Expand All @@ -38,18 +68,8 @@ export default class SqlClient {
if (reconnecting === false && this.ended === false) {
reconnecting = true;
const interval = setInterval(async () => {
try {
const pool = await this.pool;
pool.end();
} catch (err) {
console.error("[SqlClient]", "Error awaiting pool to end. It is ok it the pool connection failed.");
} finally {
this.pool = this.buildPool();
this.privateClient = this.buildPrivateClient();
this.handleReconnection();
reconnecting = false;
clearInterval(interval);
}
this.reconnect();
clearInterval(interval);
}, 5000);
}
};
Expand All @@ -71,11 +91,17 @@ export default class SqlClient {
}
}

/**
* @returns a client form the pool.
*/
private async buildPrivateClient(): Promise<PoolClient> {
const pool = await this.pool;
return pool.connect();
}

/**
* @returns a Postgres connection pool.
*/
private async buildPool(): Promise<Pool> {
return new Pool(await this.buildPoolConfig());
}
Expand Down
15 changes: 15 additions & 0 deletions src/context/asyncContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,21 @@ export default class AsyncContext extends Context {
}
}

/**
* Cancels a query by reconnecting the SQL client.
*
* This action will release the private client
* and create a new pool of clients.
*
*/
async cancelQuery() {
try {
await this.clients.sql?.reconnect();
} catch (err) {
throw new ExtensionError(Errors.cancelQueryError, err);
}
}

/**
* Adds a new profile.
*
Expand Down
30 changes: 23 additions & 7 deletions src/providers/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,17 @@ export const buildRunSQLCommand = (context: AsyncContext) => {
// the results from one query can overlap the results
// from another. We only want to display the last results.
const id = randomUUID();
let queryCanceled = false;
try {
resultsProvider.setQueryId(id);
const cancelHandler = () => {
console.log("[RunSQLCommand]", "Canceling query.");
queryCanceled = true;
context.cancelQuery();
}
resultsProvider.setQuery(
id,
cancelHandler
);
try {
const statements = await context.parseSql(query);
console.log("[RunSQLCommand]", "Running statements: ", statements);
Expand Down Expand Up @@ -78,13 +87,20 @@ export const buildRunSQLCommand = (context: AsyncContext) => {
sql: statement.sql
});

resultsProvider.setResults(id,
undefined,
{
message: error.toString(),
position: error.position,
if (!queryCanceled) {
resultsProvider.setResults(id,
undefined,
{
message: error.toString(),
position: error.position,
query,
});
} else {
resultsProvider.setResults(id, undefined, {
message: "Query canceled.",
query,
});
});
}

// Break for-loop.
break;
Expand Down
17 changes: 16 additions & 1 deletion src/providers/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export default class ResultsProvider implements vscode.WebviewViewProvider {
// It is used to display the results and not overlap them from the results of a laggy query.
private lastQueryId: string | undefined;

// Handles query cancelation. After running this function
// the context will reconnect the SQL client.
private cancelHandler: undefined | (() => void);

// The provider can be invoked from `materialize.run`.
// When this happens, the inner rendering script will not be ready
// to listen changes. This variable holds the pending data to render
Expand All @@ -34,14 +38,19 @@ export default class ResultsProvider implements vscode.WebviewViewProvider {
constructor(private readonly _extensionUri: vscode.Uri) {
this._extensionUri = _extensionUri;
this.isScriptReady = false;
this.cancelHandler = undefined;
}

/**
* Cleans the results and sets a latest query id.
* @param id
*/
public setQueryId(id: string) {
public setQuery(
id: string,
cancelHandler: () => void,
) {
this.lastQueryId = id;
this.cancelHandler = cancelHandler;

if (this._view) {
console.log("[ResultsProvider]", "New query.");
Expand Down Expand Up @@ -113,6 +122,12 @@ export default class ResultsProvider implements vscode.WebviewViewProvider {
console.error("[ResultsProvider]", error);
break;
}
case "cancelQuery": {
if (this.cancelHandler) {
this.cancelHandler();
}
break;
}
case "ready": {
console.log("[ResultsProvider]", "The script is now ready.");
this.isScriptReady = true;
Expand Down
17 changes: 17 additions & 0 deletions src/providers/scripts/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
timerElement.style.paddingTop = "0.5rem";
document.body.appendChild(timerElement);
}

// Reset and show the timer content
timerElement.textContent = 'Time elapsed:';
timerElement.style.display = 'block';
Expand Down Expand Up @@ -131,6 +132,18 @@ import {
}, 500);
}
}, 103);

const cancelQuery = () => {
vscode.postMessage({ type: "cancelQuery" });
};
const cancelElement = document.createElement('vscode-button');
cancelElement.innerHTML = "Cancel";
cancelElement.style.float = "right";
cancelElement.id = "cancelButton";
cancelElement.setAttribute("appearance", "secondary");
cancelElement.onclick = cancelQuery;
container.appendChild(cancelElement);

break;
}

Expand All @@ -139,6 +152,10 @@ import {
if (progressRing) {
progressRing.style.display = "none";
}
const cancelButton = document.getElementById("cancelButton");
if (cancelButton) {
cancelButton.style.display = "none";
}

const { data: results } = message;
console.log("[Results.js]", "New message - Results: ", results);
Expand Down
6 changes: 5 additions & 1 deletion src/utilities/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,9 @@ export enum Errors {
/**
* Raises when a fetch failes after a minute.
*/
fetchTimeoutError = "Failed to fetch after a minute."
fetchTimeoutError = "Failed to fetch after a minute.",
/**
* Raises when trying to cancel a query.
*/
cancelQueryError = "Failed to cancel the query."
}

0 comments on commit 1e65944

Please sign in to comment.