profile
viewpoint
Roberto Prevato RobertoPrevato EdgeDB Warsaw, Poland https://robertoprevato.github.io Web Developer, DevOps

RobertoPrevato/BlackSheep 386

Fast ASGI web framework and HTTP client for Python asyncio

RobertoPrevato/AzureDevOps-agents 19

Images for self-hosted DevOps agents

RobertoPrevato/aiohttp-three-template 13

Project template for Python aiohttp three-tier web applications

RobertoPrevato/AzureDocker 6

Docker images for web applications to host in Azure

RobertoPrevato/Base64 5

Pictures to base64 encoded strings bulk converter.

RobertoPrevato/BlackSheepMVC 5

MVC project template for BlackSheep web framework

RobertoPrevato/DataEntry 5

Forms validation library that implements Promise based validation of fields and forms, automatic decoration of fields, localized error messages. Integrable with Angular, Backbone, Knockout, React, Vue.js.

RobertoPrevato/azure-storage-python 4

Azure Storage Library for Python

RobertoPrevato/AzureBlobAsyncUpload 4

Example of upload and download made with asyncio and aiohttp for Azure Blob Service

RobertoPrevato/essentials 3

General purpose classes and functions, reusable in any kind of Python application.

PR opened edgedb/edgedb-js

Add a transaction api

Still a work in progress.

+374 -0

0 comment

6 changed files

pr created time in 2 days

create barnchedgedb/edgedb-js

branch : transactions

created branch time in 2 days

startedlodash/lodash

started time in 3 days

startedfoambubble/foam

started time in a month

push eventRobertoPrevato/PythonCLI

Roberto Prevato

commit sha 4d4af7cd66ab8f9a5bed2a5d01236ef29753401f

Update requirements.txt

view details

push time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 0b14e5b1c02876e7b35032f0308c7adbe0766f0f

Add a connection pool, closes #5

view details

push time in a month

delete branch edgedb/edgedb-js

delete branch : pool

delete time in a month

issue closededgedb/edgedb-js

Add a connection pool

closed time in a month

1st1

PR merged edgedb/edgedb-js

Adds a connection pool

The code mostly replicates the logic of the Python driver implementation.

+2674 -31

11 comments

12 changed files

RobertoPrevato

pr closed time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 34a8e850d832b358117f427193dcc34074569ef9

add a connection pool, closes #5

view details

push time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha dbaca36b6e8f5866c3430a428d21c96875ca2c1e

corrections on documentation

view details

push time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 84461a41f64c0d60171587d71ec702af4e486e5f

uses symbol instead of ts-ignore for internal methods, improves comments

view details

push time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 9884c7f84c4da56bd82d6681c77acd3b9e50f2ac

improves the usage of ts-ignore, adds comment

view details

push time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

 export class PoolConnectionProxy {     connection._setProxy(this);   } -  public get connection(): AwaitConnection {+  get connection(): AwaitConnection {

I modified the code to keep the connection private. :+1: I also made the queue private, and finally included a PoolStats returning the list of pending consumers + number of open connections. We will probably do a second pass over the pool stats when we have occasion to focus on this subject.

RobertoPrevato

comment created time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

 test(     async function user(pool: Pool): Promise<void> {       const proxy = await pool.acquire(); +      // @ts-ignore

Thanks for the heads up. By the way, what do you think about using @ts-ignore to keep the connectionHolder's proxy private?

    // @ts-ignore
    if (connectionProxy.holder.pool !== this) {

I like it on one hand, but I feel it's a compromise.

RobertoPrevato

comment created time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha a2442949dd142f3bd58d111310775ec147457c17

corrects typo in client.ts

view details

Roberto Prevato

commit sha 517fb0c672dc657dfee6fc9aa87956af980e5689

makes the proxy connection private

view details

Roberto Prevato

commit sha 6352e06f21f1323da8d35afc8e778cead637f356

makes the pool queue private

view details

Roberto Prevato

commit sha 63ca0253dc723f7e663e7fb4f5981e318c60904e

makes the pool queue private

view details

Roberto Prevato

commit sha cc4b0a0bb3d7b6153e969b8476b65291aad226a9

queue length and pending consumers count, pool stats

view details

Roberto Prevato

commit sha 94355438dae476a346d2ac0335ed9b485121d23f

makes more properties private on the Pool class

view details

Roberto Prevato

commit sha 5b58d0d4708f74f827a8e4573ae76a0c1d1b8c8e

tests for pool stats

view details

push time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

 export class PoolConnectionProxy {     connection._setProxy(this);   } -  public get connection(): AwaitConnection {+  get connection(): AwaitConnection {

I handled this a bit differently than how it is done in Python. In the Python driver, the metaclass PoolConnectionProxyMeta is used to copy all methods from the connection class into the proxy class. acquire returns a proxy, release accepts a proxy (this is the same in both drivers). So if the user acquires a proxy, it will behave like a connection object while not exposing the underlying connection.

I know this kind of dynamism is possible in JavaScript and TypeScript, but I wasn't sure whether this is a common approach when working with TS. I kept the proxy class simpler, and exposed the connection.

The difference in practice is:

    async def execute(self, query):
        async with self.acquire() as con: # <-- here "con" is a connection proxy
            return await con.execute(query)
  async run<T>(
    action: (connection: AwaitConnection) => Promise<T>
  ): Promise<T> {
    const proxy = await this.acquire();

    try {
      return await action(proxy.connection);  // <-- here the actual connection is used to execute
    } finally {
      await this.release(proxy);
    }
  }

  async execute(query: string): Promise<void> {
    return await this.run(async (connection) => {
      return await connection.execute(query);
    });
  }

I wanted to speak with you about this detail, then I forgot while asking about other details. If you prefer to make the connection private, then I propose to add the 5 methods to the proxy class (fetchOne, fetchOneJSON, fetchAll, fetchAllJSON, execute).

RobertoPrevato

comment created time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

 Pool         Release a previously acquired connection proxy, to return it to the         pool. -    .. js:method:: acquireAndExecute<T>(action: func)+    .. js:method:: run<T>(action: func)

You're right, I didn't include it in the documentation. In this context, is the same value returned by the func that receives a connection.

RobertoPrevato

comment created time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 5753720b6476064bd5f6c8ef704495aa23ee54ac

corrects first group of the last comments

view details

push time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2020-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {+  AwaitConnection,+  QueryArgs,+  NodeCallback,+  IConnection,+  CallbackConnectionBase,+  Connection,+} from "./client";+import {ConnectConfig} from "./con_utils";+import * as errors from "./errors";+import {LifoQueue} from "./queues";+import {Set} from "./datatypes/set";++export class Deferred<T> {+  private _promise: Promise<T>;+  private _resolve?: (value?: T | PromiseLike<T> | undefined) => void;+  private _reject?: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new Error("The deferred is not resolved.");+    }+    return this._result;+  }++  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {+    while (!this._resolve) {+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._resolve(value);+  }++  async setFailed(reason?: any): Promise<void> {+    while (!this._reject) {+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._reject(reason);+  }++  constructor() {+    this._done = false;+    this._reject = undefined;+    this._resolve = undefined;++    this._promise = new Promise((resolve, reject) => {+      this._reject = reject;++      this._resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++class PoolConnectionHolder {+  private _pool: Pool;+  private _proxy: PoolConnectionProxy | null;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _connection: AwaitConnection | null;+  private _generation: number | null;+  private _inUse: Deferred<void> | null;++  constructor(+    pool: Pool,+    onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>,+    onRelease?: (proxy: PoolConnectionProxy) => Promise<void>+  ) {+    this._pool = pool;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._connection = null;+    this._proxy = null;+    this._inUse = null;+    this._generation = null;+  }++  public get connection(): AwaitConnection | null {+    return this._connection;+  }++  public get pool(): Pool {+    return this._pool;+  }++  private getConnectionOrThrow(): AwaitConnection {+    if (this._connection === null) {+      throw new TypeError("The connection is not open");+    }+    return this._connection;+  }++  terminate(): void {+    if (this._connection !== null) {+      this._connection.close();+    }+  }++  async connect(): Promise<void> {+    if (this._connection !== null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.connect() called while another " ++          "connection already exists"+      );+    }++    this._connection = await this._pool.getNewConnection();+    this._generation = this._pool.generation;+  }++  async acquire(): Promise<PoolConnectionProxy> {+    if (this._connection === null || this._connection.isClosed()) {+      this._connection = null;+      await this.connect();+    } else if (this._generation !== this._pool.generation) {+      // Connections have been expired, re-connect the holder.+      await this._connection.close();+      this._connection = null;+      await this.connect();+    }++    const proxy = new PoolConnectionProxy(this, this.getConnectionOrThrow());+    this._proxy = proxy;++    if (this._onAcquire) {+      try {+        await this._onAcquire(proxy);+      } catch (error) {+        // If a user-defined `onAcquire` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.++        // Use `close()` to close the connection gracefully.+        // An exception in `onAcquire` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _cleanup().++        await this._connection?.close();+        throw error;+      }+    }++    this._inUse = new Deferred<void>();+    return proxy;+  }++  async release(): Promise<void> {+    if (this._inUse === null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.release() called on " ++          "a free connection holder"+      );+    }++    if (this._connection?.isClosed()) {+      // When closing, pool connections perform the necessary+      // cleanup, so we don't have to do anything else here.+      return;+    }++    if (this._generation !== this._pool.generation) {+      // The connection has expired because it belongs to+      // an older generation (Pool.expire_connections() has+      // been called).+      await this._connection?.close();+      return;+    }++    if (this._onRelease && this._proxy) {+      try {+        await this._onRelease(this._proxy);+      } catch (error) {+        // If a user-defined `onRelease` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await this._connection?.close();+        // Use `close()` to close the connection gracefully.+        // An exception in `setup` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via releaseOnClose().+        throw error;+      }+    }++    // Free this connection holder and invalidate the+    // connection proxy.+    await this._release();+  }++  async waitUntilReleased(): Promise<void> {+    if (this._inUse === null) {+      return;+    }+    await this._inUse.promise;+  }++  async close(): Promise<void> {+    if (this._connection !== null) {+      // AsyncIOConnection.aclose() will call releaseOnClose() to+      // finish holder cleanup.+      await this._connection.close();+    }+  }++  /** @internal */+  _releaseOnClose(): void {+    this._release();+    this._connection = null;+  }++  private async _release(): Promise<void> {+    // Release this connection holder.+    if (this._inUse === null) {+      // The holder is not checked out.+      return;+    }++    if (!this._inUse.done) {+      await this._inUse.setResult();+    }++    this._inUse = null;++    // Deinitialize the connection proxy.  All subsequent+    // operations on it will fail.+    if (this._proxy !== null) {+      this._proxy.detach();+      this._proxy = null;+    }++    // Put ourselves back to the pool queue.+    this._pool.queue.push(this);+  }+}++export class PoolConnectionProxy {+  private _holder: PoolConnectionHolder;+  private _connection: AwaitConnection | null;++  constructor(holder: PoolConnectionHolder, connection: AwaitConnection) {+    this._holder = holder;+    this._connection = connection;++    // Registers a callback that will be invoked when the+    // when the connection is closed, release it+    connection._setProxy(this);+  }++  public get connection(): AwaitConnection {+    if (this._connection === null) {+      throw new errors.InterfaceError("The proxy is detached");+    }+    return this._connection;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * from a connection.+   */+  public get detached(): boolean {+    return this._connection === null;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * or the underlying connection is closed.+   */+  isClosed(): boolean {+    return this._connection === null || this._connection.isClosed();+  }++  /** @internal */+  onConnectionClose(): void {+    this._holder._releaseOnClose();+  }++  /** @internal */+  public get holder(): PoolConnectionHolder {+    return this._holder;+  }++  /** @internal */+  detach(): AwaitConnection | null {+    if (this._connection === null) {+      return null;+    }++    const connection = this._connection;+    this._connection = null;+    connection._setProxy(null);+    return connection;+  }+}++const DefaultMinPoolSize = 0;+const DefaultMaxPoolSize = 100;++export interface PoolOptions {+  connectOptions?: ConnectConfig | null;+  minSize?: number;+  maxSize?: number;+  onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  onConnect?: (connection: AwaitConnection) => Promise<void>;+  connectionFactory?: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+}++/**+ * A connection pool.+ *+ * Connection pool can be used to manage a set of connections to the database.+ * Connections are first acquired from the pool, then used, and then released+ * back to the pool. Once a connection is released, it's reset to close all+ * open cursors and other resources *except* prepared statements.+ * Pools are created by calling :func:`~edgedb.pool.create`.+ */+export class Pool implements IConnection {+  private _closed: boolean;+  private _closing: boolean;+  private _queue: LifoQueue<PoolConnectionHolder>;+  private _holders: PoolConnectionHolder[];+  private _initialized: boolean;+  private _initializing: boolean;+  private _minSize: number;+  private _maxSize: number;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onConnect?: (connection: AwaitConnection) => Promise<void>;+  private _connectionFactory: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+  private _generation: number;+  private _connectOptions?: ConnectConfig | null;++  public get initialized(): boolean {+    return this._initialized;+  }++  /** @internal */+  public get generation(): number {+    return this._generation;+  }++  /** @internal */+  public get holders(): PoolConnectionHolder[] {+    return this._holders;+  }

Now I recall one of the reasons why I left holders and holder public: the ConnectionProxy is exposed by onAcquire and onRelease callbacks, and the proxy's holder is read inside a pool, to ensure that the proxy belongs to the pool where it's being released.

  1. I used public getter because I wanted certain properties to be read-only, and didn't take into consideration something like:
private __something: T;  // *really* private

/** @internal */
get _something(): T {
  return this.__something;
}
  1. An alternative would be to replace the get-only property with a method like:
/** @internal */
_getHolder() {
  return this._holder;
}
RobertoPrevato

comment created time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2020-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {+  AwaitConnection,+  QueryArgs,+  NodeCallback,+  IConnection,+  CallbackConnectionBase,+  Connection,+} from "./client";+import {ConnectConfig} from "./con_utils";+import * as errors from "./errors";+import {LifoQueue} from "./queues";+import {Set} from "./datatypes/set";++export class Deferred<T> {+  private _promise: Promise<T>;+  private _resolve?: (value?: T | PromiseLike<T> | undefined) => void;+  private _reject?: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new Error("The deferred is not resolved.");+    }+    return this._result;+  }++  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {+    while (!this._resolve) {+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._resolve(value);+  }++  async setFailed(reason?: any): Promise<void> {+    while (!this._reject) {+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._reject(reason);+  }++  constructor() {+    this._done = false;+    this._reject = undefined;+    this._resolve = undefined;++    this._promise = new Promise((resolve, reject) => {+      this._reject = reject;++      this._resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++class PoolConnectionHolder {+  private _pool: Pool;+  private _proxy: PoolConnectionProxy | null;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _connection: AwaitConnection | null;+  private _generation: number | null;+  private _inUse: Deferred<void> | null;++  constructor(+    pool: Pool,+    onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>,+    onRelease?: (proxy: PoolConnectionProxy) => Promise<void>+  ) {+    this._pool = pool;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._connection = null;+    this._proxy = null;+    this._inUse = null;+    this._generation = null;+  }++  public get connection(): AwaitConnection | null {+    return this._connection;+  }++  public get pool(): Pool {+    return this._pool;+  }++  private getConnectionOrThrow(): AwaitConnection {+    if (this._connection === null) {+      throw new TypeError("The connection is not open");+    }+    return this._connection;+  }++  terminate(): void {+    if (this._connection !== null) {+      this._connection.close();+    }+  }++  async connect(): Promise<void> {+    if (this._connection !== null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.connect() called while another " ++          "connection already exists"+      );+    }++    this._connection = await this._pool.getNewConnection();+    this._generation = this._pool.generation;+  }++  async acquire(): Promise<PoolConnectionProxy> {+    if (this._connection === null || this._connection.isClosed()) {+      this._connection = null;+      await this.connect();+    } else if (this._generation !== this._pool.generation) {+      // Connections have been expired, re-connect the holder.+      await this._connection.close();+      this._connection = null;+      await this.connect();+    }++    const proxy = new PoolConnectionProxy(this, this.getConnectionOrThrow());+    this._proxy = proxy;++    if (this._onAcquire) {+      try {+        await this._onAcquire(proxy);+      } catch (error) {+        // If a user-defined `onAcquire` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.++        // Use `close()` to close the connection gracefully.+        // An exception in `onAcquire` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _cleanup().++        await this._connection?.close();+        throw error;+      }+    }++    this._inUse = new Deferred<void>();+    return proxy;+  }++  async release(): Promise<void> {+    if (this._inUse === null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.release() called on " ++          "a free connection holder"+      );+    }++    if (this._connection?.isClosed()) {+      // When closing, pool connections perform the necessary+      // cleanup, so we don't have to do anything else here.+      return;+    }++    if (this._generation !== this._pool.generation) {+      // The connection has expired because it belongs to+      // an older generation (Pool.expire_connections() has+      // been called).+      await this._connection?.close();+      return;+    }++    if (this._onRelease && this._proxy) {+      try {+        await this._onRelease(this._proxy);+      } catch (error) {+        // If a user-defined `onRelease` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await this._connection?.close();+        // Use `close()` to close the connection gracefully.+        // An exception in `setup` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via releaseOnClose().+        throw error;+      }+    }++    // Free this connection holder and invalidate the+    // connection proxy.+    await this._release();+  }++  async waitUntilReleased(): Promise<void> {+    if (this._inUse === null) {+      return;+    }+    await this._inUse.promise;+  }++  async close(): Promise<void> {+    if (this._connection !== null) {+      // AsyncIOConnection.aclose() will call releaseOnClose() to+      // finish holder cleanup.+      await this._connection.close();+    }+  }++  /** @internal */+  _releaseOnClose(): void {+    this._release();+    this._connection = null;+  }++  private async _release(): Promise<void> {+    // Release this connection holder.+    if (this._inUse === null) {+      // The holder is not checked out.+      return;+    }++    if (!this._inUse.done) {+      await this._inUse.setResult();+    }++    this._inUse = null;++    // Deinitialize the connection proxy.  All subsequent+    // operations on it will fail.+    if (this._proxy !== null) {+      this._proxy.detach();+      this._proxy = null;+    }++    // Put ourselves back to the pool queue.+    this._pool.queue.push(this);+  }+}++export class PoolConnectionProxy {+  private _holder: PoolConnectionHolder;+  private _connection: AwaitConnection | null;++  constructor(holder: PoolConnectionHolder, connection: AwaitConnection) {+    this._holder = holder;+    this._connection = connection;++    // Registers a callback that will be invoked when the+    // when the connection is closed, release it+    connection._setProxy(this);+  }++  public get connection(): AwaitConnection {+    if (this._connection === null) {+      throw new errors.InterfaceError("The proxy is detached");+    }+    return this._connection;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * from a connection.+   */+  public get detached(): boolean {+    return this._connection === null;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * or the underlying connection is closed.+   */+  isClosed(): boolean {+    return this._connection === null || this._connection.isClosed();+  }++  /** @internal */+  onConnectionClose(): void {+    this._holder._releaseOnClose();+  }++  /** @internal */+  public get holder(): PoolConnectionHolder {+    return this._holder;+  }++  /** @internal */+  detach(): AwaitConnection | null {+    if (this._connection === null) {+      return null;+    }++    const connection = this._connection;+    this._connection = null;+    connection._setProxy(null);+    return connection;+  }+}++const DefaultMinPoolSize = 0;+const DefaultMaxPoolSize = 100;++export interface PoolOptions {+  connectOptions?: ConnectConfig | null;+  minSize?: number;+  maxSize?: number;+  onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  onConnect?: (connection: AwaitConnection) => Promise<void>;+  connectionFactory?: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+}++/**+ * A connection pool.+ *+ * Connection pool can be used to manage a set of connections to the database.+ * Connections are first acquired from the pool, then used, and then released+ * back to the pool. Once a connection is released, it's reset to close all+ * open cursors and other resources *except* prepared statements.+ * Pools are created by calling :func:`~edgedb.pool.create`.+ */+export class Pool implements IConnection {+  private _closed: boolean;+  private _closing: boolean;+  private _queue: LifoQueue<PoolConnectionHolder>;+  private _holders: PoolConnectionHolder[];+  private _initialized: boolean;+  private _initializing: boolean;+  private _minSize: number;+  private _maxSize: number;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onConnect?: (connection: AwaitConnection) => Promise<void>;+  private _connectionFactory: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+  private _generation: number;+  private _connectOptions?: ConnectConfig | null;++  public get initialized(): boolean {+    return this._initialized;+  }++  /** @internal */+  public get generation(): number {+    return this._generation;+  }++  /** @internal */+  public get holders(): PoolConnectionHolder[] {+    return this._holders;+  }

The only way I was using holders was to verify that a pool is created with a minSize of open connections. This seems a valid use case to me, too.

describe("pool.initialize: creates minSize count of connections", () => {
  each([0, 1, 5, 10, 20]).it(
    "when minSize is '%s'",
    async (minSize) => {
      const maxSize = minSize + 50;
      const pool = await Pool.create({
        connectOptions: getConnectOptions(),
        minSize,
        maxSize,
      });

      expect(pool.queue).toHaveLength(maxSize);

      const itemsWithOpenConnection = pool.holders.filter(
        (holder) => holder.connection !== null
      );

      expect(itemsWithOpenConnection).toHaveLength(minSize);
RobertoPrevato

comment created time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

 Connection             The *callback* to be invoked after closing the connection.  -    .. js:method:: wrap(conn: AwaitConnection): Connection-        :staticmethod:--        Convert an :js:class:`AwaitConnection` into :js:class:`Connection`.- .. _BigInt:     https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt+++.. _edgedb-js-api-pool:++Pool+==========++.. js:function:: createPool(options)+                 createPool(options, callback)++    Create a connection pool to an EdgeDB server.++    :param options: Connection pool parameters object.++    :param ConnectConfig options.connectOptions:+        Connection parameters object, used when establishing new connections.+        Refer to the documentation at :ref:`edgedb-js-api-connection`.++    :param number options.minSize:+        The minimum number of connections initialized by the connection pool.+        If not specified, this value is by default 0: the first connection is+        created when required.++    :param number options.maxSize:+        The maximum number of connections created by the connection pool.+        If not specified, this value is by default 100.++    :param func options.onAcquire:+        Optional callback, called when a connection is acquired.+        *(proxy: PoolConnectionProxy) => Promise<void>*++    :param func options.onRelease:+        Optional callback, called when a connection is released.+        *(proxy: PoolConnectionProxy) => Promise<void>*++    :param func options.onConnect:+        Optional callback, called when a new connection is created.+        *(connection: AwaitConnection) => Promise<void>*++    :param func options.connectionFactory:+        Optional function, used to obtain a new connection. By default, the+        function is :js:func:`connect` *(options?: ConnectConfig) =>+        Promise<AwaitConnection>*++    :param callback:+        A callback function that will be invoked when the connection pool is+        ready. The callback function should be of the form ``function(error,+        pool)``. The *pool* is an instance of :js:class:`CallbackPool`.++    :returns:+        There are two ways of creating an EdgeDB connection pool: a+        Promise-based approach and a callback-based. When a *callback*+        argument is provided, the function does not return anything and instead+        uses the *callback* with an instance of :js:class:`CallbackPool`.+        Otherwise, a ``Promise`` of an :js:class:`Pool` is returned.++.. js:class:: Pool++    A connection pool is used to manage a set of connections to a database.+    Since opening connections is an expensive operation, connection pools are+    used to maintain and reuse connections, enhancing the performance of+    database interactions.++    Pools can be created using the method ``createPool``:++    .. code-block:: js++        const edgedb = require("edgedb");++        async function main() {+            const pool = await edgedb.createPool({+                connectOptions: {+                    user: "edgedb",+                    host: "127.0.0.1",+                },+            });++            await pool.initialize();

Yes, it's a mistake.

RobertoPrevato

comment created time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

 Connection             The *callback* to be invoked after closing the connection.  -    .. js:method:: wrap(conn: AwaitConnection): Connection-        :staticmethod:--        Convert an :js:class:`AwaitConnection` into :js:class:`Connection`.- .. _BigInt:     https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt+++.. _edgedb-js-api-pool:++Pool+==========++.. js:function:: createPool(options)+                 createPool(options, callback)++    Create a connection pool to an EdgeDB server.

I added an example to usage.rst file. Should I add the same example to connection.rst?

RobertoPrevato

comment created time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2020-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {+  AwaitConnection,+  QueryArgs,+  NodeCallback,+  IConnection,+  CallbackConnectionBase,+  Connection,+} from "./client";+import {ConnectConfig} from "./con_utils";+import * as errors from "./errors";+import {LifoQueue} from "./queues";+import {Set} from "./datatypes/set";++export class Deferred<T> {+  private _promise: Promise<T>;+  private _resolve?: (value?: T | PromiseLike<T> | undefined) => void;+  private _reject?: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new Error("The deferred is not resolved.");+    }+    return this._result;+  }++  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {+    while (!this._resolve) {+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._resolve(value);+  }++  async setFailed(reason?: any): Promise<void> {+    while (!this._reject) {+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._reject(reason);+  }++  constructor() {+    this._done = false;+    this._reject = undefined;+    this._resolve = undefined;++    this._promise = new Promise((resolve, reject) => {+      this._reject = reject;++      this._resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++class PoolConnectionHolder {+  private _pool: Pool;+  private _proxy: PoolConnectionProxy | null;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _connection: AwaitConnection | null;+  private _generation: number | null;+  private _inUse: Deferred<void> | null;++  constructor(+    pool: Pool,+    onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>,+    onRelease?: (proxy: PoolConnectionProxy) => Promise<void>+  ) {+    this._pool = pool;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._connection = null;+    this._proxy = null;+    this._inUse = null;+    this._generation = null;+  }++  public get connection(): AwaitConnection | null {+    return this._connection;+  }++  public get pool(): Pool {+    return this._pool;+  }++  private getConnectionOrThrow(): AwaitConnection {+    if (this._connection === null) {+      throw new TypeError("The connection is not open");+    }+    return this._connection;+  }++  terminate(): void {+    if (this._connection !== null) {+      this._connection.close();+    }+  }++  async connect(): Promise<void> {+    if (this._connection !== null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.connect() called while another " ++          "connection already exists"+      );+    }++    this._connection = await this._pool.getNewConnection();+    this._generation = this._pool.generation;+  }++  async acquire(): Promise<PoolConnectionProxy> {+    if (this._connection === null || this._connection.isClosed()) {+      this._connection = null;+      await this.connect();+    } else if (this._generation !== this._pool.generation) {+      // Connections have been expired, re-connect the holder.+      await this._connection.close();+      this._connection = null;+      await this.connect();+    }++    const proxy = new PoolConnectionProxy(this, this.getConnectionOrThrow());+    this._proxy = proxy;++    if (this._onAcquire) {+      try {+        await this._onAcquire(proxy);+      } catch (error) {+        // If a user-defined `onAcquire` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.++        // Use `close()` to close the connection gracefully.+        // An exception in `onAcquire` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _cleanup().++        await this._connection?.close();+        throw error;+      }+    }++    this._inUse = new Deferred<void>();+    return proxy;+  }++  async release(): Promise<void> {+    if (this._inUse === null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.release() called on " ++          "a free connection holder"+      );+    }++    if (this._connection?.isClosed()) {+      // When closing, pool connections perform the necessary+      // cleanup, so we don't have to do anything else here.+      return;+    }++    if (this._generation !== this._pool.generation) {+      // The connection has expired because it belongs to+      // an older generation (Pool.expire_connections() has+      // been called).+      await this._connection?.close();+      return;+    }++    if (this._onRelease && this._proxy) {+      try {+        await this._onRelease(this._proxy);+      } catch (error) {+        // If a user-defined `onRelease` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await this._connection?.close();+        // Use `close()` to close the connection gracefully.+        // An exception in `setup` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via releaseOnClose().+        throw error;+      }+    }++    // Free this connection holder and invalidate the+    // connection proxy.+    await this._release();+  }++  async waitUntilReleased(): Promise<void> {+    if (this._inUse === null) {+      return;+    }+    await this._inUse.promise;+  }++  async close(): Promise<void> {+    if (this._connection !== null) {+      // AsyncIOConnection.aclose() will call releaseOnClose() to+      // finish holder cleanup.+      await this._connection.close();+    }+  }++  /** @internal */+  _releaseOnClose(): void {+    this._release();+    this._connection = null;+  }++  private async _release(): Promise<void> {+    // Release this connection holder.+    if (this._inUse === null) {+      // The holder is not checked out.+      return;+    }++    if (!this._inUse.done) {+      await this._inUse.setResult();+    }++    this._inUse = null;++    // Deinitialize the connection proxy.  All subsequent+    // operations on it will fail.+    if (this._proxy !== null) {+      this._proxy.detach();+      this._proxy = null;+    }++    // Put ourselves back to the pool queue.+    this._pool.queue.push(this);+  }+}++export class PoolConnectionProxy {+  private _holder: PoolConnectionHolder;+  private _connection: AwaitConnection | null;++  constructor(holder: PoolConnectionHolder, connection: AwaitConnection) {+    this._holder = holder;+    this._connection = connection;++    // Registers a callback that will be invoked when the+    // when the connection is closed, release it+    connection._setProxy(this);+  }++  public get connection(): AwaitConnection {+    if (this._connection === null) {+      throw new errors.InterfaceError("The proxy is detached");+    }+    return this._connection;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * from a connection.+   */+  public get detached(): boolean {+    return this._connection === null;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * or the underlying connection is closed.+   */+  isClosed(): boolean {+    return this._connection === null || this._connection.isClosed();+  }++  /** @internal */+  onConnectionClose(): void {+    this._holder._releaseOnClose();+  }++  /** @internal */+  public get holder(): PoolConnectionHolder {+    return this._holder;+  }++  /** @internal */+  detach(): AwaitConnection | null {+    if (this._connection === null) {+      return null;+    }++    const connection = this._connection;+    this._connection = null;+    connection._setProxy(null);+    return connection;+  }+}++const DefaultMinPoolSize = 0;+const DefaultMaxPoolSize = 100;++export interface PoolOptions {+  connectOptions?: ConnectConfig | null;+  minSize?: number;+  maxSize?: number;+  onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  onConnect?: (connection: AwaitConnection) => Promise<void>;+  connectionFactory?: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+}++/**+ * A connection pool.+ *+ * Connection pool can be used to manage a set of connections to the database.+ * Connections are first acquired from the pool, then used, and then released+ * back to the pool. Once a connection is released, it's reset to close all+ * open cursors and other resources *except* prepared statements.+ * Pools are created by calling :func:`~edgedb.pool.create`.+ */+export class Pool implements IConnection {+  private _closed: boolean;+  private _closing: boolean;+  private _queue: LifoQueue<PoolConnectionHolder>;+  private _holders: PoolConnectionHolder[];+  private _initialized: boolean;+  private _initializing: boolean;+  private _minSize: number;+  private _maxSize: number;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onConnect?: (connection: AwaitConnection) => Promise<void>;+  private _connectionFactory: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+  private _generation: number;+  private _connectOptions?: ConnectConfig | null;++  public get initialized(): boolean {+    return this._initialized;+  }++  /** @internal */+  public get generation(): number {+    return this._generation;+  }++  /** @internal */+  public get holders(): PoolConnectionHolder[] {+    return this._holders;+  }

On a second thought, having the queue public might have a good use case: consider if somebody implements telemetry and wants to monitor the size of the queue. And generation is used by the ConnectionHolder, so it makes sense to leave it as public get only property (public is actually redundant since things are public by default anyway in TS)

RobertoPrevato

comment created time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2020-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {+  AwaitConnection,+  QueryArgs,+  NodeCallback,+  IConnection,+  CallbackConnectionBase,+  Connection,+} from "./client";+import {ConnectConfig} from "./con_utils";+import * as errors from "./errors";+import {LifoQueue} from "./queues";+import {Set} from "./datatypes/set";++export class Deferred<T> {+  private _promise: Promise<T>;+  private _resolve?: (value?: T | PromiseLike<T> | undefined) => void;+  private _reject?: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new Error("The deferred is not resolved.");+    }+    return this._result;+  }++  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {+    while (!this._resolve) {+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._resolve(value);+  }++  async setFailed(reason?: any): Promise<void> {+    while (!this._reject) {+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._reject(reason);+  }++  constructor() {+    this._done = false;+    this._reject = undefined;+    this._resolve = undefined;++    this._promise = new Promise((resolve, reject) => {+      this._reject = reject;++      this._resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++class PoolConnectionHolder {+  private _pool: Pool;+  private _proxy: PoolConnectionProxy | null;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _connection: AwaitConnection | null;+  private _generation: number | null;+  private _inUse: Deferred<void> | null;++  constructor(+    pool: Pool,+    onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>,+    onRelease?: (proxy: PoolConnectionProxy) => Promise<void>+  ) {+    this._pool = pool;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._connection = null;+    this._proxy = null;+    this._inUse = null;+    this._generation = null;+  }++  public get connection(): AwaitConnection | null {+    return this._connection;+  }++  public get pool(): Pool {+    return this._pool;+  }++  private getConnectionOrThrow(): AwaitConnection {+    if (this._connection === null) {+      throw new TypeError("The connection is not open");+    }+    return this._connection;+  }++  terminate(): void {+    if (this._connection !== null) {+      this._connection.close();+    }+  }++  async connect(): Promise<void> {+    if (this._connection !== null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.connect() called while another " ++          "connection already exists"+      );+    }++    this._connection = await this._pool.getNewConnection();+    this._generation = this._pool.generation;+  }++  async acquire(): Promise<PoolConnectionProxy> {+    if (this._connection === null || this._connection.isClosed()) {+      this._connection = null;+      await this.connect();+    } else if (this._generation !== this._pool.generation) {+      // Connections have been expired, re-connect the holder.+      await this._connection.close();+      this._connection = null;+      await this.connect();+    }++    const proxy = new PoolConnectionProxy(this, this.getConnectionOrThrow());+    this._proxy = proxy;++    if (this._onAcquire) {+      try {+        await this._onAcquire(proxy);+      } catch (error) {+        // If a user-defined `onAcquire` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.++        // Use `close()` to close the connection gracefully.+        // An exception in `onAcquire` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _cleanup().++        await this._connection?.close();+        throw error;+      }+    }++    this._inUse = new Deferred<void>();+    return proxy;+  }++  async release(): Promise<void> {+    if (this._inUse === null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.release() called on " ++          "a free connection holder"+      );+    }++    if (this._connection?.isClosed()) {+      // When closing, pool connections perform the necessary+      // cleanup, so we don't have to do anything else here.+      return;+    }++    if (this._generation !== this._pool.generation) {+      // The connection has expired because it belongs to+      // an older generation (Pool.expire_connections() has+      // been called).+      await this._connection?.close();+      return;+    }++    if (this._onRelease && this._proxy) {+      try {+        await this._onRelease(this._proxy);+      } catch (error) {+        // If a user-defined `onRelease` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await this._connection?.close();+        // Use `close()` to close the connection gracefully.+        // An exception in `setup` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via releaseOnClose().+        throw error;+      }+    }++    // Free this connection holder and invalidate the+    // connection proxy.+    await this._release();+  }++  async waitUntilReleased(): Promise<void> {+    if (this._inUse === null) {+      return;+    }+    await this._inUse.promise;+  }++  async close(): Promise<void> {+    if (this._connection !== null) {+      // AsyncIOConnection.aclose() will call releaseOnClose() to+      // finish holder cleanup.+      await this._connection.close();+    }+  }++  /** @internal */+  _releaseOnClose(): void {+    this._release();+    this._connection = null;+  }++  private async _release(): Promise<void> {+    // Release this connection holder.+    if (this._inUse === null) {+      // The holder is not checked out.+      return;+    }++    if (!this._inUse.done) {+      await this._inUse.setResult();+    }++    this._inUse = null;++    // Deinitialize the connection proxy.  All subsequent+    // operations on it will fail.+    if (this._proxy !== null) {+      this._proxy.detach();+      this._proxy = null;+    }++    // Put ourselves back to the pool queue.+    this._pool.queue.push(this);+  }+}++export class PoolConnectionProxy {+  private _holder: PoolConnectionHolder;+  private _connection: AwaitConnection | null;++  constructor(holder: PoolConnectionHolder, connection: AwaitConnection) {+    this._holder = holder;+    this._connection = connection;++    // Registers a callback that will be invoked when the+    // when the connection is closed, release it+    connection._setProxy(this);+  }++  public get connection(): AwaitConnection {+    if (this._connection === null) {+      throw new errors.InterfaceError("The proxy is detached");+    }+    return this._connection;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * from a connection.+   */+  public get detached(): boolean {+    return this._connection === null;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * or the underlying connection is closed.+   */+  isClosed(): boolean {+    return this._connection === null || this._connection.isClosed();+  }++  /** @internal */+  onConnectionClose(): void {+    this._holder._releaseOnClose();+  }++  /** @internal */+  public get holder(): PoolConnectionHolder {+    return this._holder;+  }++  /** @internal */+  detach(): AwaitConnection | null {+    if (this._connection === null) {+      return null;+    }++    const connection = this._connection;+    this._connection = null;+    connection._setProxy(null);+    return connection;+  }+}++const DefaultMinPoolSize = 0;+const DefaultMaxPoolSize = 100;++export interface PoolOptions {+  connectOptions?: ConnectConfig | null;+  minSize?: number;+  maxSize?: number;+  onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  onConnect?: (connection: AwaitConnection) => Promise<void>;+  connectionFactory?: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+}++/**+ * A connection pool.+ *+ * Connection pool can be used to manage a set of connections to the database.+ * Connections are first acquired from the pool, then used, and then released+ * back to the pool. Once a connection is released, it's reset to close all+ * open cursors and other resources *except* prepared statements.+ * Pools are created by calling :func:`~edgedb.pool.create`.+ */+export class Pool implements IConnection {+  private _closed: boolean;+  private _closing: boolean;+  private _queue: LifoQueue<PoolConnectionHolder>;+  private _holders: PoolConnectionHolder[];+  private _initialized: boolean;+  private _initializing: boolean;+  private _minSize: number;+  private _maxSize: number;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onConnect?: (connection: AwaitConnection) => Promise<void>;+  private _connectionFactory: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+  private _generation: number;+  private _connectOptions?: ConnectConfig | null;++  public get initialized(): boolean {+    return this._initialized;+  }++  /** @internal */+  public get generation(): number {+    return this._generation;+  }++  /** @internal */+  public get holders(): PoolConnectionHolder[] {+    return this._holders;+  }

You are right, we don't need these. I have the tendency of leaving things public and open, because I think: "You never know what users will need to do" (I like to think that Python doesn't have private modifier for a similar reason). I will remove these public getters :)

RobertoPrevato

comment created time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 81976970f7e4ad3d201b4c07bfec8d290b596065

corrects a few typos: EDGEB_PORT instead of EDGEDB_PORT, and other minor improvements to docs

view details

push time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha b18247d355cd4feb65eca65708feb3185333a2a2

removes comment

view details

push time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 2ecf95c5f615124fc1f3efd6e0a5743d8fe81ef2

corrects comment in documentation example

view details

push time in a month

Pull request review commentedgedb/edgedb-js

Adds a connection pool

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2020-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {+  AwaitConnection,+  QueryArgs,+  NodeCallback,+  IConnection,+  CallbackConnectionBase,+} from "./client";+import {ConnectConfig} from "./con_utils";+import * as errors from "./errors";+import {LifoQueue} from "./queues";+import {Set} from "./datatypes/set";++export class Deferred<T> {+  private _promise: Promise<T>;+  private _resolve?: (value?: T | PromiseLike<T> | undefined) => void;+  private _reject?: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new Error("The deferred is not resolved.");+    }+    return this._result;+  }++  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {+    while (!this._resolve) {+      // this.resolve is configured inside a Promise callback+      // to avoid race conditions, here we await for a next cycle until the+      // reference is assigned+      // Normally, awaiting a timeout of 0 in a while loop would cause high+      // CPU usage, but in this case we expect the reference to be already+      // populated in almost all cases, and anyway in the next cycle+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._resolve(value);+  }++  async setFailed(reason?: any): Promise<void> {+    while (!this._reject) {+      // see the comment in ``setResult`` above+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this._reject(reason);+  }++  constructor() {+    this._done = false;+    this._reject = undefined;+    this._resolve = undefined;++    this._promise = new Promise((resolve, reject) => {+      this._reject = reject;++      this._resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++class PoolConnectionHolder {+  private _pool: Pool;+  private _proxy: PoolConnectionProxy | null;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _connection: AwaitConnection | null;+  private _generation: number | null;+  private _inUse: Deferred<void> | null;++  constructor(+    pool: Pool,+    onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>,+    onRelease?: (proxy: PoolConnectionProxy) => Promise<void>+  ) {+    this._pool = pool;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._connection = null;+    this._proxy = null;+    this._inUse = null;+    this._generation = null;+  }++  public get connection(): AwaitConnection | null {+    return this._connection;+  }++  public get pool(): Pool {+    return this._pool;+  }++  private getConnectionOrThrow(): AwaitConnection {+    if (this._connection === null) {+      throw new TypeError("The connection is not open");+    }+    return this._connection;+  }++  terminate(): void {+    if (this._connection !== null) {+      this._connection.close();+    }+  }++  async connect(): Promise<void> {+    if (this._connection !== null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.connect() called while another " ++          "connection already exists"+      );+    }++    this._connection = await this._pool.getNewConnection();+    this._generation = this._pool.generation;+  }++  async acquire(): Promise<PoolConnectionProxy> {+    if (this._connection === null || this._connection.isClosed()) {+      this._connection = null;+      await this.connect();+    } else if (this._generation !== this._pool.generation) {+      // Connections have been expired, re-connect the holder.+      await this._connection.close();+      this._connection = null;+      await this.connect();+    }++    const proxy = new PoolConnectionProxy(this, this.getConnectionOrThrow());+    this._proxy = proxy;++    if (this._onAcquire) {+      try {+        await this._onAcquire(proxy);+      } catch (error) {+        // If a user-defined `onAcquire` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.++        // Use `close()` to close the connection gracefully.+        // An exception in `onAcquire` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _cleanup().++        await this._connection?.close();+        throw error;+      }+    }++    this._inUse = new Deferred<void>();+    return proxy;+  }++  async release(): Promise<void> {+    if (this._inUse === null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.release() called on " ++          "a free connection holder"+      );+    }++    if (this._connection?.isClosed()) {+      // When closing, pool connections perform the necessary+      // cleanup, so we don't have to do anything else here.+      return;+    }++    if (this._generation !== this._pool.generation) {+      // The connection has expired because it belongs to+      // an older generation (Pool.expire_connections() has+      // been called).+      await this._connection?.close();+      return;+    }++    if (this._onRelease && this._proxy) {+      try {+        await this._onRelease(this._proxy);+      } catch (error) {+        // If a user-defined `onRelease` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await this._connection?.close();+        // Use `close()` to close the connection gracefully.+        // An exception in `setup` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via releaseOnClose().+        throw error;+      }+    }++    // Free this connection holder and invalidate the+    // connection proxy.+    await this._release();+  }++  async waitUntilReleased(): Promise<void> {+    if (this._inUse === null) {+      return;+    }+    await this._inUse.promise;+  }++  async close(): Promise<void> {+    if (this._connection !== null) {+      // AsyncIOConnection.aclose() will call releaseOnClose() to+      // finish holder cleanup.+      await this._connection.close();+    }+  }++  /** @internal */+  _releaseOnClose(): void {+    this._release();+    this._connection = null;+  }++  private async _release(): Promise<void> {+    // Release this connection holder.+    if (this._inUse === null) {+      // The holder is not checked out.+      return;+    }++    if (!this._inUse.done) {+      await this._inUse.setResult();+    }++    this._inUse = null;++    // Deinitialize the connection proxy.  All subsequent+    // operations on it will fail.+    if (this._proxy !== null) {+      this._proxy.detach();+      this._proxy = null;+    }++    // Put ourselves back to the pool queue.+    this._pool.queue.push(this);+  }+}++export class PoolConnectionProxy {+  private _holder: PoolConnectionHolder;+  private _connection: AwaitConnection | null;++  constructor(holder: PoolConnectionHolder, connection: AwaitConnection) {+    this._holder = holder;+    this._connection = connection;++    // Registers a callback that will be invoked when the+    // when the connection is closed, release it+    connection._setProxy(this);+  }++  public get connection(): AwaitConnection {+    if (this._connection === null) {+      throw new errors.InterfaceError("The proxy is detached");+    }+    return this._connection;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * from a connection.+   */+  public get detached(): boolean {+    return this._connection === null;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * or the underlying connection is closed.+   */+  isClosed(): boolean {+    return this._connection === null || this._connection.isClosed();+  }++  /** @internal */+  onConnectionClose(): void {+    this._holder._releaseOnClose();+  }++  public get holder(): PoolConnectionHolder {+    return this._holder;+  }++  detach(): AwaitConnection | null {+    if (this._connection === null) {+      return null;+    }++    const connection = this._connection;+    this._connection = null;+    connection._setProxy(null);+    return connection;+  }+}++const DefaultMinPoolSize = 0;+const DefaultMaxPoolSize = 100;++export interface PoolOptions {+  connectOptions?: ConnectConfig | null;+  minSize?: number;+  maxSize?: number;+  onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  onConnect?: (connection: AwaitConnection) => Promise<void>;+  connectionFactory?: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+}++/**+ * A connection pool.+ *+ * Connection pool can be used to manage a set of connections to the database.+ * Connections are first acquired from the pool, then used, and then released+ * back to the pool. Once a connection is released, it's reset to close all+ * open cursors and other resources *except* prepared statements.+ * Pools are created by calling :func:`~edgedb.pool.create`.+ */+export class Pool implements IConnection {+  private _closed: boolean;+  private _closing: boolean;+  private _queue: LifoQueue<PoolConnectionHolder>;+  private _holders: PoolConnectionHolder[];+  private _initialized: boolean;+  private _initializing: boolean;+  private _minSize: number;+  private _maxSize: number;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onConnect?: (connection: AwaitConnection) => Promise<void>;+  private _connectionFactory: (+    options?: ConnectConfig | null+  ) => Promise<AwaitConnection>;+  private _generation: number;+  private _connectOptions?: ConnectConfig | null;++  public get initialized(): boolean {+    return this._initialized;+  }++  /** @internal */+  public get generation(): number {+    return this._generation;+  }++  /** @internal */+  public get holders(): PoolConnectionHolder[] {+    return this._holders;+  }++  /** @internal */+  public get queue(): LifoQueue<PoolConnectionHolder> {+    return this._queue;+  }++  public get minSize(): number {+    return this._minSize;+  }++  public get maxSize(): number {+    return this._maxSize;+  }++  protected constructor(options: PoolOptions) {+    const {onAcquire, onRelease, onConnect, connectOptions} = options;+    const minSize =+      options.minSize === undefined ? DefaultMinPoolSize : options.minSize;+    const maxSize =+      options.maxSize === undefined ? DefaultMaxPoolSize : options.maxSize;++    this.validateSizeParameters(minSize, maxSize);++    this._queue = new LifoQueue<PoolConnectionHolder>();+    this._holders = [];+    this._initialized = false;+    this._initializing = false;+    this._minSize = minSize;+    this._maxSize = maxSize;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._onConnect = onConnect;+    this._closing = false;+    this._closed = false;+    this._generation = 0;+    this._connectOptions = connectOptions;+    this._connectionFactory = options.connectionFactory ?? connect;+  }++  static async create(options?: PoolOptions | null): Promise<Pool> {+    const pool = new Pool(options || {});+    await pool.initialize();+    return pool;+  }++  private validateSizeParameters(minSize: number, maxSize: number): void {+    if (maxSize <= 0) {+      throw new errors.InterfaceError(+        "maxSize is expected to be greater than zero"+      );+    }++    if (minSize < 0) {+      throw new errors.InterfaceError(+        "minSize is expected to be greater or equal to zero"+      );+    }++    if (minSize > maxSize) {+      throw new errors.InterfaceError("minSize is greater than maxSize");+    }+  }++  protected async initialize(): Promise<void> {+    // Ref: asyncio_pool.py _async__init__+    if (this._initialized) {+      return;+    }+    if (this._initializing) {+      throw new errors.InterfaceError("The pool is already being initialized");+    }+    if (this._closed) {+      throw new errors.InterfaceError("The pool is closed");+    }++    this._initializing = true;++    for (let i = 0; i < this._maxSize; i++) {+      const connectionHolder = new PoolConnectionHolder(+        this,+        this._onAcquire,+        this._onRelease+      );++      this._holders.push(connectionHolder);+      this._queue.push(connectionHolder);+    }++    try {+      await this._initializeHolders();+    } finally {+      this._initialized = true;+      this._initializing = false;+    }+  }++  /**+   * Expires all currently open connections.+   *+   * All currently open connections will get replaced on the+   * next Pool.acquire() call.+   */+  expireConnections(): void {+    // Expire all currently open connections+    this._generation += 1;+  }++  private async _initializeHolders(): Promise<void> {+    if (!this._minSize) {+      return;+    }++    // Since we use a LIFO queue, the first items in the queue will be+    // the last ones in `self._holders`.  We want to pre-connect the+    // first few connections in the queue, therefore we want to walk+    // `self._holders` in reverse.++    const tasks: Array<Promise<void>> = [];++    let count = 0;+    for (let i = this._holders.length - 1; i >= 0; i--) {+      if (count >= this._minSize) {+        break;+      }++      const connectionHolder = this._holders[i];+      tasks.push(connectionHolder.connect());+      count += 1;+    }++    await Promise.all(tasks);+  }++  /** @internal */+  async getNewConnection(): Promise<AwaitConnection> {+    const connection = await this._connectionFactory(this._connectOptions);++    if (this._onConnect) {+      try {+        await this._onConnect(connection);+      } catch (error) {+        // If a user-defined `connect` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await connection.close();+        throw error;+      }+    }++    return connection;+  }++  private _checkInit(): void {+    if (!this._initialized) {+      if (this._initializing) {+        throw new errors.InterfaceError(+          "The pool is being initialized, but not yet ready: " ++            "likely there is a race between creating a pool and " ++            "using it"+        );+      }++      throw new errors.InterfaceError(+        "The pool is not initialized. Call the ``initialize`` method " ++          "before using it."+      );+    }++    if (this._closed) {+      throw new errors.InterfaceError("The pool is closed");+    }+  }++  async acquire(): Promise<PoolConnectionProxy> {

I added a CallbackPool with the public methods from Pool, added tests, and documented it. I also added a createPool function counterpart of connect function. The only thing I needed to modify to work with generics and achieve DRY, was to remove the static method wrap and modify Connection constructor to be public. So instead of:

Connection.wrap(awaitConnection)

new Connection(awaitConnection)

In the process I corrected a few typos in the documentation (mystiped connection / throw / function).

RobertoPrevato

comment created time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha dbd8796d8b4ccc92474773d0d09185d6377c9692

docs corrections

view details

Roberto Prevato

commit sha 829750ecbfbfd8135081ae818e1ba369b3390220

Merge branch 'pool' of github.com:edgedb/edgedb-js into pool

view details

Roberto Prevato

commit sha a7559d0c2608434689546bd2b5bfe4d9cb971e22

Merge branch 'pool' of github.com:edgedb/edgedb-js into pool

view details

Roberto Prevato

commit sha aaf125469da1f0cd95bb4f33ad4c95b8ec8b5608

Merge branch 'pool' of github.com:edgedb/edgedb-js into pool

view details

Roberto Prevato

commit sha 05e1c199c9a44fdd6c08f82e62a4ff2390946582

documents CallbackPool class and createPool method

view details

Roberto Prevato

commit sha 0ad3047f16d060f31e0f12073d17dfecad035f07

removes public getters for minSize and maxSize

view details

push time in a month

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 001c5be39ffec12abedbb50d6e211fca06b046e4

adds first test for CallbackPool, changes index export to include createPool function

view details

Roberto Prevato

commit sha 06f8fd13c47588a8c62d7dacad463e955f6b96c8

tests and correction for CallbackPool and callback connection proxy

view details

push time in a month

pull request commentwg/wrk

Update Makefile

@bharrisau, I made the same PR in December 2018 https://github.com/wg/wrk/pull/368. It looks like the owner of this repo doesn't have time for this project ;(, (I understand and respect that, it's a pity because wrk is cool). Although lately I used wrk2 more..

bharrisau

comment created time in 2 months

pull request commentwg/wrk

Enclose $PATH in quotes

@cmello , it is also a duplicate of a PR I made in December 2018 :crying_cat_face: https://github.com/wg/wrk/pull/368

cmello

comment created time in 2 months

CommitCommentEvent

Pull request review commentedgedb/edgedb-js

Adds a connection pool

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2019-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {AwaitConnection, QueryArgs} from "./client";+import {ConnectConfig} from "./con_utils";+import * as errors from "./errors";+import {LifoQueue} from "./queues";+import {Set} from "./datatypes/set";++export class Deferred<T> {+  private _promise: Promise<T>;+  private resolve?: (value?: T | PromiseLike<T> | undefined) => void;+  private reject?: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new Error("The deferred is not resolved.");+    }+    return this._result;+  }++  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {+    while (!this.resolve) {+      // this.resolve is configured inside a Promise callback+      // to avoid race conditions, here we await for a next cycle until the+      // reference is assigned+      // Normally, awaiting a timeout of 0 in a while loop would cause high+      // CPU usage, but in this case we expect the reference to be already+      // populated in almost all cases, and anyway in the next cycle+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this.resolve(value);+  }++  async setFailed(reason?: any): Promise<void> {+    while (!this.reject) {+      // see the comment in ``setResult`` above+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this.reject(reason);+  }++  constructor() {+    this._done = false;+    this.reject = undefined;+    this.resolve = undefined;++    this._promise = new Promise((resolve, reject) => {+      this.reject = reject;++      this.resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++class PoolConnectionHolder {+  private _pool: Pool;+  private _proxy: PoolConnectionProxy | null;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _connection: AwaitConnection | null;+  private _generation: number | null;+  private _inUse: Deferred<void> | null;++  constructor(+    pool: Pool,+    onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>,+    onRelease?: (proxy: PoolConnectionProxy) => Promise<void>+  ) {+    this._pool = pool;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._connection = null;+    this._proxy = null;+    this._inUse = null;+    this._generation = null;+  }++  public get connection(): AwaitConnection | null {+    return this._connection;+  }++  public get pool(): Pool {+    return this._pool;+  }++  private getConnectionOrThrow(): AwaitConnection {+    if (this._connection === null) {+      throw new TypeError("The connection is not open");+    }+    return this._connection;+  }++  terminate(): void {+    if (this._connection !== null) {+      // TODO: synchronous close!+      this._connection.close();+    }+  }++  async connect(): Promise<void> {+    if (this._connection !== null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.connect() called while another " ++          "connection already exists"+      );+    }++    this._connection = await this._pool.getNewConnection();+    this._generation = this._pool.generation;+  }++  async acquire(): Promise<PoolConnectionProxy> {+    if (this._connection === null || this._connection.isClosed()) {+      this._connection = null;+      await this.connect();+    } else if (this._generation !== this._pool.generation) {+      // Connections have been expired, re-connect the holder.+      await this._connection.close();+      this._connection = null;+      await this.connect();+    }++    const proxy = new PoolConnectionProxy(this, this.getConnectionOrThrow());+    this._proxy = proxy;++    if (this._onAcquire) {+      try {+        await this._onAcquire(proxy);+      } catch (error) {+        // If a user-defined `onAcquire` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.++        // Use `close()` to close the connection gracefully.+        // An exception in `onAcquire` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _cleanup().++        await this._connection?.close();+        throw error;+      }+    }++    this._inUse = new Deferred<void>();+    return proxy;+  }++  async release(): Promise<void> {+    if (this._inUse === null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.release() called on " ++          "a free connection holder"+      );+    }++    if (this._connection?.isClosed()) {+      // When closing, pool connections perform the necessary+      // cleanup, so we don't have to do anything else here.+      return;+    }++    if (this._generation !== this._pool.generation) {+      // The connection has expired because it belongs to+      // an older generation (Pool.expire_connections() has+      // been called).+      await this._connection?.close();+      return;+    }++    if (this._onRelease && this._proxy) {+      try {+        await this._onRelease(this._proxy);+      } catch (error) {+        // If a user-defined `onRelease` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await this._connection?.close();+        // Use `close()` to close the connection gracefully.+        // An exception in `setup` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via releaseOnClose().+        throw error;+      }+    }++    // Free this connection holder and invalidate the+    // connection proxy.+    await this._release();+  }++  async waitUntilReleased(): Promise<void> {+    if (this._inUse === null) {+      return;+    }+    await this._inUse.promise;+  }++  async close(): Promise<void> {+    if (this._connection !== null) {+      // AsyncIOConnection.aclose() will call releaseOnClose() to+      // finish holder cleanup.+      await this._connection.close();+    }+  }++  releaseOnClose(): void {+    this._release();+    this._connection = null;+  }++  setProxy(proxy: PoolConnectionProxy): void {+    if (this._proxy !== null && proxy !== null) {+      // Should not happen unless there is a bug in `Pool`.+      throw new errors.InterfaceError(+        "internal client error: connection is already proxied"+      );+    }++    this._proxy = proxy;+  }++  private async _release(): Promise<void> {+    // Release this connection holder.+    if (this._inUse === null) {+      // The holder is not checked out.+      return;+    }++    if (!this._inUse.done) {+      await this._inUse.setResult();+    }++    this._inUse = null;++    // Deinitialize the connection proxy.  All subsequent+    // operations on it will fail.+    if (this._proxy !== null) {+      this._proxy.detach();+      this._proxy = null;+    }++    // Put ourselves back to the pool queue.+    this._pool.queue.push(this);+  }+}++export class PoolConnectionProxy {+  private _holder: PoolConnectionHolder;+  private _connection: AwaitConnection | null;++  constructor(holder: PoolConnectionHolder, connection: AwaitConnection) {+    this._holder = holder;+    this._connection = connection;++    // Registers a callback that will be invoked when the+    // when the connection is closed, release it+    connection.setProxy(this);+  }++  public get connection(): AwaitConnection {+    if (this._connection === null) {+      throw new errors.InterfaceError("The proxy is detached");+    }+    return this._connection;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * from a connection.+   */+  public get detached(): boolean {+    return this._connection === null;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * or the underlying connection is closed.+   */+  isClosed(): boolean {+    return this._connection === null || this._connection.isClosed();+  }++  /** @internal */+  onConnectionClose(): void {+    this._holder.releaseOnClose();+  }++  public get holder(): PoolConnectionHolder {+    return this._holder;+  }++  detach(): AwaitConnection | null {+    if (this._connection === null) {+      return null;+    }++    const connection = this._connection;+    this._connection = null;+    connection.setProxy(null);+    return connection;+  }+}++const DefaultMinPoolSize = 0;+const DefaultMaxPoolSize = 100;++export interface PoolOptions {+  connectOptions: ConnectConfig;+  minSize?: number;+  maxSize?: number;+  onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  onConnect?: (connection: AwaitConnection) => Promise<void>;+  connectionFactory?: (options?: ConnectConfig) => Promise<AwaitConnection>;+}++/**+ * A connection pool.+ *+ * Connection pool can be used to manage a set of connections to the database.+ * Connections are first acquired from the pool, then used, and then released+ * back to the pool. Once a connection is released, it's reset to close all+ * open cursors and other resources *except* prepared statements.+ * Pools are created by calling :func:`~edgedb.pool.create`.+ */+export class Pool {

@1st1 I propose this solution to implement the CallbackPool: instead of having code repetition of a second class that looks almost the same to the existing Connection in client.ts, I propose to add a common interface with methods that exist in both Connection and Pool object, I called it "IConnection"; then a generic class that expects T extends IConnection. https://github.com/edgedb/edgedb-js/pull/52/commits/d67f6aff6fb26c3828ea4f4b8c7c83ecab627aa6

This way we don't have code repetition. This approach would be a perfectly legitimate way of working with C#, and TypeScript generics look like ~1:1 replica of C# generics.

Classes defined this way can also be extended if desired, if the constructor is changed to be protected instead of private. For example:

export class CallbackPool extends CallbackConnectionBase<Pool> {

  constructor(pool: Pool) {
    super(pool);
  }

  example(callback: NodeCallback<null> | null = null): void {
    this.conn
      .close()
      .then((_value) => (callback ? callback(null, null) : null))
      .catch((error) => (callback ? callback(error, null) : null));
  }
}
RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 876ecb73881cc739d72c2d2b3bc146bffeee17ac

improves queue tests to use async await

view details

Roberto Prevato

commit sha 425e00f4609374f78542458f54e05069df376795

replaces InvalidValueError with InterfaceErro for wrong size params

view details

Roberto Prevato

commit sha d67f6aff6fb26c3828ea4f4b8c7c83ecab627aa6

adds CallbackPool implementation, defining a common IConnection interface and generic class

view details

push time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2019-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import {LifoQueue} from "../src/queues";++test("LifoQueue `get` awaits for items in the queue", async (done) => {+  let result = -1;+  const queue = new LifoQueue<number>();++  queue.get().then((value: number) => {+    expect(value).toBe(result);++    done();

Done here is right, because get() gets resolved after an item is pushed to the queue. I am rewriting to use async/await syntax.

RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 6d50cf2a2d323dab939fdff61b915137088603dc

more corrections: copyright date, LIFO queue test using async await, private fields with _s

view details

push time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2019-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {AwaitConnection, QueryArgs} from "./client";+import {ConnectConfig} from "./con_utils";+import * as errors from "./errors";+import {LifoQueue} from "./queues";+import {Set} from "./datatypes/set";++export class Deferred<T> {+  private _promise: Promise<T>;+  private resolve?: (value?: T | PromiseLike<T> | undefined) => void;+  private reject?: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new Error("The deferred is not resolved.");+    }+    return this._result;+  }++  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {+    while (!this.resolve) {+      // this.resolve is configured inside a Promise callback+      // to avoid race conditions, here we await for a next cycle until the+      // reference is assigned+      // Normally, awaiting a timeout of 0 in a while loop would cause high+      // CPU usage, but in this case we expect the reference to be already+      // populated in almost all cases, and anyway in the next cycle+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this.resolve(value);+  }++  async setFailed(reason?: any): Promise<void> {+    while (!this.reject) {+      // see the comment in ``setResult`` above+      await new Promise((resolve) => process.nextTick(resolve));+    }+    this.reject(reason);+  }++  constructor() {+    this._done = false;+    this.reject = undefined;+    this.resolve = undefined;++    this._promise = new Promise((resolve, reject) => {+      this.reject = reject;++      this.resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++class PoolConnectionHolder {+  private _pool: Pool;+  private _proxy: PoolConnectionProxy | null;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _connection: AwaitConnection | null;+  private _generation: number | null;+  private _inUse: Deferred<void> | null;++  constructor(+    pool: Pool,+    onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>,+    onRelease?: (proxy: PoolConnectionProxy) => Promise<void>+  ) {+    this._pool = pool;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._connection = null;+    this._proxy = null;+    this._inUse = null;+    this._generation = null;+  }++  public get connection(): AwaitConnection | null {+    return this._connection;+  }++  public get pool(): Pool {+    return this._pool;+  }++  private getConnectionOrThrow(): AwaitConnection {+    if (this._connection === null) {+      throw new TypeError("The connection is not open");+    }+    return this._connection;+  }++  terminate(): void {+    if (this._connection !== null) {+      // TODO: synchronous close!+      this._connection.close();+    }+  }++  async connect(): Promise<void> {+    if (this._connection !== null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.connect() called while another " ++          "connection already exists"+      );+    }++    this._connection = await this._pool.getNewConnection();+    this._generation = this._pool.generation;+  }++  async acquire(): Promise<PoolConnectionProxy> {+    if (this._connection === null || this._connection.isClosed()) {+      this._connection = null;+      await this.connect();+    } else if (this._generation !== this._pool.generation) {+      // Connections have been expired, re-connect the holder.+      await this._connection.close();+      this._connection = null;+      await this.connect();+    }++    const proxy = new PoolConnectionProxy(this, this.getConnectionOrThrow());+    this._proxy = proxy;++    if (this._onAcquire) {+      try {+        await this._onAcquire(proxy);+      } catch (error) {+        // If a user-defined `onAcquire` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.++        // Use `close()` to close the connection gracefully.+        // An exception in `onAcquire` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _cleanup().++        await this._connection?.close();+        throw error;+      }+    }++    this._inUse = new Deferred<void>();+    return proxy;+  }++  async release(): Promise<void> {+    if (this._inUse === null) {+      throw new errors.ClientError(+        "PoolConnectionHolder.release() called on " ++          "a free connection holder"+      );+    }++    if (this._connection?.isClosed()) {+      // When closing, pool connections perform the necessary+      // cleanup, so we don't have to do anything else here.+      return;+    }++    if (this._generation !== this._pool.generation) {+      // The connection has expired because it belongs to+      // an older generation (Pool.expire_connections() has+      // been called).+      await this._connection?.close();+      return;+    }++    if (this._onRelease && this._proxy) {+      try {+        await this._onRelease(this._proxy);+      } catch (error) {+        // If a user-defined `onRelease` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await this._connection?.close();+        // Use `close()` to close the connection gracefully.+        // An exception in `setup` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via releaseOnClose().+        throw error;+      }+    }++    // Free this connection holder and invalidate the+    // connection proxy.+    await this._release();+  }++  async waitUntilReleased(): Promise<void> {+    if (this._inUse === null) {+      return;+    }+    await this._inUse.promise;+  }++  async close(): Promise<void> {+    if (this._connection !== null) {+      // AsyncIOConnection.aclose() will call releaseOnClose() to+      // finish holder cleanup.+      await this._connection.close();+    }+  }++  releaseOnClose(): void {+    this._release();+    this._connection = null;+  }++  setProxy(proxy: PoolConnectionProxy): void {+    if (this._proxy !== null && proxy !== null) {+      // Should not happen unless there is a bug in `Pool`.+      throw new errors.InterfaceError(+        "internal client error: connection is already proxied"+      );+    }++    this._proxy = proxy;+  }++  private async _release(): Promise<void> {+    // Release this connection holder.+    if (this._inUse === null) {+      // The holder is not checked out.+      return;+    }++    if (!this._inUse.done) {+      await this._inUse.setResult();+    }++    this._inUse = null;++    // Deinitialize the connection proxy.  All subsequent+    // operations on it will fail.+    if (this._proxy !== null) {+      this._proxy.detach();+      this._proxy = null;+    }++    // Put ourselves back to the pool queue.+    this._pool.queue.push(this);+  }+}++export class PoolConnectionProxy {+  private _holder: PoolConnectionHolder;+  private _connection: AwaitConnection | null;++  constructor(holder: PoolConnectionHolder, connection: AwaitConnection) {+    this._holder = holder;+    this._connection = connection;++    // Registers a callback that will be invoked when the+    // when the connection is closed, release it+    connection.setProxy(this);+  }++  public get connection(): AwaitConnection {+    if (this._connection === null) {+      throw new errors.InterfaceError("The proxy is detached");+    }+    return this._connection;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * from a connection.+   */+  public get detached(): boolean {+    return this._connection === null;+  }++  /**+   * Returns a value indicating whether this PoolConnectionProxy is detached+   * or the underlying connection is closed.+   */+  isClosed(): boolean {+    return this._connection === null || this._connection.isClosed();+  }++  /** @internal */+  onConnectionClose(): void {+    this._holder.releaseOnClose();+  }++  public get holder(): PoolConnectionHolder {+    return this._holder;+  }++  detach(): AwaitConnection | null {+    if (this._connection === null) {+      return null;+    }++    const connection = this._connection;+    this._connection = null;+    connection.setProxy(null);+    return connection;+  }+}++const DefaultMinPoolSize = 0;+const DefaultMaxPoolSize = 100;++export interface PoolOptions {+  connectOptions: ConnectConfig;+  minSize?: number;+  maxSize?: number;+  onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  onConnect?: (connection: AwaitConnection) => Promise<void>;+  connectionFactory?: (options?: ConnectConfig) => Promise<AwaitConnection>;+}++/**+ * A connection pool.+ *+ * Connection pool can be used to manage a set of connections to the database.+ * Connections are first acquired from the pool, then used, and then released+ * back to the pool. Once a connection is released, it's reset to close all+ * open cursors and other resources *except* prepared statements.+ * Pools are created by calling :func:`~edgedb.pool.create`.+ */+export class Pool {

So maybe for consistency, I will rename the current Pool into AwaitPool, and have the Pool class work using callbacks. To follow: AwaitConnection - Connection; AwaitPool - Pool.

RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 6317a946e213655605ff24f61bd390e480e4384e

adds underscore to setProxy and releaseOnClose methods, other minor corrections

view details

push time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool

 Connection  .. _BigInt:     https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt+++.. _edgedb-js-api-pool:++Pool+==========++.. js:class:: Pool(options: PoolOptions)++    A connection pool is used to manage a set of connections to a database.+    Since opening connections is an expensive operation, connection pools are used+    to maintain and reuse connections, enhancing the performance of database+    interactions.++    Pools can be created using the static constructor :js:meth:`Pool.create` of+    js:class:`~edgedb.Pool`.++    .. code-block:: js++        // Use the Node.js assert library to test results.+        const assert = require("assert");+        const edgedb = require("edgedb");++        async function main() {+        const pool = await edgedb.Pool.create({+            connectOptions: {+                user: "edgedb",+                host: "127.0.0.1",+            },+        });++        await pool.initialize();++        try {+            let data = await pool.fetchOne("SELECT [1, 2, 3]");++            // The result is an Array.+            assert(data instanceof Array);+            assert(typeof data[0] === "number");+            assert(data.length === 3);+            assert(data[2] === 3);+        } finally {+            // in this example, the pool is closed after a single+            // operation; in real scenarios a pool is initialized+            // at application startup, and closed at application shutdown+            await pool.close();+        }+        }

No, indentation was wrong, the bracket is right. I am fixing it. :)

RobertoPrevato

comment created time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool

     "type": "git",     "url": "https://github.com/edgedb/edgedb-js.git"   },-  "version": "0.7.3",+  "version": "0.7.4",

I thought we would publish a new version of the package to npm, but I made a mistake not asking. Besides, I see there is already 0.7.4 in npmjs.com. I revert to the previous value.

RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 89722ae645141863f463495a2b09c38f95bb863f

correct \r\n at end of file

view details

push time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha a54e72b5910c0e5b41f950b7943ae7c268f3e4ca

protects the Pool constructor, enforces using the static async create that initializes the pool automatically

view details

push time in 2 months

pull request commentedgedb/edgedb-js

Adds a connection pool (WIP)

The only missing is the documentation: I wrote it but I didn't see it yet on the edgedb-site locally. Because I only looked at it using make html from Sphinx (I didn't know yet how to run edgedb-site when I used Sphinx directly).

RobertoPrevato

comment created time in 2 months

pull request commentedgedb/edgedb-js

Adds a connection pool (WIP)

@1st1 a few days ago I wanted to propose something like this for transactions (you would find it the edit history of the description of this PR), this seems reasonable to me. I then removed this part of text because it's a different topic then connection pooling, and deserves a dedicated PR.

async transaction<T>(
  action: (connection: AwaitConnection) => Promise<T>
): Promise<T> {
  let result: T;
  await this.execute("START TRANSACTION");
  try {
    result = await action(this);
    // All done - commit.
    await this.execute("COMMIT");
  } catch (err) {
    await this.execute("ROLLBACK");

    throw err;
  }
  return result;
}

RobertoPrevato

comment created time in 2 months

pull request commentedgedb/edgedb-js

Adds a connection pool (WIP)

Unless I am wrong, the transaction object is not implemented in the JS driver (I'll verify later today).

Earlier today I tried the following piece of code, and the code in the finally block and __aexit__ are executed even without calling the generator .aclose(): why is that? (just out of curiosity)

import asyncio


class AsyncContext:

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        # And we get here, too
        print("And here, too")


class MyException(Exception):
    pass


async def iterate(times):
    async with AsyncContext():
        try:
            for x in range(times):
                await asyncio.sleep(0.01)
                yield x
        except Exception as ex:
            # We never get here!!
            print("Not here")
        finally:
            # We get here...
            print("We get here...")


async def main():
    async for item in iterate(10):
        print(item)
        if item > 3:
            raise MyException()


asyncio.run(main())

Produces the output:

0
1
2
3
4
We get here...
And here, too
Traceback (most recent call last):
...
RobertoPrevato

comment created time in 2 months

PullRequestEvent

pull request commentedgedb/edgedb-js

Adds a connection pool (WIP)

In the code above, the code execution is not resumed after the CrashTestError is thrown while iterating the async generator.

Why should it be resumed? The exception just crashes the main() function, I don't see what's there to execute? What does specifically fail in that example?

I looked more into this, and my understanding was wrong. This test wanted to reproduce this one in Python:

    async def test_pool_handles_transaction_exit_in_asyncgen_1(self):
        pool = await self.create_pool(min_size=1, max_size=1)

        async def iterate(con):
            async with con.transaction():
                for record in await con.fetchall("SELECT {1, 2, 3}"):
                    yield record

        class MyException(Exception):
            pass

        with self.assertRaises(MyException):
            async with pool.acquire() as con:
                agen = iterate(con)
                try:
                    async for _ in agen:  # noqa
                        raise MyException()
                finally:
                    await agen.aclose()

        await pool.aclose()

My understanding is that when MyException is raised, code execution resumes in the async generator at the point that yielded, exiting the transaction with a ROLLBACK, thanks to the fact that con.transaction() is an async context manager and its __aexit__ method is called even in this case.

test(
  "pool handles transaction exit in asyncgen 1",
  async () => {
    const pool = await Pool.create({
      connectOptions: getConnectOptions(),
      minSize: 1,
      maxSize: 1,
    });

    let calledRollback = false;

    async function* iterate(
      connection: AwaitConnection
    ): AsyncGenerator<number, void, unknown> {
      await connection.execute("START TRANSACTION");
      try {
        const values = await connection.fetchAll("SELECT {1, 2, 3}");

        let value: number;
        for (value of values) {
          yield value;
        }
        await connection.execute("COMMIT");
      } catch (err) {
        calledRollback = true;
        await connection.execute("ROLLBACK");

        throw err;
      }
    }

    async function main(): Promise<void> {
      const proxy = await pool.acquire();

      const agen = iterate(proxy.connection);

      try {
        for await (const _ of agen) {
          throw new CrashTestError();
        }
      } finally {
        await pool.release(proxy);
      }
    }

    await expect(main()).rejects.toThrow(new CrashTestError().message);

    expect(calledRollback).toBe(true);
    await pool.close();
  },
  BiggerTimeout
);

The same doesn't happen in the TypeScript code I wrote: my mistake was thinking that a catch block would caught an exception happened during the async iteration. Instead, only the code inside a finally block is executed and it's the same in Python (I just verified in an example).

Anyway, since the JS driver doesn't have something like the Python driver transaction context manager, there is nothing to test here.

RobertoPrevato

comment created time in 2 months

PR closed edgedb/edgedb-js

Adds a connection pool (WIP)

Still a work in progress. The code tries to be a faithful replica of the logic implemented in the Python driver.

  • To replicate the asyncio future, a simple Deferred<T> class has be added.
  • Since implementing a LIFO queue with promises is simple, I finally included a class for the Pool.queue (see LifoQueue<T> and related tests).

Still to be done:

  • AwaitConnection is not yet modified to have the pool related methods defined in the Python counterpart (`asyncio_con set_proxy, _cleanup, etc.).
  • I started reimplementing the Python test-suite in TypeScript, but this is not complete yet. See for example test_pool_07 --> pool.onAcquire onConnect callbacks
  • At the moment I didn't replicate the optimization to reuse the connection address after the first connection holder is initialized (async def _initialize(self): -> first_ch ...), for simplicity I removed this part since it's accessory compared to the core logic of the pool. If possible I would like to postpone the implementation of this feature.

[UPDATE 2020-06-18]

  • the Python pool driver test suite was almost completely replicated in TypeScript - I couldn't recreate only a few tests (more information below)
  • the set_proxy, cleanup from the Python driver was re-implemented. It should work in the same way. The only thing that was omitted is ensure_proxied method, which I don't understand fully.
  • The Python driver supports custom connection classes. To support the same in the Node.js driver, we would need to modify the current implementation of Connection and AwaitConnection to use protected constructors, instead of private constructors. How would you like to proceed? For the time being, I added support for defining a custom connectionFactory for the Pool object, but it's poorer than supporting custom classes.

Other changes:

  • Improved the api to instantiate a Pool, adding a PoolOptions interface, and also a static async method to instantiate a pool and have it initialized automatically:
    const pool = await Pool.create({
      connectOptions: getConnectOptions(),
      minSize: 1,
      maxSize: 1,
    });

    # in alternative to:
    const pool1 = new Pool({
      connectOptions: getConnectOptions(),
      minSize: 1,
      maxSize: 1,
    });
    await pool1.initialize();

About tests that could not be replicated:

I had problem replicating tests that use transactions and async generators. My understanding is that async generators in TypeScript are less powerful than in Python and cannot handle exceptions during an iteration (not sure if I am correct).

This is what I tried:

test(
  "pool handles transaction exit in asyncgen 1",
  async () => {
    const pool = await Pool.create({
      connectOptions: getConnectOptions(),
      minSize: 1,
      maxSize: 1,
    });

    let calledRollback = false;

    async function* iterate(
      connection: AwaitConnection
    ): AsyncGenerator<number, void, unknown> {
      await connection.execute("START TRANSACTION");
      try {
        const values = await connection.fetchAll("SELECT {1, 2, 3}");

        let value: number;
        for (value of values) {
          yield value;
        }
        await connection.execute("COMMIT");
      } catch (err) {
        calledRollback = true;
        await connection.execute("ROLLBACK");

        throw err;
      }
    }

    async function main(): Promise<void> {
      const proxy = await pool.acquire();

      const agen = iterate(proxy.connection);

      try {
        for await (const _ of agen) {
          throw new CrashTestError();
        }
      } finally {
        await pool.release(proxy);
      }
    }

    await expect(main()).rejects.toThrow(new CrashTestError().message);

    expect(calledRollback).toBe(true);
    await pool.close();
  },
  BiggerTimeout
);

In the code above, the code execution is not resumed after the CrashTestError is thrown while iterating the async generator. Maybe I am doing something wrong with async generators in TypeScript (I used them conveniently in Python for some time).

  1. Generally tests that use the transaction, since this method is not available in the JS implementation.
  2. Tests related to custom connection classes.
+2074 -3

5 comments

12 changed files

RobertoPrevato

pr closed time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool (WIP)

 export class AwaitConnection {       this._abort();     } finally {       this._leaveOp();+      this._cleanup();

I asked your confirmation because I was in doubt whether I should only do the proxy cleaning during the onClose socket event handler, rather than calling the close method that does more cleaning.

RobertoPrevato

comment created time in 2 months

issue commentmicrosoft/mssql-docker

Docker images stop working after OS restart

Thank You! I tried again on a laptop running Ubuntu 18.04 (in this case it is not a VM running in Virtualbox), and indeed it works even after OS restart. Tomorrow I can try on the same Virtualbox machine in which I initially saw the problem, and leave a comment here. Tonight I don't have time to check on the Virtualbox VM.

RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 3a7bd70e092abda19eb93a2f4663afdf6350d4dc

adds documentation, increases package version, polishing touches to pool

view details

push time in 2 months

issue commentmicrosoft/mssql-docker

Docker images stop working after OS restart

Hi @DavidSpackman and @gadeleonp , no I didn't find a solution to this problem. I just resigned from using the Docker image. Quite sad that this ticket is not getting attention from Microsoft. In the past I got answers for several other issues here in GitHub.

RobertoPrevato

comment created time in 2 months

push eventRobertoPrevato/PyAzBlob

Roberto Prevato

commit sha 3031d30ef029a3d49ee8eccc9b2732249548e2ff

logger to write to stout, not stderr

view details

push time in 2 months

push eventRobertoPrevato/PyAzBlob

Roberto Prevato

commit sha ee1162fcbc4cd81f10136af1e5d5cdf2f2f31d02

corrects package errors

view details

push time in 2 months

delete branch RobertoPrevato/PyAzBlob

delete branch : revamped

delete time in 2 months

push eventRobertoPrevato/PyAzBlob

Roberto Prevato

commit sha e7f92e513f4f8d4bae99023243aacff5b8b099f1

revamped click application with flake8, black, mypy :sparkles:

view details

Roberto Prevato

commit sha ef80146f728d77625d54b527d312de396ea92265

revamped click application with flake8, black, mypy, github CI :sparkles:

view details

push time in 2 months

create barnchRobertoPrevato/PyAzBlob

branch : revamped

created branch time in 2 months

startedsepandhaghighi/art

started time in 2 months

issue commentedgedb/edgedb-python

Possibly undefined exception class InternalClientError

Reminder for myself:

  • onRelease callback seems to be not tested, add a test to the suite
RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 7bd4f83612672362d6b280529246edee1b836154

adds missing test for onRelease callback

view details

push time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool (WIP)

 export class AwaitConnection {       this._abort();     } finally {       this._leaveOp();+      this._cleanup();

I registered an event listener to socket onClose event. I read your code several times in the existing async close, and it seems reasonable to call this method in the onClose event handler (I mean: not only the method that cleans the proxy), to ensure that all necessary operations are executed. Can you please confirm this?

RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 0eb151f6eefb0d2c495ba8d5f6d09d2802a774a4

handles socket onClose to do cleanup, removes underscores from queue private members

view details

push time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 5407067624d6b08bf9b08e7975ef7a49a69d53d6

renames connection "user" to "proxy"

view details

Roberto Prevato

commit sha 1d79ff03c31857d1ed1f4d3e59dc3ac7bbf0b4ab

removes TODOs where applicable, adds @internal, corrects proxy name

view details

push time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 1f6dc9c9ddb5b03896959eb8ca5c0eb09c238885

replaces setTimeout 0 with process.nextTick

view details

push time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool (WIP)

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2019-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {AwaitConnection, QueryArgs} from "./client";+import {ConnectConfig} from "./con_utils";+import * as errors from "./errors";+import {LifoQueue} from "./queues";+import {Set} from "./datatypes/set";++export class Deferred<T> {+  private _promise: Promise<T>;+  private resolve?: (value?: T | PromiseLike<T> | undefined) => void;+  private reject?: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new Error("The deferred is not resolved.");+    }+    return this._result;+  }++  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {+    while (!this.resolve) {+      // this.resolve is configured inside a Promise callback+      // to avoid race conditions, here we await for a next cycle until the+      // reference is assigned+      // Normally, awaiting a timeout of 0 in a while loop would cause high+      // CPU usage, but in this case we expect the reference to be already+      // populated in almost all cases, and anyway in the next cycle+      await new Promise((resolve) => setTimeout(resolve, 0));+    }+    this.resolve(value);+  }++  async setFailed(reason?: any): Promise<void> {+    while (!this.reject) {+      // see the comment in ``setResult`` above+      await new Promise((resolve) => setTimeout(resolve, 0));+    }+    this.reject(reason);+  }++  constructor() {+    this._done = false;+    this.reject = undefined;+    this.resolve = undefined;++    this._promise = new Promise((resolve, reject) => {+      this.reject = reject;++      this.resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++class PoolConnectionHolder {+  private _pool: Pool;

I asked somewhere the same question in one of the other comments (whether we should keep underscores for private attributes). In some cases, having underscores make sense if you want to have, or add in the future, public get accessor.

I mean for example:

private _foo: boolean;

public get foo(): boolean { return this._foo; }
RobertoPrevato

comment created time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool (WIP)

 export default function connect(   } } +export interface ConnectionUser {

I can rename to proxy. The only reason why I wrote it different is that I wanted to avoid adding complexity of defining an interface in a third common file, to reference the common object in both client.ts and pool.ts, and seemed reasonable to kept it abstract (since not only a proxy might need to cleanup resources when a connection is closed).

RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha ce36a5f6c9f4780451803d0e05e561453fd23338

improves deferred comment

view details

Roberto Prevato

commit sha 472dca031bfd11ea598781b4398540b93c893ab3

recreating test exception in onAcquire

view details

Roberto Prevato

commit sha d058140ff8c143416a844b383c09dd7a88859e43

recreates method to test exception in onAcquire

view details

Roberto Prevato

commit sha 91d641a355a34d0a7e8b2d5dddd0f2b2d2d2032a

more tests, pool improvements

view details

Roberto Prevato

commit sha c5e3d3a47b27102095ca5d46a70a2b402e5bc883

release on close, option with callback

view details

Roberto Prevato

commit sha adbf861f11f78d41e7308a9a5a86758fcae3f81a

simplifies cleanup on close

view details

Roberto Prevato

commit sha 75471e05a777e2cc3f95243f83d7c59e278c72d4

completes the test suite

view details

push time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool (WIP)

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2019-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {AwaitConnection, QueryArgs} from "./client";+import {InvalidValueError} from "./errors";+import {ConnectConfig} from "./con_utils";+import {Set} from "./datatypes/set";+import {LifoQueue} from "./queues";++export class Deferred<T> {+  private _promise: Promise<T>;+  private resolve!: (value?: T | PromiseLike<T> | undefined) => void;+  private reject!: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new InterfaceError("The deferred is not resolved");+    }+    return this._result;+  }++  setResult(value?: T | PromiseLike<T> | undefined): void {+    this.resolve(value);

I didn't do a research yet, however an idea came to my mind. What about making these methods async and awaiting for a next cycle until references are assigned?

  async setResult(value?: T | PromiseLike<T> | undefined): Promise<void> {
    while (!this.resolve) {
      // this.resolve is configured inside a Promise callback;
      // to avoid race conditions, here we await for a next cycle until the
      // reference is assigned
      // Normally, awaiting a timeout of 0 in a while loop would cause high
      // CPU usage, but in this case we expect the reference to be already
      // populated in almost all cases, and anyway in the next cycle
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
    this.resolve(value);
  }

  async setFailed(reason?: any): Promise<void> {
    while (!this.reject) {
      // see the comment in ``setResult`` above
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
    this.reject(reason);
  }

This looks sound to me.

RobertoPrevato

comment created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha a2821eadae90c6b5a1a10cd20f87cf4ba5fe0d39

handles possible race condition in Deferred object, removes reversed helper

view details

push time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 9e141dc21f2a048a741fd506b4b16ceaa41ce172

corrects formatting, corrects how errors are used in pool.ts

view details

push time in 2 months

issue openededgedb/edgedb-python

Possibly undefined exception class

asyncio_pool refers to errors.InternalClientError in two conditions, but there is not custom exception InternalClientError defined in errors namespace.

Should errors.InternalClientError be replaced with ClientError or one of its subclasses?

Noticed while working on JS pool.

created time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool (WIP)

 import * as process from "process"; import connect from "../src/index.node"; import {NodeCallback, AwaitConnection, Connection} from "../src/client"; import {ConnectConfig} from "../src/con_utils";+import { Pool } from "../src/pool";

I was missing this setting in VS Code, that is why formatting on save wasn't working: :innocent:

"editor.defaultFormatter": "esbenp.prettier-vscode"
RobertoPrevato

comment created time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool (WIP)

+/*!+ * This source file is part of the EdgeDB open source project.+ *+ * Copyright 2019-present MagicStack Inc. and the EdgeDB authors.+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++import connect, {AwaitConnection, QueryArgs} from "./client";+import {InvalidValueError} from "./errors";+import {ConnectConfig} from "./con_utils";+import {Set} from "./datatypes/set";+import {LifoQueue} from "./queues";++export class Deferred<T> {+  private _promise: Promise<T>;+  private resolve!: (value?: T | PromiseLike<T> | undefined) => void;+  private reject!: (reason?: any) => void;+  private _result: T | PromiseLike<T> | undefined;+  private _done: boolean;++  public get promise(): Promise<T> {+    return this._promise;+  }++  public get done(): boolean {+    return this._done;+  }++  public get result(): T | PromiseLike<T> | undefined {+    if (!this._done) {+      throw new InterfaceError("The deferred is not resolved");+    }+    return this._result;+  }++  setResult(value?: T | PromiseLike<T> | undefined): void {+    this.resolve(value);+  }++  setFailed(reason?: any): void {+    this.reject(reason);+  }++  constructor() {+    this._done = false;+    this._promise = new Promise((resolve, reject) => {+      this.reject = reject;++      this.resolve = (value?: T | PromiseLike<T> | undefined) => {+        this._done = true;+        this._result = value;+        resolve(value);+      };+    });+  }+}++function reversed<T>(items: T[]): T[] {+  const result: T[] = [];+  for (let i = items.length - 1; i >= 0; i--) {+    result.push(items[i]);+  }+  return result;+}++class InternalClientError extends Error {}++class InterfaceError extends Error {}++class PoolConnectionHolder {+  private _pool: Pool;+  private _proxy: PoolConnectionProxy | null;+  private _onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _onRelease?: (proxy: PoolConnectionProxy) => Promise<void>;+  private _connection: AwaitConnection | null;+  private _generation: number | null;+  private _inUse: Deferred<void> | null;++  constructor(+    pool: Pool,+    onAcquire?: (proxy: PoolConnectionProxy) => Promise<void>,+    onRelease?: (proxy: PoolConnectionProxy) => Promise<void>+  ) {+    this._pool = pool;+    this._onAcquire = onAcquire;+    this._onRelease = onRelease;+    this._connection = null;+    this._proxy = null;+    this._inUse = null;+    this._generation = null;+  }++  public get connection(): AwaitConnection | null {+    return this._connection;+  }++  private getConnectionOrThrow(): AwaitConnection {+    if (this._connection === null) {+      throw new TypeError("The connection is not open");+    }+    return this._connection;+  }++  terminate(): void {+    if (this._connection !== null) {+      // TODO: synchronous close!+      this._connection.close();+    }+  }++  async connect(): Promise<void> {+    if (this._connection !== null) {+      throw new InternalClientError(+        "PoolConnectionHolder.connect() called while another " ++          "connection already exists"+      );+    }++    this._connection = await this._pool._getNewConnection();+    this._generation = this._pool._generation;+  }++  async acquire(): Promise<PoolConnectionProxy> {+    if (this._connection === null || this._connection.isClosed()) {+      this._connection = null;+      await this.connect();+    } else if (this._generation !== this._pool._generation) {+      // TODO: double check this part of code+      // Connections have been expired, re-connect the holder.+      await this._connection.close();+      this._connection = null;+      await this.connect();+    }++    const proxy = new PoolConnectionProxy(this, this.getConnectionOrThrow());+    this._proxy = proxy;++    if (this._onAcquire) {+      try {+        await this._onAcquire(proxy);+      } catch (error) {+        // If a user-defined `onAcquire` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.++        // Use `close()` to close the connection gracefully.+        // An exception in `onAcquire` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _release_on_close().++        // TODO: check _release_on_close in Connection+        await this._connection?.close();+        throw error;+      }+    }++    this._inUse = new Deferred<void>();+    return proxy;+  }++  async release(): Promise<void> {+    if (this._inUse === null) {+      throw new InternalClientError(+        "PoolConnectionHolder.release() called on " ++          "a free connection holder"+      );+    }++    if (this._connection?.isClosed()) {+      // When closing, pool connections perform the necessary+      // cleanup, so we don't have to do anything else here.+      return;+    }++    if (this._generation !== this._pool._generation) {+      // The connection has expired because it belongs to+      // an older generation (AsyncIOPool.expire_connections() has+      // been called).+      await this._connection?.close();+      return;+    }++    if (this._onRelease && this._proxy) {+      try {+        await this._onRelease(this._proxy);+      } catch (error) {+        // If a user-defined `onRelease` function fails, we don't+        // know if the connection is safe for re-use, hence+        // we close it.  A new connection will be created+        // when `acquire` is called again.+        await this._connection?.close();+        // Use `close()` to close the connection gracefully.+        // An exception in `setup` isn't necessarily caused+        // by an IO or a protocol error.  close() will+        // do the necessary cleanup via _release_on_close().+        throw error;+      }+    }++    // Free this connection holder and invalidate the+    // connection proxy.+    this._release();+  }++  async waitUntilReleased(): Promise<void> {+    if (this._inUse === null) {+      return;+    }+    await this._inUse.promise;+  }++  async close(): Promise<void> {+    if (this._connection !== null) {+      // TODO: complete, remove following comment+      // AsyncIOConnection.aclose() will call _release_on_close() to+      // finish holder cleanup.+      await this._connection.close();+    }+  }++  _release_on_close(): void {

An oversight. By the way, how do you feel about private methods starting with an underscore? I have mixed feelings about this, because TypeScript supports private anyway.

RobertoPrevato

comment created time in 2 months

Pull request review commentedgedb/edgedb-js

Adds a connection pool (WIP)

 import * as process from "process"; import connect from "../src/index.node"; import {NodeCallback, AwaitConnection, Connection} from "../src/client"; import {ConnectConfig} from "../src/con_utils";+import { Pool } from "../src/pool";

Yes, I am using esbenp.prettier-vscode, but I didn't configure it to automatically format on save. I'm doing that now and I'll soon do a push to correct the formatting errors.

RobertoPrevato

comment created time in 2 months

PR opened edgedb/edgedb-js

Reviewers
Adds a connection pool (WIP)

Still a work in progress. The code tries to be a faithful replica of the logic implemented in the Python driver.

  • To replicate the asyncio future, a simple Deferred<T> class has be added.
  • Since implementing a LIFO queue with promises is simple, I finally included a class for the Pool.queue (see LifoQueue<T> and related tests).

Still to be done:

  • AwaitConnection is not yet modified to have the pool related methods defined in the Python counterpart (`asyncio_con set_proxy, _cleanup, etc.).
  • I started reimplementing the Python test-suit in TypeScript, but this is not complete yet. See for example test_pool_07 --> pool.onAcquire onConnect callbacks
  • At the moment I didn't replicate the optimization to reuse the connection address after the first connection holder is initialized (async def _initialize(self): -> first_ch ...), for simplicity I removed this part since it's accessory compared to the core logic of the pool. If possible I would like to postpone the implementation of this feature.
+1193 -1

0 comment

7 changed files

pr created time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha 86b4b0f97dd6207d68cfb983f85d221587874709

one more test replicated from Python driver test suite, small change in connection proxy design

view details

push time in 2 months

push eventedgedb/edgedb-js

Roberto Prevato

commit sha cf4b6b7d35afce359f0313ba5899c1e60de6c1c1

onAcquire test

view details

push time in 2 months

create barnchedgedb/edgedb-js

branch : pool

created branch time in 2 months

delete branch RobertoPrevato/essentials

delete branch : flake_fix

delete time in 2 months

push eventRobertoPrevato/essentials

Roberto Prevato

commit sha e1d5cd44a375e2d98f03247e089c181e4fb5dee6

corrects f string format error (#4)

view details

push time in 2 months

create barnchRobertoPrevato/essentials

branch : flake_fix

created branch time in 2 months

delete branch RobertoPrevato/pytest-azurepipelines

delete branch : css_styles2

delete time in 2 months

push eventRobertoPrevato/PythonCLI

Roberto Prevato

commit sha 9b81fe35feaacddc6d83551e49f6839afcdcfad9

Project template :cherries:

view details

push time in 2 months

create barnchRobertoPrevato/PythonCLI

branch : master

created branch time in 2 months

created repositoryRobertoPrevato/PythonCLI

Project template for Python CLI projects

created time in 2 months

push eventRobertoPrevato/PythonTemplate

Roberto Prevato

commit sha 19922f9684801cf586682169a7d2835e8a876663

updates README

view details

push time in 2 months

push eventRobertoPrevato/PythonTemplate

Roberto Prevato

commit sha a6eff116e708b3e48cac2720acc38643d52ba2f3

structure :boat:

view details

push time in 2 months

create barnchRobertoPrevato/PythonTemplate

branch : master

created branch time in 2 months

created repositoryRobertoPrevato/PythonTemplate

Python project template for personal use

created time in 2 months

startedfacebook/create-react-app

started time in 2 months

PullRequestEvent
PullRequestEvent
more