question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

The `rawConnection` footgun

See original GitHub issue

This is a description of a couple of issues I noticed while using the TransactionalConnection.rawConnection for making queries. I will paste some code, which reproduces the error, but which I haven’t tried isolated from my own project. When I have time I can try to replicate it in a standalone Vendure installation.

  • Vendure version 1.4.7
  • Database: Postgres 13

Issue 1: Using rawConnection inside a field resolver can deplete* your connection pool (my guess*)

This is a nasty bug since your server will continue running but it won’t be able to service requests. In our case it manifested in a way that the site suddenly started hanging. Google Cloud Run did not kill the container and it was accepting but not servicing requests.

Here’s the code that caused it - when running a GQL query selecting the “categories” property of Product around 10 times and you’ll see that the entire server is blocked. My guess is because the connection pool of the pg driver is depleted.

Replacing await this.conn.rawConnection.createQueryRunner() with await this.conn.getRepository(ctx, Category) fixes the issue.

// A product entity resolver to return categories that the product belongs to

import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { Ctx, RequestContext, Product, TransactionalConnection } from '@vendure/core';
import { CategoryService } from '../service';

@Resolver('Product')
export class ProductEntityResolver {
  constructor(protected service: CategoryService, private conn: TransactionalConnection) {}

  @ResolveField()
  async categories(@Ctx() ctx: RequestContext, @Parent() product: Product) {
    const q = `
    SELECT cfv1."categoryId" AS id
    FROM category_facet_value AS cfv1
    LEFT JOIN product_facet_values_facet_value AS pfv1
      ON pfv1."facetValueId" = cfv1."facetValueId"
      AND pfv1."productId" = $1
    WHERE "categoryId" IN (
      SELECT DISTINCT cfv2."categoryId"
      FROM category_facet_value AS cfv2
      INNER JOIN product_facet_values_facet_value AS pfv2 ON pfv2."facetValueId" = cfv2."facetValueId"
      WHERE pfv2."productId" = $1 AND cc."channelId" = $2
    )
    GROUP BY "categoryId"
    HAVING COUNT(cfv1."facetValueId") = COUNT(pfv1."facetValueId");`;

    const results: { id: string }[] = await this.conn.rawConnection
      .createQueryRunner()
      .query(q, [product.id, ctx.channelId]);

    return this.service.getCategoriesBySlugOrId(ctx, { ids: results.map(id => id) })
  }
}

To make this work locally, you would need to define the following entity, and schema extensions for it, as well as the schema extensions for Product. But this can probably be replicated with a much simpler extension to product, maybe without including a new entity.

/**
 * A category is defined over a set of facets.
 * All products that have *ALL* the facets of a certain category belong to the category.
 * All products in a category also belong to its ancestors
 */
@Entity('category')
export class Category extends VendureEntity implements ChannelAware {
  constructor(input?: DeepPartial<Category>) {
    super(input);
  }

  /**
   * There is a single "root" category that contains all others
   */
  @Column('boolean', { default: false })
  isRoot: boolean;

  /**
   * The child categories
   */
  @TreeChildren({ cascade: true })
  children: Category[];

  /**
   * The parent category
   */
  @TreeParent({ onDelete: 'CASCADE' })
  parent: Category | null;

  /**
   * We sometimes need the parentId
   */
  @RelationId((category: Category) => category.parent)
  parentId: string;

  /**
   * Facet values that determine which products belong here.
   */
  @OneToMany(() => CategoryFacetValue, catFacetValue => catFacetValue.category, {
    cascade: true,
    onDelete: 'CASCADE',
    onUpdate: 'CASCADE',
  })
  catFacetValues: CategoryFacetValue[];

  /**
   * Categories belong to one or more channels
   */
  @ManyToMany(() => Channel, {
    cascade: true,
    onDelete: 'CASCADE',
    onUpdate: 'CASCADE',
    orphanedRowAction: 'delete',
  })
  @JoinTable()
  channels: Channel[];

  /**
   * Length of the children array
   */
  get childrenCount() {
    return this.children?.length || 0;
  }
}

/**
 * A link table between categories and facet values
 */
@Entity('category_facet_value')
export class CategoryFacetValue {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('uuid')
  facetValueId: string;

  @Column('uuid')
  categoryId: string;

  @Column('varchar', { nullable: true })
  mode: string | null;

  @ManyToOne(() => FacetValue, { onDelete: 'CASCADE' })
  facetValue: FacetValue;

  @ManyToOne(() => Category, { onDelete: 'CASCADE' })
  category: Category;
}

Issue 2: Using getRepository(ctx, Entity) is orders of magnitude slower than using rawConnection

Since raw connection is to be avoided, Vendure does not offer a good way to run raw queries. The only way you can obtain a queryBuilder bound to the current context is if you do getRepository first. However, thus turns out to be almost three thousand times slower for some reason. I should note that this is run on a table with some 5000 facet values and 7000 products and the ids is an array of 1168 ids - so not the most efficient query, but still, the difference is 8.3s vs 30ms.

Here’s the code.

console.time('Query')
await this.connection
        .getRepository(ctx, FacetValue)
        .createQueryBuilder()
        .select('f.id', 'id')
        .addSelect('COUNT(DISTINCT p.productId)', 'productCount')
        .from('facet_value', 'f')
        .leftJoin('product_facet_values_facet_value', 'p', 'p.facetValueId = f.id')
        .where('f.id IN (:...ids)', { ids })
        .groupBy('f.id')
        .getRawMany();
console.timeEnd('Query') // 8.723s

console.time('RawConn')
await this.connection
        .rawConnection
        .createQueryBuilder()
        .select('f.id', 'id')
        .addSelect('COUNT(DISTINCT p.productId)', 'productCount')
        .from('facet_value', 'f')
        .leftJoin('product_facet_values_facet_value', 'p', 'p.facetValueId = f.id')
        .where('f.id IN (:...ids)', { ids })
        .groupBy('f.id')
        .getRawMany();
console.timeEnd('RawConn') // 28.33ms

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
michaelbromleycommented, May 11, 2022

@pleerock thanks for taking the time to advise on this! Yes, I ended up implementing concurrent queries rather than big joins for certain Vendure internals and did see very good perf gains! And when we upgrade to 0.3.x I’ll be able to use the new load strategy, so thanks for that!

0reactions
pleerockcommented, May 11, 2022

since issue https://github.com/typeorm/typeorm/issues/2381 was pointed here, I just wanted to point that using two queries instead of using joins can solve the issue in most cases.

Read more comments on GitHub >

github_iconTop Results From Across the Web

TLS 1.3 and Proxies | Hacker News
A regular (non-transparent) HTTP proxy like Squid will process HTTP requests internally, but clients use CONNECT to forward their raw connection ...
Read more >
702559 - Create a pure-async mozIStorageAsyncConnection
It's a foot-gun for a traditional connection which is then used asynchronously, but XPConnect's assertions should back-stop us there when present (and ...
Read more >
https://gitee.com/opengauss/openGauss-connector-no...
This has been a longstanding footgun within node-postgres and I am happy to get it ... it is a raw connection string so...
Read more >
vault: CHANGELOG.md - Fossies
... means the CRL can't be trusted anyways, so it's not useful and easy to footgun. ... the raw connection errors were being...
Read more >
CVS log for pkgsrc/security/vault/Makefile
... means the CRL can't be trusted anyways, so it's not useful and easy to footgun. ... the raw connection errors were being...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found