Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync APIs #6

Closed
ppcano opened this issue Jul 15, 2022 · 2 comments
Closed

Sync APIs #6

ppcano opened this issue Jul 15, 2022 · 2 comments
Assignees
Labels
documentation Improvements or additions to documentation

Comments

@ppcano
Copy link

ppcano commented Jul 15, 2022

#4 brought async support to most of the xk6-redis APIs. Currently, await is not currently supported, so there is no way to interact with Redis synchronously for most APIs.

// await is not supported
const counter = await redisClient.get('my_counter');
if (counter > 10) { 
   // do something in VU code
}

I discussed this briefly with @mstoykov and @sniku. It seems that async APIs does not allow using using Redis for some particular cases.

The main reason is that Promise handlers are always executed after the VU code. Let's show one example:

export default function () {

    CONSOLE.log(`before async ${exec.vu.idInTest}`);
    
    redisClient.incr('my_key').then((total) => { 
          // this will only be executed when the VUs code finalizes
	   CONSOLE.log(`promise callback ${exec.vu.idInTest}`);
    });
    sleep(Math.random() * 5);
    CONSOLE.log(`exit VU code ${exec.vu.idInTest}`);
}

In this case, the execution will always be in this order:

before async 1                             
exit VU code 1                                
promise callback 1         

With async APIs, the result of a promise cannot change the VU execution code (outside the promise handler). Users could not do something like:

let localCounter = 0;
redisClient.get('my_counter').then((counter) => { 
     localCounter = counter;
});

while (localCounter === 0) {
   // VU will be blocked in this loop.
}

This issue requests to provide "sync" support for Redis APIs to not limit the potential of using Redis in k6 tests. For example, to share data across VUs in your load test.

cc @oleiade

@oleiade
Copy link
Member

oleiade commented Jul 19, 2022

Hi @ppcano 👋🏻

Thank you so much for your input, it's been really useful, and triggered productive discussions and investigations on the backstage 🙇🏻

While you're right, nor k6, nor Goja, support the async/await syntax presently, it doesn't mean a seemingly synchronous behavior can't be achieved. The async/await is somewhat of syntactic sugar over Promises (don't quote me on this, there are some differences induced by how the runtime behaves, but that's the gist), to make it easier to reason about red/blue functions. Namely, anything you can do with async/await can be done with Promises. Moreover, Promises have been around for quite some time in the JS ecosystem, and one could argue most developers are comfortable dealing with them.

This being said, following the introduction of the event loop, although it might not be feature-complete yet, we consider the future of k6 modules is asynchronous. xk6-redis is making its way to becoming a core extension, and the move to a Promises based API as a result is only natural. Some other xk6 extensions are already using promises, or working towards switching to them. The http core extension is also planned to be rewritten that way (although making it asynchronous is only one of the motivations there).

Thus, how do we intend to move forward considering the issue you've raised, while also respecting the technical plan that led to switching to asynchronous API in the first place?

  • ⚔️ Introducing a synchronous API means duplicating and maintaining much more code. It also would lead to difficulties deprecating the synchronous API in the future. The whole point of the change we've made was to introduce the Redis functionality to the core down the line in a future-proof state. Therefore, we are definitely against that.
  • 👍🏻 We believe that despite k6 and Goja not yet having all the niceties allowing the use of asynchronous programming in a seemingly asynchronous way, the generalization of promise-based APIs in the close future, will make the adaptation easier for our users over time.
  • 👍🏻 Anything that's possible using async/await is indeed possible using only Promises, albeit the result being arguably less elegant. Thus, we will make sure to provide more thorough examples of how to use xk6-redis in scenarios where it should be used as an external storage solution, or in the context of testing a redis installation itself.

Here's a more full-fledged example demonstrating how to address the sort of issues you pointed out in your comment:

import { check } from 'k6'
import http from 'k6/http'
import redis from 'k6/x/redis'
import exec from 'k6/execution'

export const options = {
    scenarios: {
        redisPerformance: {
            executor: 'shared-iterations',
            vus: 10,
            iterations: 200,
            exec: 'measureRedisPerformance',
        },
        usingRedisData: {
            executor: 'shared-iterations',
            vus: 10,
            iterations: 200,
            exec: 'measureUsingRedisData',
        },
    },
}

// Instantiate a new redis client
const redisClient = new redis.Client({
    addrs: new Array('localhost:6379'),
    password: 'foobar',
})

// Prepare an array of crocodile ids for later use
// in the context of the measureUsingRedisData function.
const crocodileIDs = new Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

export function measureRedisPerformance() {
    // VUs are executed in a parallel fashion,
    // thus, to ensure that parallel VUs are not
    // modifying the same key at the same time,
    // we use keys indexed by the VU id.
    const key = `foo-${exec.vu.idInTest}`

    redisClient
        .set(`foo-${exec.vu.idInTest}`, 1)
        .then(() => redisClient.get(`foo-${exec.vu.idInTest}`))
        .then((value) => redisClient.incrBy(`foo-${exec.vu.idInTest}`, value))
        .then((_) => redisClient.del(`foo-${exec.vu.idInTest}`))
        .then((_) => redisClient.exists(`foo-${exec.vu.idInTest}`))
        .then((exists) => {
            if (exists !== 0) {
                throw new Error('foo should have been deleted')
            }
        })
}

export function setup() {
    redisClient.sadd('crocodile_ids', ...crocodileIDs)
}

export function measureUsingRedisData() {
    // Pick a random crocodile id from the dedicated redis set,
    // we have filled in setup().
    redisClient
        .srandmember('crocodile_ids')
        .then((randomID) => {
            const url = `https://test-api.k6.io/public/crocodiles/${randomID}`
            const res = http.get(url)

            check(res, {
                'status is 200': (r) => r.status === 200,
                'content-type is application/json': (r) =>
                    r.headers['content-type'] === 'application/json',
            })

            return url
        })
        .then((url) => redisClient.hincrby('k6_crocodile_fetched', url, 1))
}

export function teardown() {
    redisClient.del('crocodile_ids')
}

export function handleSummary(data) {
    redisClient
        .hgetall('k6_crocodile_fetched')
        .then((fetched) => Object.assign(data, { k6_crocodile_fetched: fetched }))
        .then((data) => redisClient.set(`k6_report_${Date.now()}`, JSON.stringify(data)))
        .then(() => redisClient.del('k6_crocodile_fetched'))
}

Note how we, for instance, call the (synchronous) http.get method in the context of a promise. We believe developers are already used to asynchronous programming involving promise chains, as demonstrated here.

PS: In your specific counter example, that would indeed translate to writing most of the test as a promise chain: if everything is a promise, then nothing is, and that's basically what we're shooting for 🙇🏻

@oleiade oleiade self-assigned this Jul 19, 2022
@oleiade oleiade added the documentation Improvements or additions to documentation label Jul 19, 2022
oleiade added a commit that referenced this issue Jul 26, 2022
As raised by #6, the use of xk6-redis' module APIs was not clear,
nor thorough enough; especially in a context where it would need
to be used side by side with synchronous APIs.
oleiade added a commit that referenced this issue Jul 29, 2022
As raised by #6, the use of xk6-redis' module APIs was not clear,
nor thorough enough; especially in a context where it would need
to be used side by side with synchronous APIs.
oleiade added a commit that referenced this issue Aug 2, 2022
As raised by #6, the use of xk6-redis' module APIs was not clear,
nor thorough enough; especially in a context where it would need
to be used side by side with synchronous APIs.
@ppcano
Copy link
Author

ppcano commented Feb 22, 2023

Closing it as await is now supported in k6.

@ppcano ppcano closed this as completed Feb 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

2 participants