DEV Community

Cover image for Contributing To Open Source Projects Might Be Easier Than You Think
JS for ZenStack

Posted on • Originally published at zenstack.dev

Contributing To Open Source Projects Might Be Easier Than You Think

As a software developer, you may not have directly participated in any open-source project, but you have likely benefited from the open-source world unless you don't use Git, GitHub, or Linux. 😄 In return, many of us would like to contribute and be a part of this new wave of collaborative and transparent development.

However, many people have not taken their first step because the process of contributing can be intimidating.

  • How to find a suitable issue to contribute?
  • How to confirm with the maintainer that this is actually what they want?
  • How to make sure my solution is the right way to go?
  • Do I need to add some test cases or documentation?
  • What if something goes wrong?

First of all, due to the nature of human beings, we tend to exaggerate difficulties before attempting a new task. Even leave it alone, here lies a common misconception about contributing to open-source projects:

You need to contribute code

While code contributions are undoubtedly valuable, they represent just one facet of the broader landscape of open-source collaboration. There are other ways you might make an even greater contribution than code.

What It Means To Contribute

I wrote a post about implementing a high-concurrency ticket booking system leveraging on Optimistic Concurrency Control (OCC):

Many people are impressed by the concise code by Prisma or ZenStack to achieve that:

    await client.seat.update({
        data: {
            userId: userId,
            version: {
                increment: 1,
            },
        },
        where: {
            id: availableSeat.id,
        },
    });
Enter fullscreen mode Exit fullscreen mode

What they might not know is that it’s not possible before Prisma 4.5.0 because of the below issue:

Be able to update or retrieve a single record including non-unique fields in the "where" conditions. #7290

Problem

I am unable to utilize non-unique fields in single update queries. To be clear, I want to still use the unique fields, I just want to filter it down even more.

Suggested solution

Here are my models

model Person {
  id     String   @id @db.Uuid
  client Client[]
}

model Client {
  id       String @id @db.Uuid
  name     String
  personId String @db.Uuid
  person   Person @relation(fields: [personId], references: [id])
}
Enter fullscreen mode Exit fullscreen mode

I have "people" who have "clients" attached to them. Only the person who owns the client can update the name. So when an API call comes in, I know who the current person is and what client they're trying to update. So the update query I would like to use is this:

const client = await prisma.client.update({
  data: { name: input.name },
  where: {
    id: input.clientId,
    personId: input.personId
  }
})
Enter fullscreen mode Exit fullscreen mode

but it only allows fields which are marked as @unique or @id

Alternatives

One alternative is to do a .findUnique({ where: { id: input.clientId } }) and then check if the personId of the client is the same as the one passed in. This however creates two database calls where only one is needed.

Another alternative is to do a .updateMany({ where: { id: input.clientId, personId: input.personId } }) but I don't get any of the clients back. If I got the clients back in the query and if there was a limit I could pass in to limit it to one, I would feel better about this so it wouldn't have to do any unneeded scans of the rows, but it still feels less idiomatic than updating the .update() command to allow for non-unique fields.

Before that you have to use updateMany followed by the count check as below:

const seats = await client.seat.updateMany({
  data: {
    claimedBy: userEmail,
    version: {
      increment: 1,
    },
  },
  where: {
    id: availableSeat.id,
    version: availableSeat.version, 
})

if (seats.count === 0) {
  throw new Error(`That seat is already booked! Please try again.`)
}
Enter fullscreen mode Exit fullscreen mode

Although a little verbose, it works. However, before Prisma 4.4.0, it was unable to function due to the issue described below:

`updateMany()` causes lost-updates #8612

Bug description

updateMany() returns incorrect updated counts when multiple processes or asynchronous functions run simultaneously.

The SQL log of updateMany() shows it emits SELECT and then UPDATE. Since prisma uses the default isolation level of DBMS, those queries are NON-REPEATEBLE READ in many DBMS and can cause lost-updates. I believe SELECT ... FOR UPDATE or single query UPDATE .... WHERE ... is required.

How to reproduce

bookSeat.js
const {PrismaClient} = require('@prisma/client')

const client = new PrismaClient({
  // log: ["query"]
})


async function bookSeat(userId) {
  const movieName = 'Hidden Figures'

  // Find the first available seat
  // availableSeat.version might be 0
  const availableSeat = await client.seat.findFirst({
    where: {
      // Movie: {
      //  name: movieName,
      // },
      claimedBy: null,
    },
    orderBy: [{id: "asc"}]
  })

  if (!availableSeat) {
    throw new Error(`Oh no! ${movieName} is all booked.`)
  }

  // Only mark the seat as claimed if the availableSeat.version
  // matches the version we're updating. Additionally, increment the
  // version when we perform this update so all other clients trying
  // to book this same seat will have an outdated version.
  const seats = await client.seat.updateMany({
    data: {
      claimedBy: userId,
      version: {
        increment: 1,
      },
    },
    where: {
      id: availableSeat.id,
      version: availableSeat.version, // This version field is the key; only claim seat if in-memory version matches database version, indicating that the field has not been updated
    },
  })

  if (seats.count === 0) {
    throw new Error(`That seat is already booked! Please try again.`)
  }

  return seats.count
}


async function demonstrateLostUpdate() {
  if (process.argv[2] === "createData") {
    await client.seat.deleteMany()
    for (let i = 0; i < 1000; i++) {
      await client.seat.create({
        data: {
          id: i,
          version: 0,
          movieId: 1,
          claimedBy: null,
        }
      })
    }
    process.exit()
  }


  const userId = process.argv[2]

  let updatedCount = 0
  for (let i = 0; i < 1000; i++) {
    try {
      updatedCount += await bookSeat(userId)
    } catch {
      // ignore lock failure
    }
  }


  // Detect lost-updates
  const actualCount = await client.seat.count({
    where: {
      claimedBy: userId
    },
  })
  console.log({
    userId,
    updatedCountByUpdateMany: updatedCount,
    actualUpdatedCount: actualCount
  })
  process.exit()
}

demonstrateLostUpdate()
Enter fullscreen mode Exit fullscreen mode

With the above script, run

$ node bookSeat.js createData
$ node bookSeat.js Sorcha & node bookSeat.js Ellen
Enter fullscreen mode Exit fullscreen mode

The schema and logic in the script are taken from the optimistic concurrency control example in the doc: https://www.prisma.io/docs/guides/performance-and-optimization/prisma-client-transactions-guide#optimistic-concurrency-control

Outputs:

{
  userId: 'Sorcha',
  updatedCountByUpdateMany: 968,
  actualUpdatedCount: 461
}
{
  userId: 'Ellen',
  updatedCountByUpdateMany: 974,
  actualUpdatedCount: 539
}

Expected behavior

updatedCountByUpdateMany should be equal to actualUpdatedCount in the outputs.

Prisma information

schema.prisma:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Seat {
  id        Int   @id @default(autoincrement())
  userId    Int?
  // claimedBy User? @relation(fields: [userId], references: [id])
  claimedBy String?
  movieId   Int
  // movie     Movie @relation(fields: [movieId], references: [id])
  version   Int
}

SQL query Logs of repro:

prisma:query SELECT "public"."Seat"."id", "public"."Seat"."userId", "public"."Seat"."claimedBy", "public"."Seat"."movieId", "public"."Seat"."version" FROM "public"."Seat" WHERE "public"."Seat"."claimedBy" IS NULL ORDER BY "public"."Seat"."id" ASC LIMIT $1 OFFSET $2 prisma:query BEGIN prisma:query SELECT "public"."Seat"."id" FROM "public"."Seat" WHERE ("public"."Seat"."id" = $1 AND "public"."Seat"."version" = $2) prisma:query UPDATE "public"."Seat" SET "claimedBy" = $1, "version" = ("version" + $2) WHERE "public"."Seat"."id" IN ($3) prisma:query COMMIT

Environment & setup

  • OS: Mac OS
  • Database: PostgreSQL
  • Node.js version: v14.17.0

Prisma Version

Environment variables loaded from .env
prisma               : 2.28.0
@prisma/client       : 2.28.0
Current platform     : darwin
Query Engine         : query-engine 89facabd0366f63911d089156a7a70125bfbcd27 (at node_modules/prisma/node_modules/@prisma/engines/query-engine-darwin)
Migration Engine     : migration-engine-cli 89facabd0366f63911d089156a7a70125bfbcd27 (at node_modules/prisma/node_modules/@prisma/engines/migration-engine-darwin)
Introspection Engine : introspection-core 89facabd0366f63911d089156a7a70125bfbcd27 (at node_modules/prisma/node_modules/@prisma/engines/introspection-engine-darwin)
Format Binary        : prisma-fmt 89facabd0366f63911d089156a7a70125bfbcd27 (at node_modules/prisma/node_modules/@prisma/engines/prisma-fmt-darwin)
Default Engines Hash : 89facabd0366f63911d089156a7a70125bfbcd27
Studio               : 0.417.0

The issue was filed when it was already listed as an example of Optimistic Concurrency Control (OCC) in the official documentation of Prisma (which is still present today). Therefore, before it was fixed, the Prisma team had to temporarily add a warning to the documentation at that time.

prisma-doc-fix

After closely examining the threads of the two mentioned issues, I believe you would agree that Prisma team implemented the fix as a result of conversations and discussions among actively involved community members. If you are the maintainer of Prisma, would you consider these people as contributors? Regardless of your opinion, GitHub will do:

Issues, pull requests and discussions
Issues, pull requests, and discussions will appear on your contribution graph if they were opened in a standalone repository, not a fork.

Feedback Might Be More Valuable Than PR

I recently listened to a talk by Tanner Linsley, the creator of TanStack (React Query), about his personal experience in the open-source community. I highly recommend listening to it:

Open Source Hour with Tanner Linsley

During the interview, the hoster asked him a question:

What types of contributions do you find most valuable for your projects

Here is the simplified version of his answer:

The most valuable contributions are at kind of the leaf node level. It’s the gentle feedback of

  • I tried following the documentation or I tired doing this and it didn’t work
  • I got confused or I’m stuck It may not be feedback that makes it all the way out of the library.

And once in a while, you will get people who go above and beyond and propose something, either a documentation change itself or maybe an API change. I feel like it's an exponential falloff. There are so many people who are willing to offer their two cents on, it didn't work. Most people beyond that aren't going to help you out; they are just going to kind of dine and dash.

Only one percent of those people are going to open a PR. Those are great, but usually, they require guidance from the core maintainers to say: "We would love to do that, but we can't do it that way because of XYZ." And I feel like less than 10 percent of those people will stick to the PR. Those are the people you need to nurture and pay attention to.

contributions come in many forms.

Open Source Contributions Go Beyond Code

Open source projects are more than just code. They provide a collaborative space where people can report problems, ask questions, and suggest fixes or improvements, among other things. So, if you notice anything wrong or feel that something could be better or different in the project you already use or want to use, don't hesitate to let the community hear your voice.

ZenStack is an open-source toolkit that we are developing on top of Prisma ORM. It provides an innovative schema-first approach for building web applications. If you feel that it could be helpful for you, simply trying it out and letting us know your thoughts would be a great contribution to us!

Top comments (0)