Comments (17)
@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.
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.
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.
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.
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.
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.
Looking into it. I've kind of got it working but not really (does that make any sense 👅?).
from iter-ops.
Well, if it can pass the test, that's a good sign ;)
from iter-ops.
@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.
The code written by @bergus has been merged into the master, refactored somewhat.
Further refactoring is possible.
from iter-ops.
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.
@bergus Thank you for your contribution! If you can improve the code further, you are very welcome!
from iter-ops.
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.
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.
Open related update ticket - #186
from iter-ops.
@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.
@bergus Thank you!!! This commit did it 😄
from iter-ops.
Related Issues (20)
- Remove IterationState from timeout HOT 1
- Add support for infinite timeouts HOT 1
- Extend "last" operator with iteration state HOT 17
- "reduce" callback gets incorrect index HOT 1
- pipeSync should throw an error on any asynchronous input
- pipeAsync should be able to accept Promise as input HOT 4
- Add async callback support to operator "reduce" HOT 5
- Operator waitRace should deactivate when cacheSize < 2 HOT 1
- start vs stop logic inconsistency HOT 11
- Operators "wait" and "waitRace" should simply forward in sync mode HOT 2
- Post-merge issues of decoupling the interface HOT 31
- toIterable() fails with completed iterators HOT 7
- Operator concurrencyFork should handle pipeline-construction errors HOT 1
- Review toIterable logic HOT 3
- Operator timeout should forward callback errors HOT 1
- Operator timeout should account for the first value. HOT 1
- Benchmark Readme statement about RXJS subscription is incorrect. HOT 3
- takeUntil takes iterates to one more elements HOT 1
- Further iterable extension with method `Add` HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from iter-ops.