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

Feature: Reactive Soy #26

Closed
wants to merge 33 commits into from

Conversation

sgammon
Copy link
Contributor

@sgammon sgammon commented Nov 13, 2019

This changeset amends the Soy view renderer, to support a new interface called ReactiveViewRenderer. The new interface generates a Flowable instead of a Writable, so that reactive chunked responses can be generated when the renderer is waiting on Future values to resolve.

  • Refactor async views to use Flowable.generate
  • Rename AsyncViewsRenderer -> ReactiveViewRenderer
  • Render until told to DETACH or LIMIT(ED), then wait for future
  • Bug: Does not work for views that hit DETACH (see below)

sgammon added 29 commits August 22, 2019 10:37
This changeset introduces a new Micronaut package to render views
via *[Soy](https://github.com/google/closure-templates)* (also
known as *Closure Templates*).

For now, the renderer simply uses `SoyTofu` to render views from
Soy template sources. There are more efficient ways to render Soy
templates in Java, and I'm planning to add those, too, subject to
feedback from the Micronaut team.

I've based this proposal on the structure of `views-freemarker`,
to make sure I've adopted Micronaut's coding style as closely as
I can. There is a corresponding set of unit tests as well, that
pass, at least locally.

_NOTE:_ This change requires a patched version of Soy Java, which
applies a PR upstream to support querying a `SoyTofu` instance for
templates (see: google/closure-templates#185). A patched JAR for
Soy can be installed with the script `patch-soy.sh` added at the
root of the repo. This is run in Travis, for instance, but can be
eliminated when the upstream PR is merged and a release is cut.

Changes made:
- [x] Support for `SoyTofu` - i.e. rendering from template sources
  - [x] `SoyTofuRenderer` - where the rendering happens
  - [x] `SoyTofuViewsRendererConfiguration` - where a `SoyFileSet`
        is loaded and configured
  - [x] `SoyFileSetProvider` - uses DI to resolve a `SoyFileSet`,
        which is used to back the `SoyTofu.Renderer`
- [x] Unit tests for `SoyTofuViewsRenderer`
- [ ] Coming shortly: `SoySauce` - i.e. rendering via templates
      pre-compiled to Java sources
- Add bean for generating or using `SoySauce`-based renderer
- Move `AppendableToWritable` to its own file, enhance it with
  support for `AdvisingAppendable`
- Add `SoySauceRenderer`
- Add ability for `SoyFileSetProvider` to provide pre-compiled
  templates via an existing `SoySauce` instance
Force-SecureRandom defaults to `false`, to avoid infinitely blocking
if there isn't enough entropy available. This can be mitigated with
an overridden Java system property in Linux like so:

-Djava.security.egd=file:/dev/./urandom
This changeset proposes a structure for an alternate `View`
annotation, `AsyncView`. If `AsyncView` is used with a controller,
it will try to load an `AsyncViewsRenderer` rather than a regular
one, which is allowed to return a `Publisher` rather than producing
an HTTP response synchronously from template context.

Changes so far:
- [x] Add `AsyncView` annotation
- [x] Add `AsyncViewsRenderer` interface
- [x] Refactor common items to `BaseViewsRenderer`
- [x] Make `ViewsRenderer` comply with `BaseViewsRenderer`
This changeset builds upon PRs micronaut-projects#13 (*Soy Views Renderer*) and micronaut-projects#14
(*CSP Nonces*) to enable Soy with integrated CSP support when used
with Micronaut.

The CSP nonce may be provided via the Soy injected data, as
documented [here](https://csp.withgoogle.com/docs/faq.html#passing-nonces).
When the nonce value is provided, this way, inline scripts will
automatically gain a `nonce="{nonceValue"}` attribute.

All one must do in Micronaut to enable this is:
```
micronaut.views.soy.enabled = true
micronaut.views.soy.engine = "sauce"
micronaut.views.csp.enabled = true
micronaut.views.csp.generateNonce = true
micronaut.views.csp.policyDirectives = "default-src self:; script-src 'nonce-{#nonceValue}';"
```

And the framework will take care of the rest.

Changes so far:
- [x] Resolve CSP nonce when rendering templates via Soy
- [x] Provide CSP nonce to Soy render context
- [x] Add unit tests to make sure nonce lines up, is injected
      on both `<script>` tags and headers
This changeset amends the Soy view renderer, to support a new
interface called `ReactiveViewRenderer`. The new interface
generates a `Flowable` instead of a `Writable`, so that reactive
chunked responses can be generated when the renderer is waiting
on `Future` values to resolve.

- [x] Refactor async views to use Flowable.generate
- [x] Rename AsyncViewsRenderer -> ReactiveViewRenderer
- [x] Render until told to DETACH or LIMIT(ED), then wait for future
- [ ] Bug: Does not work for views that hit DETACH (see below)
@sgammon sgammon mentioned this pull request Nov 13, 2019
7 tasks
@sgammon
Copy link
Contributor Author

sgammon commented Nov 13, 2019

@graemerocher This works great, save for one rather large issue. When Soy gives the DETACH signal, indicating it is waiting on a Future value of some kind, the renderer correctly yields out and waits for another turn of Flowable.generate. It continues the render, finalizing with emitter.onNext() and emitter.onComplete(), which should terminate the flow.

But, it doesn't terminate it. It just hangs somehow, forever, never emitting a response to the unit test client I have setup in SoySauceReactiveRendererSpec.

I've debugged extensively and added plenty of debug/trace logging. The trace log appears to be the correct flow. I'll take another crack at debugging this but it would help immensely if you could take a look - perhaps what I'm doing wrong is simple.

Here is the trace log, which can be obtained by running SoySauceReactiveRendererSpec:

22:49:47.936 [nioEventLoopGroup-1-4] DEBUG i.m.context.condition.Condition - Bean [Definition: io.micronaut.management.endpoint.caches.CachesEndpoint] will not be loaded due to failing conditions:
22:49:47.936 [nioEventLoopGroup-1-4] DEBUG i.m.context.condition.Condition - * Custom condition [class io.micronaut.management.endpoint.EndpointEnabledCondition] failed evaluation
22:49:47.938 [nioEventLoopGroup-1-4] DEBUG i.m.context.condition.Condition - Bean [Definition: io.micronaut.management.endpoint.stop.ServerStopEndpoint] will not be loaded due to failing conditions:
22:49:47.938 [nioEventLoopGroup-1-4] DEBUG i.m.context.condition.Condition - * Custom condition [class io.micronaut.management.endpoint.EndpointEnabledCondition] failed evaluation
22:49:47.975 [pool-1-thread-1] INFO  io.micronaut.docs.SoyController - Rendering async Soy page.
22:49:47.978 [pool-1-thread-1] INFO  io.micronaut.docs.SoyController - Kicking off async task
22:49:48.011 [pool-1-thread-1] INFO  io.micronaut.docs.SoyController - Task is immediately: NOT DONE.
22:49:48.028 [pool-1-thread-1] DEBUG i.m.views.soy.SoySauceViewsRenderer - Preparing render for template path 'sample.home'
22:49:48.063 [pool-1-thread-1] DEBUG i.m.views.soy.SoySauceViewsRenderer - CSP nonce injection is active.
22:49:48.063 [pool-1-thread-1] TRACE i.m.views.soy.SoySauceViewsRenderer - Injecting nonce value 'ldHUV1x8Bah93o7B3SNrJA'.
22:49:48.064 [pool-1-thread-1] DEBUG i.m.views.soy.SoySauceViewsRenderer - Style renaming is disabled.
22:49:48.078 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Allocating chunk of sizing = i: 1024, m: 2048
22:49:48.080 [pool-1-thread-1] TRACE i.m.views.soy.SoySauceViewsRenderer - Initial render for view 'sample.home'
22:49:48.085 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 30 (c: 30, b: 0)
22:49:48.085 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:48.085 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 56 (c: 86, b: 0)
22:49:48.085 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:48.087 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 8 (c: 94, b: 0)
22:49:48.087 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:48.088 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 22 (c: 116, b: 0)
22:49:48.088 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:48.088 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 1 (c: 117, b: 0)
22:49:48.088 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:48.088 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 44 (c: 161, b: 0)
22:49:48.088 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:48.088 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 20 (c: 181, b: 0)
22:49:48.089 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:48.090 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 7 (c: 188, b: 0)
22:49:48.090 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 12 (c: 200, b: 0)
22:49:48.090 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:48.091 [pool-1-thread-1] DEBUG i.m.views.soy.SoySauceViewsRenderer - Render emitting chunk
22:49:48.093 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Allocating chunk of sizing = i: 1024, m: 2048
22:49:48.094 [pool-1-thread-1] DEBUG io.micronaut.views.soy.SoyRender - Exporting capped chunk: ReadOnlyByteBuf(ridx: 0, widx: 200, cap: 200/200, unwrapped: UnpooledSlicedByteBuf(ridx: 0, widx: 200, cap: 200/200, unwrapped: CompositeByteBuf(ridx: 200, widx: 200, cap: 200, components=1)))
22:49:48.110 [pool-1-thread-1] DEBUG io.micronaut.views.soy.SoyRender - SoyRender received DETACH signal.
22:49:48.110 [pool-1-thread-1] TRACE i.m.views.soy.SoySauceViewsRenderer - Continue render for view 'sample.home'
22:49:48.110 [pool-1-thread-1] DEBUG io.micronaut.views.soy.SoyRender - SoyRender received DETACH signal.
22:49:48.111 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Still waiting on future value
22:49:48.111 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Future value is not yet ready. Blocking for return.
22:49:53.006 [pool-3-thread-1] INFO  io.micronaut.docs.SoyController - Emitting async content
22:49:53.008 [pool-3-thread-1] INFO  io.micronaut.docs.SoyController - Told executor to kick rocks
22:49:53.009 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Future value block finished.
22:49:53.009 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Future value has arrived: SoyRender is READY.
22:49:53.009 [pool-1-thread-1] DEBUG i.m.views.soy.SoySauceViewsRenderer - Render is READY to proceed. Continuing.
22:49:53.009 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 10 (c: 10, b: 200)
22:49:53.009 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:53.009 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 7 (c: 17, b: 200)
22:49:53.010 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 14 (c: 31, b: 200)
22:49:53.010 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:53.010 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Appended char seq of size = 14 (c: 45, b: 200)
22:49:53.010 [pool-1-thread-1] TRACE io.micronaut.views.soy.SoyRender - Soft limit not yet reached
22:49:53.011 [pool-1-thread-1] DEBUG i.m.views.soy.SoySauceViewsRenderer - Render emitting chunk
22:49:53.011 [pool-1-thread-1] TRACE i.m.views.soy.SoyResponseBuffer - Allocating chunk of sizing = i: 1024, m: 2048
22:49:53.012 [pool-1-thread-1] DEBUG io.micronaut.views.soy.SoyRender - Exporting capped chunk: ReadOnlyByteBuf(ridx: 0, widx: 45, cap: 45/45, unwrapped: UnpooledSlicedByteBuf(ridx: 0, widx: 45, cap: 45/45, unwrapped: CompositeByteBuf(ridx: 245, widx: 245, cap: 245, components=2)))
22:49:53.012 [pool-1-thread-1] DEBUG io.micronaut.views.soy.SoyRender - SoyRender flow is DONE.
22:49:53.012 [pool-1-thread-1] DEBUG i.m.views.soy.SoySauceViewsRenderer - Finished Soy render routine. Calling `onComplete`.
22:49:53.012 [pool-1-thread-1] DEBUG io.micronaut.views.soy.SoyRender - Closing render buffer (state: CLOSED)
22:49:53.012 [pool-1-thread-1] DEBUG i.m.views.soy.SoyResponseBuffer - Closing render buffer (state: CLOSED)
22:49:53.013 [pool-1-thread-1] DEBUG i.m.views.soy.SoyResponseBuffer - Chunk references = 0
22:49:53.013 [pool-1-thread-1] DEBUG i.m.views.soy.SoyResponseBuffer - Buffer references = 0

Expected no exception to be thrown, but got 'io.micronaut.http.client.exceptions.ReadTimeoutException'
Expected no exception to be thrown, but got 'io.micronaut.http.client.exceptions.ReadTimeoutException'
	at spock.lang.Specification.noExceptionThrown(Specification.java:118)
	at io.micronaut.docs.SoySauceReactiveRendererSpec.invoking /soy/asyncContent should produce a chunked response(SoySauceReactiveRendererSpec.groovy:69)
Caused by: io.micronaut.http.client.exceptions.ReadTimeoutException: Read Timeout
	at io.micronaut.http.client.exceptions.ReadTimeoutException.<clinit>(ReadTimeoutException.java:26)
	at io.micronaut.http.client.DefaultHttpClient$10.exceptionCaught(DefaultHttpClient.java:1917)
	at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:285)
	at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:264)
	at io.netty.channel.AbstractChannelHandlerContext.fireExceptionCaught(AbstractChannelHandlerContext.java:256)

the log shows:

  1. the initial part of the response gathering in the buffer (Appended char seq...), then
  2. emitting the first chunk (Exporting capped chunk...), then
  3. the DETACH signal, and the generate routine kicking out, then
  4. the engine calls back, this time it waits on the future, receives a value
  5. more Appended char seq... as it keeps rendering, with the new future in-hand
  6. Exporting capped chunk... again, this time 45 bytes to finish the stream, which should be chunk 2 in a Transfer-Encoding: chunked response
  7. it notes where it calls onComplete
  8. but then nothing happens. the next thing in the log is a read timeout, which i've tried at 8, 20, and 300 seconds.

AFAIK nothing else is broken. all other tests pass, including the existing suite for Soy. it's just when there is a future value in the Soy render context that isn't immediately ready.

@sgammon sgammon force-pushed the soy/async-chunked branch 2 times, most recently from 613e267 to 9a5aae1 Compare November 13, 2019 08:37
@graemerocher
Copy link
Contributor

I will take a look shortly. Thanks.

@sgammon
Copy link
Contributor Author

sgammon commented Nov 13, 2019

@graemerocher Two ideas popped into my head in case they are relevant. To yield a chunk, I am issuing a read-only slice of the backing render buffer (CompositeByteBuf), wrapping them in ByteBuffer (via ByteBufferFactory.wrap) and then emitting them up to Micronaut. These chunks each have a read index of 0 and a write index at their full capacity when emitted, which is set to the size of the chunk, not the render buffer.

The read-only slice is retained. After more real-world testing, I get a reference count for the underlying buffer (at close time) that approaches 10/12. Could it be that:

  1. Micronaut doesn't release wrapped buffers and that is my responsibility to do? But I figure it is unlikely that an unreleased buffer would hang a response. Would still like to check into the proper procedure here to prevent leaks.

  2. Perhaps Micronaut expects the full HTTP response and buffer to be yielded each time, but with a stateful read and write index, rather than a new buffer for each chunk?

If you point me to the relevant code, I'd be happy to understand more and try and pry it open, perhaps with some trace logging. Thank you for taking a look

EDIT: one final note. once we get this working, i'm happy to clean up this PR, add some more testing for the chunking/buffer sizing, and squash/rebase on master.

@graemerocher
Copy link
Contributor

@graemerocher
Copy link
Contributor

What may be missing is the writing of the DefaultLastHttpContent

@sgammon
Copy link
Contributor Author

sgammon commented Nov 13, 2019

@graemerocher Perfect! Thank you. I'll try debugging starting from isStreaming onward.

If I can generate a more useful log, I'll post it.

@sgammon
Copy link
Contributor Author

sgammon commented Nov 15, 2019

@graemerocher I took a look at the code you pointed out, but I don't quite understand how DefaultLastHttpContent works into the flow here. Do I emit that myself? It appears as though RoutingInBoundHandler only invokes DefaultLastHttpContent when handling SSE streams, but perhaps I'm missing something.

@graemerocher
Copy link
Contributor

@sgammon I will be looking at this this afternoon. Sorry for the delay

@graemerocher
Copy link
Contributor

Yeah so the issue is that the decision to switch to streaming the data is statically decided based on the return type of the controller method, not based on the body of the response emitted. We need to add the streaming logic to core before we can proceed ahead with this otherwise dynamically switching to streaming is not possible.

@sgammon
Copy link
Contributor Author

sgammon commented Nov 21, 2019

@graemerocher no problem on the delay, thank you for taking the time to look :)

i see, well that does explain it. for now, this isn't a blocker for us, because we resolve all futures before rendering anyway. but i am happy to pick it back up again or contribute to get it merged eventually. thank you again for your help

@sgammon
Copy link
Contributor Author

sgammon commented Feb 13, 2020

@graemerocher, happy new year - is there any update on the upstream support for dynamically deciding on streaming vs. buffered responses?

@sgammon
Copy link
Contributor Author

sgammon commented Feb 26, 2020

closing this because it is superseded by #34 (Reactive Soy v2)

@sgammon sgammon closed this Feb 26, 2020
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.

2 participants