Skip to content

Initial draft for specificationa #5

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

Merged
merged 11 commits into from
Mar 6, 2019

Conversation

OlegDokuka
Copy link
Member

This PR provides an initial draft of the Reactive Streams Specification for JavaScript.

At the point, the following PR is aimed to amend the specification and after that provide work for actual implementation of Spec and TCK.

@OlegDokuka
Copy link
Member Author

@OlegDokuka OlegDokuka force-pushed the master branch 2 times, most recently from 3e7d0e7 to 7ba2381 Compare January 17, 2019 00:40
tck/README.md Outdated
@@ -0,0 +1 @@
# Reactive Streams TCK #
Copy link

Choose a reason for hiding this comment

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

  • Add link to CONTRIBUTING doc
  • Add link to a code of conduct.

Copy link
Member Author

Choose a reason for hiding this comment

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

This just drafted a spec and not aimed to provide everything at once. Im planning to fix TCK readme once we agreed on spec. Then, after first step, going to fix docs. Does it sound good to you?

CONTRIBUTING.md Outdated

## Performing Official Releases

Creating binary artifacts, uploading them to central repositories and declaring these to be an official release of the Reactive Streams project requires the consent of all gatekeepers. The process is initiated by creating a ticket in the `reactive-streams` repository for this purpose and consent is signaled in the same way as for pull requests. The actual work of updating version numbers and publishing the artifacts will typically involve pull requests targeting the affected repositories.
Copy link
Contributor

Choose a reason for hiding this comment

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

update reactive-streams to reactive-streams-js

Copy link
Member Author

Choose a reason for hiding this comment

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

done

README.md Outdated
| <a name="1.2">2</a> | A `Publisher` MAY signal fewer `onNext` than requested and terminate the `Subscription` by calling `onComplete` or `onError`. |
| [:bulb:](#1.2 "1.2 explained") | *The intent of this rule is to make it clear that a Publisher cannot guarantee that it will be able to produce the number of elements requested; it simply might not be able to produce them all; it may be in a failed state; it may be empty or otherwise already completed.* |
| <a name="1.3">3</a> | `onSubscribe`, `onNext`, `onError` and `onComplete` signaled to a `Subscriber` MUST be signaled in a [thread-safe](#term_thread-safe) manner—and if performed by multiple threads—use [external synchronization](#term_ext_sync). |
| [:bulb:](#1.3 "1.3 explained") | *The intent of this rule is to make it clear that [external synchronization](#term_ext_sync) must be employed if the Publisher intends to send signals from multiple/different threads. Note, this rule is implemented by default in JavaScript, since it is [single-threaded](#term_single-threaded) within the same context* |
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing period after context

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

README.md Outdated
| <a name="2.6">6</a> | A `Subscriber` MUST call `Subscription.cancel()` if the `Subscription` is no longer needed. |
| [:bulb:](#2.6 "2.6 explained") | *The intent of this rule is to establish that Subscribers cannot just throw Subscriptions away when they are no longer needed, they have to call `cancel` so that resources held by that Subscription can be safely, and timely, reclaimed. An example of this would be a Subscriber which is only interested in a specific element, which would then cancel its Subscription to signal its completion to the Publisher.* |
| <a name="2.7">7</a> | A `Subscriber` MUST ensure that all calls on its `Subscription` take place from the same thread or provide for respective [external synchronization](#term_ext_sync). |
| [:bulb:](#2.7 "2.7 explained") | *The intent of this rule is to establish that [external synchronization](#term_ext_sync) must be added if a Subscriber will be using a Subscription concurrently by two or more threads. Note, this rule is implemented by default in JavaScript, since it is [single-threaded](#term_single-threaded) within the same context* |
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing period after context.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

README.md Outdated
| <a name="2.10">10</a> | A `Subscriber` MUST be prepared to receive an `onError` signal with or without a preceding `Subscription.request(n: number | bigint)` call. |
| [:bulb:](#2.10 "2.10 explained") | *The intent of this rule is to establish that Publisher failures may be completely unrelated to signalled demand. This means that Subscribers do not need to poll to find out if the Publisher will not be able to fulfill its requests.* |
| <a name="2.11">11</a> | A `Subscriber` MUST make sure that all calls on its [signal](#term_signal) methods happen-before the processing of the respective signals. I.e. the Subscriber must take care of properly publishing the signal to its processing logic. |
| [:bulb:](#2.11 "2.11 explained") | *The intent of this rule is to establish that it is the responsibility of the Subscriber implementation to make sure that asynchronous processing of its signals are thread safe. See [JMM definition of Happens-Before in section 17.4.5](https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5). Note, this rule is implemented by default in JavaScript, since it is [single-threaded](#term_single-threaded) within the same context* |
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing period after context.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

@viktorklang
Copy link
Contributor

@OlegDokuka It's a bit tricky to see the diff between the original README/spec and this one—mai I suggest adding the original first and creating the PR against it so the diff is "accurate"? :-)

@OlegDokuka
Copy link
Member Author

@viktorklang sure thing. Give me a minute

@OlegDokuka
Copy link
Member Author

OlegDokuka commented Feb 1, 2019

@viktorklang done. Added additional commit as a copy/paste from reactive-streams-jvm, so now you can observe introduced changes in that way ->
https://github.com/reactive-streams/reactive-streams-js/pull/5/files/d0b1e000b5baf7c5bbdf88b481fe020b716c841d..483069fc7f7cb1a9d2fa899545d4b29fa463a5ec

@viktorklang
Copy link
Contributor

@OlegDokuka Perfect! Thank YOU! ^^

@viktorklang
Copy link
Contributor

@reactive-streams/contributors Please have a look if you're interested!

@OlegDokuka
Copy link
Member Author

@viktorklang dont have an access to that

@viktorklang
Copy link
Contributor

@OlegDokuka you should be on @reactive-streams/js-contributors

@OlegDokuka
Copy link
Member Author

OlegDokuka commented Feb 1, 2019

Long.MAX_VALUE in JS

Regarding usage of BigInt in JS. Unfortunately, JS supports only 2^53 as a MAX save the number to increment without losing calculation precision. Therefore, to keep spec binary compatible with other languages, we have to use BigInt type which allows doing math up to 10^1000. However, BigInt has its own performance penalty. What I measured is that increment of BigInt is about 5 times slower than the increment of the plain Number. For example, the following naive benchmark for BigInt:

function benchmark() {
    const startTime = process.hrtime();
    let i = 0n; // here is n notation after digit means BigInt type 

    while (i < 100000000) {
        i = i + 1n;
    }

    console.log(i);

    const endTime = process.hrtime();

    return [(endTime[0] - startTime[0]), endTime[1] - startTime[1]];
}

on average results in (on my 2.9 GHz Intel Core i9):

[ 4, 472734712 ] 

where the first number in the array is Seconds and the second in is Nanos.

In contrast, the same code but for a plain Number type on the same machine:

function benchmark() {
    const startTime = process.hrtime();
    let i = 0;

    while (i < 100000000) {
        i = i + 1;
    }

    console.log(i);

    const endTime = process.hrtime();

    return [(endTime[0] - startTime[0]), endTime[1] - startTime[1]];
}

on average results in:

[ 0, 47754006 ]

which is about 4~5 times slower.

From our conversation with @viktorklang on Twitter

What I saw in Reactor Core JS, which was developed by @akarnokd:

David Karnok, developing reactor-core-js implemented that special signal in JS as this -> https://github.com/reactor/reactor-core-js/blob/master/src/subscriber.js#L50

Viktor Klang
When is Infinity reached by adding numbers?

11m 10 minutes ago
Oleh Dokuka

-> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity

Viktor Klang
I mean, if a Subscriber requests 1, and the Publisher doesn't publish anything (yet), when does the aggregation of demand reach the value Infinity?

6m 5 minutes ago
Guess more than iterating to Long.MAX_VALUE

the problem in js is that precision of calculation which overflows 2^53 becomes incorrect like the following

Oleh Dokuka

> Number.MAX_SAFE_INTEGER + 1000013
9007199255741004
> Number.MAX_SAFE_INTEGER + 1000015
9007199255741006
> Number.MAX_SAFE_INTEGER + 1000016
9007199255741008

@OlegDokuka
Copy link
Member Author

OlegDokuka commented Feb 1, 2019

Ahh, even though we tried to rely on incorrect Number calculation, in case one send us Long.MAX_VALUE - 1 we will not be able to do the right math. See the following sample -> https://jsbin.com/gujaxiqodu/edit?js,console

@rdegnan
Copy link

rdegnan commented Feb 1, 2019

I would recommend keeping request_n as Number instead of BigInt. I don't see a need to preserve binary compatibility with other languages within a single language reactive streams implementation, that should be in the domain of binary reactive streams network protocols. RSocket for example limits request_n frames to 2^31-1, so it is up to the RSocket implementation to maintain high/low watermarks and hand out request_n frames up to the requested amount.

@OlegDokuka
Copy link
Member Author

After some thoughts, I concluded and agree with @rdegnan that we could rely on Infinite object as a signal for PUSH-only model and use Number instead of BigInt since it will have never been reached from the perspective of the time taken for incrementing to that 10^1000 value as well as from the JS behavior which prevents incrementing/decrementing for Number type after some specific number.

Any thoughts @viktorklang @benlesh?

@viktorklang
Copy link
Contributor

@OlegDokuka Hmmm, if there is to be a bridge between implementations in different languages, think for instance in Graal, then that bridge would have to know to translate between Long.MAX_VALUE and Infinity. Do we htink that is going to be an issue?

@OlegDokuka
Copy link
Member Author

OlegDokuka commented Feb 4, 2019

@viktorklang I don't think this should be a big deal. I guess if such interop is somewhen, we can always provide an additional bridge for that which will catch Infinite from js and map it to Long.MAX_VALUE and vice versa.

@viktorklang
Copy link
Contributor

@OlegDokuka The thing is that such a bridge would have to keep track of cumulative demand which overflows Long.MAX_VALUE too.

@OlegDokuka
Copy link
Member Author

Another option - stick to Long.MAX_VALUE and leave details of the implementation for libs vendors. Anyway, using pure Number, it will not be possible to accumulate/decrement the requested amount by one started from 2^54.

On the other hand, in real-world scenarios - no-one will request data by one up to Long.MAX_VALUE. If such happens - either publisher should wait for 300 years (or in case of JS for half of the year) which is less possible ( guess such publisher should be considered as dead and closed by timeout) or size of request should be higher than 1 or 10 or 100, so in such case, if client increment by 1000 and sending speed is 1000 lower than request frequency, then at some point sending model will be considered as pure push model and in that way interop will be achieved

@viktorklang
Copy link
Contributor

@OlegDokuka What happens if someone asks for (Long.MAX_VALUE / 10) 10 times in a row?

@OlegDokuka
Copy link
Member Author

https://jsbin.com/nariqatexe/edit?js,console

that is funny)

@OlegDokuka
Copy link
Member Author

OlegDokuka commented Feb 4, 2019

@viktorklang OK, what if we stick to long max value and leave details of the implementation for vendors. For example, in Reactor, we skip validation of the negative request and provide some StrictSubscriber implementation which follows all the rules and applies to the given subscriber in case it is non-reactor ecosystem one

@OlegDokuka
Copy link
Member Author

@viktorklang provided last fixes to the spec related to the upper limit. Spec keeps clear a requirement of the upper limit equal to Long.MAX_VALUE. It does not specify a particular type of solution and does not limit API to bigint type. It mentions JS limits and advice vendors to provide appropriate math in order to achieve the right precision in tracking the requested amount.

If it looks good to you, we can merge it and I will send TCK shortly

@OlegDokuka OlegDokuka self-assigned this Feb 26, 2019
@viktorklang
Copy link
Contributor

@OlegDokuka Latest commit looks good to me. You might want to check the latest master in reactive-streams-jvm for some updated verbiage, such as: reactive-streams/reactive-streams-jvm@4d6cdb7

Nice work, Oleg!

@OlegDokuka
Copy link
Member Author

Waits resolution for reactive-streams/reactive-streams-jvm#448 so once we have it, amend latest fixes and merge this PR

@OlegDokuka OlegDokuka merged commit 1a82d50 into reactive-streams:master Mar 6, 2019
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.

5 participants