GithubHelp home page GithubHelp logo

Comments (17)

bergus avatar bergus commented on June 26, 2024 1

@bergus I have tried to apply your solution from StackOverflow, but wasn't successful.

Aw. It seems to work fine for me:

class AsyncQueue {
    constructor() {
        // invariant: at least one of the arrays is empty.
        // when `resolvers` is `null`, the queue has ended.
        this.resolvers = [];
        this.promises = [];
    }
    putNext(result) {
        if (!this.resolvers)
            throw new Error('Queue already ended');
        if (this.resolvers.length)
            this.resolvers.shift()(result);
        else
            this.promises.push(Promise.resolve(result));
    }
    put(value) {
        this.putNext({ done: false, value });
    }
    end() {
        for (const res of this.resolvers)
            res({ done: true, value: undefined });
        this.resolvers = null;
    }
    next() {
        if (this.promises.length)
            return this.promises.shift();
        else if (this.resolvers)
            return new Promise(resolve => { this.resolvers.push(resolve); });
        else
            return Promise.resolve({ done: true, value: undefined });
    }
    [Symbol.asyncIterator]() {
        // Todo: Use AsyncIterator.from()
        return this;
    }
}
function limitConcurrent(iterable, n) {
    const produced = new AsyncQueue();
    const consumed = new AsyncQueue();
    (async () => {
        try {
            let count = 0;
            for await (const p of iterable) {
                const promise = Promise.resolve(p.nested);
                promise.then(value => {
                    produced.put(value);
                }, _err => {
                    produced.putNext(promise); // with rejection already marked as handled
                });
                if (++count >= n) {
                    await consumed.next(); // happens after any produced.put[Next]()
                    count--;
                }
            }
            while (count) {
                await consumed.next(); // happens after any produced.put[Next]()
                count--;
            }
        }
        catch (e) {
            // ignore `iterable` errors?
        }
        finally {
            produced.end();
        }
    })();
    return (async function* () {
        for await (const value of produced) {
            yield value;
            consumed.put();
        }
    }());
}

const sleep = t => new Promise(resolve => {
    setTimeout(resolve, t);
});

const start = Date.now();
const log = x => console.log(`${x} @${Date.now() - start}`);

async function* produceSlowly(n) {
    for (let i = 0; i < n; i++) {
        await sleep(2000);
        log(`i=${i}`);
        yield {nested: sleep(i * 1000).then(() => i)};
    }
}

(async function () {
    const i = limitConcurrent(produceSlowly(9), 2);
    for await (const x of i) {
        log(`         x=${x}`);
    }
})();

Maybe it can be reduced to a simpler fix on the existing code?

I'm sure it can. I'll try to come up with something.

from iter-ops.

bergus avatar bergus commented on June 26, 2024 1

Here's what I'd do:

export function waitRaceAsync<T>(
    iterable: AsyncIterable<Promise<T> | T>,
    cacheSize: number
): AsyncIterable<T> {
    cacheSize = cacheSize > 1 ? cacheSize : 1; // cache size cannot be smaller than 1
    return {
        [$A](): AsyncIterator<T> {
            const source = iterable[$A]();
            let finished = false;
            // resolvers for currently active tasks,  which are racing to remove and call the first one
            const resolvers: null | ((res: IteratorResult<T> | Promise<never>) => void)[] = [];
            // cache of promises to be resolved or to be returned by `.next()` to the destination
            const promises: Promise<IteratorResult<T>>[] = [];
            
            function kickOffNext(): void {
                promises.push(new Promise((resolve, reject) => {
                    resolvers.push(resolve);
                    
                    // `new Promise` executor handles synchronous exceptions
                    source.next().then(a => {
                        if (a.done) {
                            finished = true;
                            resolvers.pop()(a);
                        } else if (isPromiseLike(a.value)) {
                            const promise = Promise.resolve(a.value);
                            promise.then(value => {
                                resolvers.shift()({done: false, value});
                                kickOffMore();
                            }, _ => {
                                resolvers.shift()(promise);
                                kickOffMore();
                            });
                        } else {
                            resolvers.shift()(a);
                        }
                        kickOffMore(); // advance source iterator as far as possible within limit
                    }, err => {
                        // handle rejections from calling `i.next()`
                        resolvers.shift()(Promise.reject(err));
                        finished = true; // ???
                    });
                });
            }
            function kickOffMore() {
                if (!finished // stop when source is done
                    && promises.length < cacheSize // backpressure: don't put too many promises in the cache if destination doesn't poll `.next()` fast enough
                    && resolvers.length < cacheSize // limit: don't let more tasks than the maximum race to resolve the next promises
                ) {
                    kickOffNext();
                }
            }
            return {
                next(): Promise<IteratorResult<T>> {
                    if (!promises.length) {
                        kickOffNext();
                    }
                    return promises.shift();
                },
            };
        },
    };
}

There may be a problem if the returned iterator is polled too fast (not waiting for the promise returned by .next() before calling it again), currently this will just go beyond the limit but I think that's the best (and easiest) way to handle this - a well-behaved consumer should never do this anyway.

from iter-ops.

bergus avatar bergus commented on June 26, 2024 1

Start with an actual async generator, instead of mapping a synchronous iterator to an iterator of promises :-)
async function* oops() { yield 1; await sleep(10); throw new Error('boom'); } will reject on the second .next() call.

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

I have added a test that can be uncommented once the fix is in place.

The logic behind the test is as follows: replace waitRace(5) with wait(), and the test will pass. The delays should be quite similar (right now they are way off).

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

I have improved the test further, to make it more comprehensive.

Again, the test passes when we replace waitRace(5) with wait().

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

I made a couple more attempts at fixing it, without success again. It requires a special inspiration to implement it properly.

@RebeccaStevens I could really use some help here, as I'm stuck with this issue.

from iter-ops.

RebeccaStevens avatar RebeccaStevens commented on June 26, 2024

Looking into it. I've kind of got it working but not really (does that make any sense 👅?).

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

Well, if it can pass the test, that's a good sign ;)

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

@bergus Thank you!!! This does work, and passes the test that I created! 👍

@RebeccaStevens I have the latest, using what @bergus gave us, inside 182-bugfix branch, which works perfectly.

It just needs some rework for proper types control, and that's it!

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

The code written by @bergus has been merged into the master, refactored somewhat.

Further refactoring is possible.

from iter-ops.

bergus avatar bergus commented on June 26, 2024

Oh well. I write

a well-behaved consumer should never do this anyway.

but then actually that's exactly what the code does with source. If the promises contained in the source iterator resolve faster than the source iterator itself progresses, the kickOffMore() function will be called multiple times - and source.next() will be called again before the previous source.next() promise fulfilled.

This may require some more thought.

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

@bergus Thank you for your contribution! If you can improve the code further, you are very welcome!

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

The main issue has been addressed, so I am closing the issue. If something comes up again - we can open a new one ;)

Released in v2.2.2.

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

On the final note, I did the following interesting test locally...

import {map, pipeAsync, waitRace} from 'iter-ops';

function* gen(n) {
    while (n > 0) {
        yield n--;
    }
}

const sleep = (n) => new Promise(resolve => {
    setTimeout(resolve, n);
});

const i = pipeAsync(gen(10), map(async a => {
    const delay = Math.random() * (a % 2 === 0 ? 10 : 1000);
    await sleep(delay);
    return a;
}), waitRace(5));

(async function () {
    for await(const a of i) {
        console.log(a);
    }
})();

It outputs:

8
10
6
4
2
5
1
7
3
9

It gives us perfect consistency in prioritizing even numbers over odd ones, as we random-delay even numbers for longer. And if we reduce the cache size below 5 (total of even numbers here), that consistency starts to go away, exactly as expected.

Great job @bergus! 👏 I had no idea it was even possible to implement without using Promise.race 😄

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

Open related update ticket - #186

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

@bergus I couldn't come up with a test that would make use of this piece of code:

                            (err) => {
                                // handle rejections from calling `i.next()`
                                resolvers.shift()!(Promise.reject(err));
                                finished = true;
                            }

Are you sure this section should ever execute? If so, how do you think it should be tested? I have added all the exception-throwing tests that I could think of, but none execute that code block.

from iter-ops.

vitaly-t avatar vitaly-t commented on June 26, 2024

@bergus Thank you!!! This commit did it 😄

from iter-ops.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.