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

Fix/49 cannot find context with specified id #169

Merged
merged 2 commits into from
Jan 5, 2022

Conversation

inancgumus
Copy link
Member

@inancgumus inancgumus commented Dec 27, 2021

This PR fixes #49 for good 🎉

After long debugging sessions and trials, finally, I figured out the problem: FrameSession was blocking frame handling, and the rest of the system couldn't propagate execution contexts, causing the context with specified id not found error. This misbehavior happens because a Session can get closed while a FrameSession is in the initialization stage.

The only error we are receiving is different now: wait for selector didn't result in any nodes on the Click Continue (Recommendations) group. I tested this patch with Chromium 98.0.4753.0. I also tested this with other test runners, and they stuck on the same step. I'm going to create a new issue for it.

Here's the script I'm using. It's a more concise and faster (_pauseMin/Max_) version of Tom's original script.
import { sleep, group } from 'k6';
import launcher from 'k6/x/browser';

import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js';

export const options = {};

export default function () {
        const pauseMin = 1;
        const pauseMax = 2;

        let browser = launcher.launch('chromium', {
                args: [],
                debug: false,
                devtools: false,
                env: {},
                executablePath: null,
                headless: false,
                ignoreDefaultArgs: [],
                proxy: {},
                slowMo: '500ms',
                timeout: '30s',
        });

        let context = browser.newContext({
                ignoreHTTPSErrors: true,
                screen: { width: 1920, height: 1080 },
                viewport: { width: 1920, height: 1080 },
        });

        let page = context.newPage();

        group("Navigate to Homepage", () => {
                console.log('Starting navigateToHomepage...');
                // page.goto('https://vistaprint.ca', { waitUntil: 'networkidle' });
                page.goto('https://vistaprint.ca');
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Business Cards", () => {
                console.log('Starting clickBusinessCards...');
                page.waitForSelector("//span[text()='Business Cards']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Browse Designs", () => {
                console.log('Starting clickBrowseDesigns...');
                page.waitForSelector("//a[text()='Browse designs']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Select Design Option", () => {
                console.log('Starting clickDesign...');

                // const designOptions = page.$$("li.design-tile");
                // console.log(`Found ${designOptions.length} design options.`);

                const designOption = page.waitForSelector(`li.design-tile:first-child`);
                console.log("Design options:", designOption.textContent());
                designOption.click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Customize Design", () => {
                console.log('Starting clickCustomizeDesign...');
                page.waitForSelector("//a[text()='Customize this design']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });


        group("Click Next", () => {
                console.log('Starting clickNext...');
                page.waitForSelector("//button[text()='Next']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Continue Without Back", () => {
                console.log('Starting continueWithoutBack...');
                page.waitForSelector("//button[text()='Continue without back']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Customer Reviewed", () => {
                console.log('Starting customerReviewed...');

                page.waitForSelector("//input[@value='customerReviewed']").click();
                sleep(1);

                page.waitForSelector("//button[text()='Continue']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Add to Cart", () => {
                console.log('Starting addToCart...');
                page.waitForSelector("//button[text()='Add to cart']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Continue (Recommendations)", () => {
                console.log('Starting continuePastRecommendations...');
                page.waitForSelector("//button[text()='Add to cart']").click(); // works
                
                // const els = page.$$("//a[text()='Continue']");
                // els[0].click();
                page.waitForSelector("//a[text()='Continue']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Continue to Cart", () => {
                console.log('Starting continueToCart...');

                page.waitForSelector(".slim-footer"); // works
                // const els = page.$$("//a[text()='Continue to cart']");
                // els[0].click();
                page.waitForSelector("//a[text()='Continue to cart']").click();

                sleep(randomIntBetween(pauseMin, pauseMax));
        });

        group("Click Checkout", () => {
                console.log('Starting goToCheckout...');
                page.waitForSelector("//a[text()='Checkout']").click();
                sleep(randomIntBetween(pauseMin, pauseMax));
        });
}
Here's Tom's original script that also runs without context errors. I just added a `waitForNavigation` call before `page.waitForSelector("//span[text()='Step 4']");`.
import { sleep, group } from 'k6';
import launcher from 'k6/x/browser';

import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js';

export const options = {};

let browser, context, page;

const pauseMin = 10;
const pauseMax = 10;

export default function () {
        initialize();

        navigateToHomepage();
        clickBusinessCards();
        clickBrowseDesigns();
        clickDesign();
        clickCustomizeDesign();
        clickNext();
        continueWithoutBack();
        customerReviewed();
        addToCart();
        continuePastRecommendations();
        continueToCart();
        goToCheckout();
}

function initialize() {
        browser = launcher.launch('chromium', {
                args: [],
                debug: false,
                devtools: false,
                env: {},
                executablePath: null,
                headless: false,
                ignoreDefaultArgs: [],
                proxy: {},
                slowMo: '500ms',
                timeout: '30s',
        });

        context = browser.newContext({
                ignoreHTTPSErrors: true,
                screen: { width: 1920, height: 1080 },
                viewport: { width: 1920, height: 1080 },
        });

        page = context.newPage();
}

function navigateToHomepage() {
        console.log('Starting navigateToHomepage...');

        group("Navigate to Homepage", () => {
                page.goto('https://vistaprint.ca', { waitUntil: 'networkidle' });

                // Wait for a specific element to appear
                page.waitForSelector(".fluid-image");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function clickBusinessCards() {
        console.log('Starting clickBusinessCards...');

        const elem = page.$("//span[text()='Business Cards']");

        group("Click Business Cards", () => {
                elem.click();

                page.waitForSelector("//a[text()='Browse designs']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function clickBrowseDesigns() {
        console.log('Starting clickBrowseDesigns...');

        const elem = page.$("//a[text()='Browse designs']");

        group("Click Browse Designs", () => {
                elem.click();

                page.waitForSelector("li.design-tile");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function clickDesign() {
        console.log('Starting clickDesign...');

        // fetch all of the design options
        const designOptions = page.$$("li.design-tile");
        console.log(`Found ${designOptions.length} design options.`);

        /*
        designOptions.some((designOption) => {
          const designOptionName = designOption.textContent();
          console.log(`Design option: ${designOptionName}`);
      
          if (designOptionName === designName) {
            console.log(`Selecting "${designName}"...`);
      
            group("Select Design Option", () => {
              designOption.click();
      
              page.waitForSelector("//a[text()='Customize this design']");
            });
      
            return true;
          }
        });
        */ // no bueno

        /*
        page.$$("li.design-tile", (designOption) => {
          const designOptionName = designOption.textContent();
          console.log(`Design option: ${designOptionName}`);
      
          if (designOptionName === designName) {
            console.log(`Selecting "${designName}"...`);
      
            group("Select Design Option", () => {
              designOption.click();
      
              page.waitForSelector("//a[text()='Customize this design']");
            });
      
            return true;
          }
        });
        */ // also no bueno

        const designOption = page.$(`li.design-tile >> nth=0`);
        console.log(designOption.textContent());

        group("Select Design Option", () => {
                designOption.click();

                page.waitForSelector("//a[text()='Customize this design']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function clickCustomizeDesign() {
        console.log('Starting clickCustomizeDesign...');

        const elem = page.$("//a[text()='Customize this design']");

        group("Click Customize Design", () => {
                elem.click();

                page.waitForSelector("//button[text()='Next']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function clickNext() {
        console.log('Starting clickNext...');

        const elem = page.$("//button[text()='Next']");

        group("Click Next", () => {
                elem.click();

                page.waitForSelector("//button[text()='Continue without back']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function continueWithoutBack() {
        console.log('Starting continueWithoutBack...');

        const elem = page.$("//button[text()='Continue without back']");

        group("Click Continue Without Back", () => {
                elem.click();

                page.waitForSelector("//input[@value='customerReviewed']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function customerReviewed() {
        console.log('Starting customerReviewed...');

        let elem = page.$("//input[@value='customerReviewed']");
        elem.click();

        sleep(5);

        elem = page.$("//button[text()='Continue']");

        group("Click Customer Reviewed", () => {
                elem.click();

                page.waitForNavigation();
                page.waitForSelector("//span[text()='Step 4']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function addToCart() {
        console.log('Starting addToCart...');

        const elem = page.$("//button[text()='Add to cart']");

        group("Click Add to Cart", () => {
                elem.click();

                page.waitForSelector("//a[text()='Continue']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function continuePastRecommendations() {
        console.log('Starting continuePastRecommendations...');

        const elem = page.$("//a[text()='Continue']");

        group("Click Continue (Recommendations)", () => {
                elem.click();

                page.waitForSelector("//a[text()='Continue to cart']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function continueToCart() {
        console.log('Starting continueToCart...');

        const elem = page.$("//a[text()='Continue to cart']");

        group("Click Continue to Cart", () => {
                elem.click();

                page.waitForSelector("//a[text()='Checkout']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}

function goToCheckout() {
        console.log('Starting goToCheckout...');

        const elem = page.$("//a[text()='Checkout']");

        group("Click Checkout", () => {
                elem.click();

                page.waitForSelector("//button[text()='Sign in']");
        });

        sleep(randomIntBetween(pauseMin, pauseMax));
}
Here's another script Tom has shared on Jan 4th, 2022. I modified it a little bit to wait for navigation.
import launcher from 'k6/x/browser';
import { check, group, sleep } from 'k6';

export default function () {
  const browser = launcher.launch('chromium', {
    headless: false,
    // slowMo: '500ms' // slow down by 500ms
  });
  
  let context = browser.newContext({    
    // hack:
    // Without setting viewport, the script cannot find the add-to-cart
    // element. We might want to improve this.

    // screen: { width: 1920, height: 1080 },
    // viewport: { width: 1920, height: 1080 },
  });

  const page = context.newPage();

  group("Navigate to Homepage", () => {
    page.goto('http://ecommerce.test.k6.io/', { waitUntil: 'networkidle' });
  });

  sleep(2);

  group("Click Product", () => {
    page.waitForSelector('.woocommerce-result-count');
    page.click('.woocommerce-loop-product__title');
  });
  
  sleep(2);

  group("Click Add to Cart", () => {
    console.log("Clicking Add to Cart...");

    // wait for the element to be attached
    let b = page.waitForSelector('button[name="add-to-cart"]');

    // hack: scroll the element into view
    page.evaluate(() => {
      const el = document.querySelector('button[name="add-to-cart"]');
      el.scrollIntoView();
    });
    b.click();

    // 💣 💣 💣
    // we wait for navigation to happen
    // before waiting for a selector.
    page.waitForNavigation();
	
    // page is properly navigated. let's
    // wait for the selector.
    console.log("Waiting for .woocommerce-message...");
    page.waitForSelector('.woocommerce-message'); 
  });

  sleep(2);

  group("View Cart", () => {
    page.click('.cart-contents');
    page.waitForSelector('.woocommerce-cart-form__cart-item');
  });
  
  sleep(2);

  page.close();
  browser.close();
}

@inancgumus inancgumus force-pushed the fix/49-cannot-find-context-with-specified-id branch 5 times, most recently from 3b87092 to 63041e8 Compare December 28, 2021 15:49
@inancgumus inancgumus self-assigned this Dec 30, 2021
@inancgumus inancgumus force-pushed the fix/49-cannot-find-context-with-specified-id branch 2 times, most recently from 6b9a264 to 7e4eb83 Compare December 30, 2021 14:12
Sometimes sessions get closed and they clog
the communication between frame sessions and
sessions. This usually happens when a frame
session is getting created after new frame
attachments. This was causing problems with
frame handling.

This commit prevents lagging by failing
fast when it detects a session is closed.

It also changes the ordering of FrameSession
init steps. The reason of putting initOptions
last is: It was propagating frame events
before the frame is ready to be used.
@inancgumus inancgumus force-pushed the fix/49-cannot-find-context-with-specified-id branch from 7e4eb83 to 8c7fc3b Compare January 3, 2022 14:02
@inancgumus inancgumus linked an issue Jan 3, 2022 that may be closed by this pull request
Copy link
Member

@robingustafsson robingustafsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disclaimer: I haven't run the code, just looked at it 🙂

You mention in one of the commit comments:

It also changes the ordering of FrameSession
init steps. The reason of putting initOptions
last is: It was propagating frame events
before the frame is ready to be used.

Could you share more info here, what was being propagated by initOptions? From what I know it's the enabling of domains and the target.SetAutoAttach call in initDomains that triggers the browser to report all frames and execution contexts etc.

common/session.go Show resolved Hide resolved
common/frame_session.go Show resolved Hide resolved
@inancgumus
Copy link
Member Author

inancgumus commented Jan 4, 2022

Could you share more info here, what was being propagated by initOptions? From what I know it's the enabling of domains and the target.SetAutoAttach call in initDomains that triggers the browser to report all frames and execution contexts etc.

@robingustafsson, yes, you're right: It triggers a lot of things. That's why I put it last for safety. I wanted a FrameSession to get initialized first without dealing with a lot of CDP messages. And there were other goroutines are being involved when initOptions gets called. But it doesn't change anything in general. I can revert it back if you want.

Fail-fast fix:

What this fix is all about is to terminate FrameSession initialization in a fail-fast fashion when its Session is told to be closed by the Connection hence a browser:

// contextWithDoneChan returns a new context that is canceled when the done channel
// is closed. The context will leak if the done channel is never closed.
func contextWithDoneChan(ctx context.Context, done chan struct{}) context.Context {
ctx, cancel := context.WithCancel(ctx)
go func() {
<-done
cancel()
}()
return ctx
}

return s.conn.send(contextWithDoneChan(ctx, s.done), msg, ch, res)

return s.conn.send(contextWithDoneChan(ctx, s.done), msg, nil, res)

case <-fs.session.done:
fs.logger.Debugf("FrameSession:initEvents:go:session.done",
"sid:%v tid:%v", fs.session.id, fs.targetID)
return

case <-session.done:
reason = "session closed"
return

@inancgumus inancgumus changed the title Fix/49 cannot find context with specified Fix/49 cannot find context with specified id Jan 4, 2022
@robingustafsson
Copy link
Member

Ah wait, I see now in the code that you actually put initDomains() last and NOT initOptions() as your commit comment says. I think I got confused and reacted to the commit comment rather than looking at the code 😅 All good then.

Thanks for clarifying the fail-fast fix as well, I see now that it's the "auto-cancellation" of the context when donechannel is closed that makes the execution short-circuit and fail-fast.

@inancgumus
Copy link
Member Author

Yeah, my bad, sorry 🤦

Yes, exactly, that's what I mean by failing-fast 😄

@inancgumus
Copy link
Member Author

All good then.

@robingustafsson Does that mean you approve the PR? :) Can I include this fix in Friday's release?

@robingustafsson
Copy link
Member

Yes, it's good to go (I can't seem to change my review to approve) 🙂

@inancgumus inancgumus merged commit aadc374 into main Jan 5, 2022
@inancgumus inancgumus deleted the fix/49-cannot-find-context-with-specified-id branch January 5, 2022 11:12
Copy link
Member Author

@inancgumus inancgumus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self TODO.

case <-c.ctx.Done():
c.logger.Errorf("Connection:send:<-c.ctx.Done()", "wsURL:%q sid:%v err:%v", c.wsURL, msg.SessionID, c.ctx.Err())
return nil
return ctx.Err()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c.ctx.Err()

// Erroneous cases
defer fs.logger.Debugf("FrameSession:onAttachedToTarget:NewFrameSession",
"sid:%v tid:%v esid:%v etid:%v ebctxid:%v type:%q err:%v",
if session == nil {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment here that explains why the session can be nil.

err = fs.attachWorkerToTarget(ti, sid)
default:
// Just unblock (debugger continue) these targets and detach from them.
s := fs.page.browserCtx.conn.getSession(sid)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment that says the session may get lost interim.

default:
// Just unblock (debugger continue) these targets and detach from them.
s := fs.page.browserCtx.conn.getSession(sid)
_ = s.ExecuteWithoutExpectationOnReply(fs.ctx, runtime.CommandRunIfWaitingForDebugger, nil, nil)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check s.close here?

if err == nil {
return
}
// Handle or ignore errors.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this IsIgnorableFrameSessionErr?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cannot find context with specified id
2 participants