Create a good k6 load test with graphql/subscription - graphql

After struggling against k6, with the help of the answers to my other question, i was able (i think) to test subscriptions with websocket.
Now i'm trying to desing a good test. Our app is a Single Page App that mainly uses websocket subscriptions instead of queries. The problem is that i'm a bit lost on how to define how many subscriptiosn i should test in order to detect if my app will be able to support around 800 users working simultaneously in real time. (I started testing it first with a top of 150 VUs).
For now, i'm running the test with 3 commonly used subscriptions in our user path, but:
Should I try more subscriptions, trying to cover the most used ones in our user path, or should I keep it simple and pick the 3 most used ones and add a timeout between them?
Is this a correct approach for load/stress testing with GRAPHQL subscriptions/websocket? I'm not sure if i'm being too cautious about this, but i'm afraid of giving a false status about our situation.
I am a bit confused on how to interpret the results screen, especially on how I can infer if we will be able to give a good experience to our users. Should I take the avg and p(95) of ws_ping as a reference for this?
k6 screen result
As a reference, here is part of the code i'm using to perform the test
Thanks in advance!
main.js
import { Httpx } from 'https://jslib.k6.io/httpx/0.0.5/index.js';
const session = new Httpx({
baseURL: `https://${enviorment}`
});
const wsUri = `wss://${enviorment}/v1/graphql`;
const pauseMin = 2;
const pauseMax = 6;
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '30s', target: 100 },
{ duration: '30s', target: 150 },
{ duration: '120s', target: 150 },
{ duration: '60s', target: 50 },
{ duration: '30s', target: 0 },
]
};
export default function () {
session.addHeader('Content-Type', 'application/json');
todas(keys.profesorFirebaseKey, keys.desafioCursoKey, keys.nivelCurso, keys.profesorKey)
}
todas.js:
import ws from 'k6/ws';
import {fail,check} from 'k6'
import exec from 'k6/execution';
export function todas(id, desafioCursoKey, nivel, profesorKey) {
const queryList = [`];
let ArraySubscribePayload = []
for (let i = 0; i < 3; i++) {
let subscribePayload = {
id: String(1 + 3 * i),
payload: {
extensions: {},
query: queryList[i],
variables: {},
},
type: "start",
}
ArraySubscribePayload.push(subscribePayload)
}
const initPayload = {
payload: {
headers: {
xxxxxxxxxxxxx
},
lazy: true,
},
type: "connection_init",
};
const res = ws.connect(wsUri, initPayload, function (socket) {
socket.on('open', function () {
socket.setInterval(function timeout() {
socket.send(JSON.stringify(initPayload));
socket.send(JSON.stringify(ArraySubscribePayload[0]));
socket.setTimeout(function timeout() {
socket.send(JSON.stringify(ArraySubscribePayload[1]));
socket.send(JSON.stringify(ArraySubscribePayload[2]));
ArraySubscribePayload[1].id = String(parseInt(ArraySubscribePayload[1].id) + 1)
ArraySubscribePayload[2].id = String(parseInt(ArraySubscribePayload[2].id) + 1)
}, 3000);
ArraySubscribePayload[0].id = String(parseInt(ArraySubscribePayload[0].id) + 1)
socket.ping();
}, 7000);
})
});
}

Related

fetch in parallel async/await with Promises.all

I continue to struggle with serial/parallel processing in JS (promises). I want to query my server and identify those queries that take longer than, say 500 ms. The following works, but as far as I understand, the queries are made one after another.
const query = async (queries) => {
for (let i = 0, j = queries.length; i < j; i++) {
let t = process.hrtime();
const response = await fetch(queries[i]);
const result = await response.json();
t = process.hrtime(t);
const ms = Math.round((t[0] * 1000) + (t[1] / 1000000));
if (ms > 500) {
console.log(ms, queries[i]);
}
}
}
query(arrayOfQueries);
// console output below (snipped for brevity)
3085 http://localhost:3010/v3/…
2463 http://localhost:3010/v3/…
2484 http://localhost:3010/v3/…
…
I change the above code to fire the queries in parallel, but now I get back an array of promises. I can't figure out how to identify only those promises that take longer than 500ms to resolve
const query = async (queries) => {
const r = await Promise.all(queries.map(async (q) => fetch(q)));
console.log(r);
};
// console output
[
Response {
size: 0,
timeout: 0,
[Symbol(Body internals)]: { body: [PassThrough], disturbed: false, error: null },
[Symbol(Response internals)]: {
url: 'http://localhost:3010/v3/…',
status: 200,
statusText: 'OK',
headers: [Headers],
counter: 0
}
},
Response {
size: 0,
timeout: 0,
[Symbol(Body internals)]: { body: [PassThrough], disturbed: false, error: null },
[Symbol(Response internals)]: {
url: 'http://localhost:3010/v3/…',
status: 200,
statusText: 'OK',
headers: [Headers],
counter: 0
}
},
Response {
size: 0,
timeout: 0,
[Symbol(Body internals)]: { body: [PassThrough], disturbed: false, error: null },
[Symbol(Response internals)]: {
url: 'http://localhost:3010/v3/…',
status: 200,
statusText: 'OK',
headers: [Headers],
counter: 0
}
},
When running the queries in parallel, you would have to add code (similar to what you had for your non-parallel example) to time each one separately so you could track each individual request separately.
The time of each request overlaps so you can't keep track of the timing of each individual request from the outside. Here's an example of timing each individual request:
const query = async (queries) => {
const r = await Promise.all(queries.map(async (q) => {
const start = Date.now();
const response = await fetch(q);
const json = await response.json();
const delta = Date.now() - start;
console.log(`${delta}ms for ${q}`);
return json;
});
return r;
};
This will output the timing for each request at the time it finishes which may not be in the same order that the requests were made. If you want, you can collect these timing results into an array and output all the timing at once at the end.

Apollo Graphql fetchMore, updateQuery does not update state

I'm currently trying to implement pagination on my posts.
Using Apollo graphql here is my useQuery
const { data: postsData, fetchMore } = useQuery(POSTS_BY_USER_DRAFT, {
fetchPolicy: 'network-only',
variables: {
user: user.id,
start: 0,
limit: limit
},
onCompleted: () => {
setTotal(postsData[model].meta.pagination.total)
}})
and here is my onClick handler for fetching more posts
const loadMorePosts = async () => {
const nextStart = start + limit
setStart(nextStart);
await fetchMore({
variables: {
user: user.id,
offset: nextStart,
limit: limit,
},
updateQuery: (prevResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return prevResult
}
const prevData = prevResult[model].data
const moreData = fetchMoreResult[model].data
fetchMoreResult[model].data = [...prevData, ...moreData]
// fetchMoreResult[model].data = [...moreData]
return fetchMoreResult
},
})}
My queries are successful as I do get correctly the data, however postsData does not get updated
[NOTICED]: If I switch fetchMoreResult[model].data = [...prevData, ...moreData] for
fetchMoreResult[model].data = [...moreData] my postsData does get updated.
I have tried return { ...fetchMoreResult } and multiple ways of returning data fearing an immutability/comparaison issue but it does not seem to do the job.
I'm not sure why, but setting a fetchPolicy for Apollo will do the job
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Publication: {
merge: true,
},
Post: {
merge: true,
},
},
}),
defaultOptions: defaultOptions,
})

Pagination Apollo Client 3 - Cache merges but does not render new results when paginating

I'd love to implement nested pagination within my application. I have been reading the docs and looking at several other examples but I just can't get this to work - any help is appreciated! Thanks!
React component:
I am clicking the button to run the fetchMore function provided by the useQuery hook (apollo). The network request is going through and the new products are merged into the cache... but no new products render on the page.
export const FilterableKit = () => {
const selectedKitId = useReactiveVar(selectedKitIdVar);
const [
getKitProducts,
{ data: getKitProductsData, loading: getKitProductsLoading, fetchMore },
] = useGetKitProductsLazyQuery();
useEffect(() => {
if (selectedKitId) {
getKitProducts({
variables: {
getKitsInput: {
_id: {
string: selectedKitId,
filterBy: "OBJECTID" as StringFilterByEnum,
},
},
getProductsInput: {
config: {
pagination: {
reverse: true,
limit: 3,
},
},
},
},
});
}
}, [getKitProducts, selectedKitId]);
const kitProducts = getKitProductsData?.getKits.data?.find(
(kit) => kit?._id === selectedKitId
)?.products.data;
const handleLoadMore = () => {
if (kitProducts && kitProducts?.length > 0) {
const remaining =
getKitProductsData?.getKits.data[0]?.products.stats?.remaining;
if (remaining && remaining > 0) {
const cursor =
kitProducts[kitProducts.length - 1] &&
kitProducts[kitProducts.length - 1]?.createdAt;
fetchMore({
variables: {
getProductsInput: {
config: {
pagination: {
reverse: true,
createdAt: cursor,
},
},
},
},
});
}
}
};
return (
<CContainer>
<KitItemCards products={kitProducts} loading={getKitProductsLoading} />
<CContainer className="d-flex justify-content-center my-3">
<CButton color="primary" className="w-100" onClick={handleLoadMore}>
Load More
</CButton>
</CContainer>
</CContainer>
);
};
Type Policies: I define the "Kit" typePolicy to merge products into the correct field.
export const cache: InMemoryCache = new InMemoryCache({
typePolicies: {
Kit: {
fields: {
products: {
keyArgs: false,
merge(existing = [] as Product[], incoming: GetProductsResponse) {
if (!incoming) return existing;
if (!existing) return incoming;
const { data: products, ...rest } = incoming;
let result: any = rest;
result = [...existing, ...(products ?? [])];
return result;
},
},
},
},
});
Thanks for any pointers in the right direction! Let me know if there is something else you'd like to see.

How to get random records from Strapi content API

I have records in strapi. I am using strapi content API. In my front-end, I need to display only 2 records randomly. For limiting, I have used limit query from content API. But random fetching what keyword I need to use. The official documentation doesn't provide any details regarding this - https://strapi.io/documentation/v3.x/content-api/parameters.html#available-operators
There's no official Strapi API parameter for random. You have to implement your own. Below is what I've done previously, using Strapi v3:
1 - Make a service function
File: api/mymodel/services/mymodel.js
This will contain our actual random query (SQL), and wrapping it in a service is handy because it can be used in many places (cron jobs, inside other models, etc).
module.exports = {
serviceGetRandom() {
return new Promise( (resolve, reject) => {
// There's a few ways to query data.
// This example uses Knex.
const knex = strapi.connections.default
let query = knex('mydatatable')
// Add more .select()'s if you want other fields
query.select('id')
// These rules enable us to get one random post
query.orderByRaw('RAND()')
query.limit(1)
// Initiate the query and do stuff
query
.then(record => {
console.log("getRandom() record: %O", record[0])
resolve(record[0])
})
.catch(error => {
reject(error)
})
})
}
}
2 - Use the service somewhere, like a controller:
File: api/mymodel/controllers/mymodel.js
module.exports = {
//(untested)
getRandom: async (ctx) => {
await strapi.services.mymodel.serviceGetRandom()
.then(output => {
console.log("getRandom output is %O", output.id)
ctx.send({
randomPost: output
}, 200)
})
.catch( () => {
ctx.send({
message: 'Oops! Some error message'
}, 204) // Place a proper error code here
})
}
}
3 - Create a route that points to this controller
File: api/mymodel/config/routes.json
...
{
"method": "GET",
"path": "/mymodelrandom",
"handler": "mymodel.getRandom",
"config": {
"policies": []
}
},
...
4 - In your front-end, access the route
(However you access your API)
e.g. ajax call to /api/mymodelrandom
There is no API parameter for getting a random result.
So: FrontEnd is the recommended solution for your question.
You need to create a random request range and then get some random item from this range.
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
const firstID = getRandomInt(restaurants.length);
const secondID = getRandomInt(3);
const query = qs.stringify({
id_in:[firstID,secondID ]
});
// request query should be something like GET /restaurants?id_in=3&id_in=6
One way you can do this reliably is by two steps:
Get the total number of records
Fetch the number of records using _start and _limit parameters
// Untested code but you get the idea
// Returns a random number between min (inclusive) and max (exclusive)
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
const { data: totalNumberPosts } = await axios.get('/posts/count');
// Fetch 20 posts
const _limit = 20;
// We need to be sure that we are not fetching less than 20 posts
// e.g. we only have 40 posts. We generate a random number that is 30.
// then we would start on 30 and would only fetch 10 posts (because we only have 40)
const _start = getRandomArbitrary(0, totalNumberPosts - _limit);
const { data: randomPosts } = await axios.get('/posts', { params: { _limit, _start } })
The problem with this approach is that it requires two network requests but for my needs, this is not a problem.
This seem to work for me with Strapi v.4 REST API
Controller, Get 6 random entries
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries].sort(() => 0.5 - Math.random());
ctx.body = randomEntries.slice(0, numberOfEntries);
},
};
});
Route
random.js
"use strict";
module.exports = {
routes: [
{
method: "GET",
path: "/artwork/random",
handler: "artwork.random",
config: {
auth: false,
},
},
],
};
API
http://localhost:1337/api/artwork/random
To match default data structure of Strapi
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries]
.sort(() => 0.5 - Math.random())
.slice(0, numberOfEntries);
const structureRandomEntries = {
data: randomEntries.map((entry) => {
return {
id: entry.id,
attributes: entry,
};
}),
};
ctx.body = structureRandomEntries;
},
};
});
There is also a random sort plugin.
https://www.npmjs.com/package/strapi-plugin-random-sort
This seem to work for me with Strapi v4.3.8 and graphql
src/index.js
"use strict";
module.exports = {
register({ strapi }) {
const extensionService = strapi.service("plugin::graphql.extension");
const extension = ({ strapi }) => ({
typeDefs: `
type Query {
randomTestimonial: Testimonial
}
`,
resolvers: {
Query: {
randomTestimonial: async (parent, args) => {
const entries = await strapi.entityService.findMany(
"api::testimonial.testimonial"
);
const sanitizedRandomEntry =
entries[Math.floor(Math.random() * entries.length)];
return sanitizedRandomEntry;
},
},
},
resolversConfig: {
"Query.randomTestimonial": {
auth: false,
},
},
});
extensionService.use(extension);
},
bootstrap({ strapi }) {},
};
graphql query:
query GetRandomTestimonial {
randomTestimonial {
__typename
name
position
location
description
}
}
generate random testimonial on route change/refresh
https://jungspooner.com/biography

vue-rx: how to watch value of object from an array is not change anymore?

"vue-rx": "^6.1.0",
"rxjs": "^6.4.0",
"vue": "^2.5.17",
I'm new in vue-rx and rxjs,But when I see several demo of rx, I'm quite interested in this.So I want to use it in my project which posts a request when attribute num will not change anymore
[
{
id: 0,
name: 'giftA',
num: 0 // will turn to 1,2,3,4,5,...after running `send({id: 0})` function 1,2,3,4,5,...times
},
{
id: 1,
name: 'giftB',
num: 0
},
...
]
And Here is my solution:
using $watchAsObservable to watch the change of sendCalledTimes, and then using mergeMap to post the request.
the variable sendCalledTimes is a number which will sendCalledTimes++ when called send function, And after posting the request, reset this to sendCalledTimes = 0.
So that $watchAsObservable('sendCalledTimes')(vue-rx) will execute every three seconds, and will reduce request times in my project. But i think it's still not good because it just like a timer and can't watch weather num of each object in the Array changes. The good example should be like this search example.
data() {
return {
sendCalledTimes: 0,
giftArr: []
}
},
created() {
this.$watchAsObservable('sendCalledTimes').pipe(
pluck('newValue'),
filter(val => val > 0),
debounceTime(3000),
// if `sendCalledTimes` is the same number as previous
// will not execute follows
// distinctUntilChanged(),
mergeMap(
(val) => this.requestSendGift()
),
).subscribe(
(val) => { }
)
},
methods: {
send (obj) {
let pushFlag = true
for (const gift in this.giftArr) {
if (gift.id === obj.id) {
gift.num++
pushFlag = false
break
}
}
if (pushFlag) {
this.giftArr.push(obj)
}
// observable
this.sendCalledTimes++
},
async requestSendGift () {
for (const gift in this.giftArr) {
// example for post a request to store each gift
await axios({
data: gift,
type: 'post',
url: '...'
}).then(res => { ... })
}
// reset `this.sendCalledTimes`
this.sendCalledTimes = 0
}
}
Also since vue-rx doesn't have many examples on github, so i need help to solve creating good subscription for this situation.
I have tried this, but failed:
data () {
return {
giftArr: []
}
},
subscriptions: {
test: from(this.giftArr) // console.log(this.$observables.test) throw an error: typeError: Cannot read property 'giftArr' of undefined
},
It would be greatly appreciated if anyone can help me to solve this question.
It's a little unclear from your question exactly what you're trying to do, but I've created an example based on what I believe to be your intent.
I made some assumptions:
You have a 'gifts' array that represents all of the gifts that will ever exist.
You want to make updates to that array.
Every time you make an update to the array, you want to see the update in the form of an Observable emitting an event.
Use a Subject
I think what you want is a Subject.
const gift$ = new Subject();
Make it Emit on Updates
And you would set it up to emit every time you increment num or add a new gift.
function addGift(gift) {
gifts.push(gift);
gift$.next(gift);
}
function incrementGift(gift) {
gift.num++;
gift$.next(gift);
}
All together it could look something like this:
import { Subject } from 'rxjs';
const gift$ = new Subject();
const gifts = [{ id: 0, name: 'giftA', num: 0 }, { id: 1, name: 'giftB', num: 0 }];
function addGift(gift) {
gifts.push(gift);
gift$.next(gift);
}
function incrementGift(gift) {
gift.num++;
gift$.next(gift);
}
function sendGift(newGift) {
const currentGift = gifts.find(g => g.id === newGift.id);
currentGift ? incrementGift(currentGift) : addGift(newGift);
}
gift$.subscribe(update => {
console.log(gifts);
console.log(update);
});
// You should see an initial logging of 'gifts' and update will be 'undefined' at first. Then you'll see a log for every 'sendGift'.
sendGift({ id: 0 });
sendGift({ id: 3, name: 'giftC', num: 0 });
StackBlitz

Resources