Groovy/Grails promises/futures. There is no .resol

2019-07-19 13:48发布

问题:

I am developing in a Grails application. What I want to do is to lock the request/response, create a promise, and let someone else resolve it, that is somewhere else in the code, and then flush the response.

What I find really strange is that the Promise promise = task {} interface has no method that resembles resolve or similar.

I need to lock the response until someone resolves the promise, which is a global/static property set in development mode.

Promise interface: http://grails.org/doc/latest/api/grails/async/Promise.html

I have looked at the GPars doc and can't find anything there that resembles a resolve method.

How can I create a promise, that locks the response or request, and then flushes the response when someone resolves it?

回答1:

You can call get() on the promise which will block until whatever the task is doing completes, but I imagine what that is not what you want. What you want seems to be equivalent to a GPars DataflowVariable:

http://gpars.org/1.0.0/javadoc/groovyx/gpars/dataflow/DataflowVariable.html

Which allows using the left shift operator to resolve the value from another thread. Currently there is no way to use the left shift operator via Grails directly, but since Grails' promise API is just a layer over GPars this can probably be accomplished by using the GPars API directly with something like:

 import org.grails.async.factory.gpars.*
 import groovyx.gpars.dataflow.*
 import static grails.async.Promise.*

 def myAction() {
    def dataflowVar = new DataflowVariable()
    task {
       // do some calculation and resolve data flow variable
       def expensiveData = ...
       dataflowVar << expensiveData
    }
    return new GParsPromise(dataflowVar)        
 }


回答2:

It took me quite some time to get around this and have a working answer.

I must say that it appears as if Grails is quite a long way of making this work properly.

task { }

will always execute immediatly, so the call is not put on hold until dispatch() or whatever is invoked which is a problem.

Try this to see:

    public def test() {
            def dataflowVar = new groovyx.gpars.dataflow.DataflowVariable()

            task {
                    // do some calculation and resolve data flow variable
                    println '1111111111111111111111111111111111111111111111111111'
                    //dataflowVar << expensiveData
            }

            return new org.grails.async.factory.gpars.GparsPromise(dataflowVar);
    }

If you are wondering what this is for, it is to make the lesscss refresh automatically in grails, which is a problem when you are using import statements in less. When the file is touched, the lesscss compiler will trigger a recompilation, and only when it is done should it respond to the client.

On the client side I have some javascript that keeps replacing the last using the refresh action here:

In my controller:

    /**
     * Refreshes link resources. refresh?uri=/resource/in/web-app/such/as/empty.less
     */
    public def refresh() {
            return LessRefresh.stackRequest(request, params.uri);
    }

A class written for this:

import grails.util.Environment
import grails.util.Holders

import javax.servlet.AsyncContext
import javax.servlet.AsyncEvent
import javax.servlet.AsyncListener
import javax.servlet.http.HttpServletRequest

/**
 * @Author SecretService
 */
class LessRefresh {
        static final Map<String, LessRefresh> FILES = new LinkedHashMap<String, LessRefresh>();

        String file;
        Boolean touched
        List<AsyncContext> asyncContexts = new ArrayList<AsyncContext>();
        String text;

        public LessRefresh(String file) {
                this.file = file;
        }

        /** Each request will be put on hold in a stack until dispatchAll below is called when the recompilation of the less file finished **/
        public static AsyncContext stackRequest(HttpServletRequest request, String file) {
                if ( !LessRefresh.FILES[file] ) {
                        LessRefresh.FILES[file] = new LessRefresh(file);
                }

                return LessRefresh.FILES[file].handleRequest(request);
        }

        public AsyncContext handleRequest(HttpServletRequest request) {
                if ( Environment.current == Environment.DEVELOPMENT ) {

                        // We only touch it once since we are still waiting for the less compiler to finish from previous edits and recompilation
                        if ( !touched ) {
                                touched = true
                                touchFile(file);
                        }

                        AsyncContext asyncContext = request.startAsync();

                        asyncContext.setTimeout(10000)

                        asyncContexts.add (asyncContext);

                        asyncContext.addListener(new AsyncListener() {

                                @Override
                                void onComplete(AsyncEvent event) throws IOException {
                                        event.getSuppliedResponse().writer << text;
                                }

                                @Override
                                void onTimeout(AsyncEvent event) throws IOException {

                                }

                                @Override
                                void onError(AsyncEvent event) throws IOException {

                                }

                                @Override
                                void onStartAsync(AsyncEvent event) throws IOException {

                                }
                        });

                        return asyncContext;
                }

                return null;
        }

        /** When recompilation is done, dispatchAll is called from LesscssResourceMapper.groovy **/
        public void dispatchAll(String text) {
                this.text = text;

                if ( asyncContexts ) {

                        // Process all
                        while ( asyncContexts.size() ) {

                                AsyncContext asyncContext = asyncContexts.remove(0);

                                asyncContext.dispatch();
                        }

                }

                touched = false;
        }

        /** A touch of the lessfile will trigger a recompilation **/
        int count = 0;
        void touchFile(String uri) {
                if ( Environment.current == Environment.DEVELOPMENT ) {
                        File file = getWebappFile(uri);

                        if (file && file.exists() ) {
                                ++count;

                                if ( count < 5000 ) {
                                        file << ' ';
                                }
                                else {
                                        count = 0

                                        file.write( file.getText().trim() )
                                }
                        }
                }
        }

        static File getWebappFile(String uri) {
                new File( Holders.getServletContext().getRealPath( uri ) )
        }

}

In LesscssResourceMapper.groovy of the lesscsss-recources plugin:

    ...
    try {
            lessCompiler.compile input, target
            // Update mapping entry
            // We need to reference the new css file from now on
            resource.processedFile = target
            // Not sure if i really need these
            resource.sourceUrlExtension = 'css'
            resource.contentType = 'text/css'
            resource.tagAttributes?.rel = 'stylesheet'

            resource.updateActualUrlFromProcessedFile()

            // ==========================================
            // Call made here!
            // ==========================================
            LessRefresh.FILES[resource.sourceUrl.toString()]?.dispatchAll( target.getText() );

    } catch (LessException e) {
            log.error("error compiling less file: ${originalFile}", e)
    }
    ...

In the index.gsp file:

<g:set var="uri" value="${"${App.files.root}App/styles/empty.less"}"/>
<link media="screen, projection" rel="stylesheet" type="text/css" href="${r.resource(uri:uri)}" refresh="${g.createLink(controller:'home', action:'refresh', params:[uri:uri])}" resource="true">

JavaScript method refreshResources to replace the previous link href=...

    /**
     * Should only be used in development mode
     */
    function refreshResources(o) {
            o || (o = {});

            var timeoutBegin      = o.timeoutBegin      || 1000;
            var intervalRefresh   = o.intervalRefresh   || 1000;
            var timeoutBlinkAvoid = o.timeoutBlinkAvoid || 400 ;
            var maxErrors         = o.maxErrors         || 200 ;

            var xpath = 'link[resource][type="text/css"]';

            // Find all link[resource]
            $(xpath).each(function(i, element) {
                    refresh( $(element) );
            });

            function refresh(element) {

                    var parent     = element.parent();
                    var next       = element.next();
                    var outer      = element.clone().attr('href', '').wrap('<p>').parent().html();
                    var uri        = element.attr('refresh');
                    var errorCount = 0;

                    function replaceLink() {
                            var link = $(outer);

                            link.load(function () {
                                    // The link has been successfully added! Now remove the other ones, then do again

                                    errorCount = 0;

                                    // setTimeout needed to avoid blinking, we allow duplicates for a few milliseconds
                                    setTimeout(function() {
                                            var links = parent.find(xpath + '[refresh="'+uri+'"]');

                                            var i = 0;
                                            // Remove all but this one
                                            while ( i < links.length - 1 ) {
                                                    links[i++].remove();
                                            }

                                            replaceLinkTimeout();

                                    }, timeoutBlinkAvoid );

                            });

                            link.error(function(event, handler) {
                                    console.log('Error refreshing: ' + outer );

                                    ++errorCount;

                                    if ( errorCount < maxErrors ) {
                                            // Load error, it happens. Remove this & redo!
                                            link.remove();

                                            replaceLink();
                                    }
                                    else {
                                            console.log('Refresh: Aborting!')
                                    }

                            });

                            link.attr('href', urlRandom(uri)).get(0);
                            link.insertBefore(next); // Insert just after
                    }

                    function urlRandom(uri) {
                            return uri + "&rand=" + Math.random();
                    }

                    function replaceLinkTimeout() {
                            setTimeout(function() {
                                    replaceLink();
                            }, intervalRefresh ) ;

                    }

                    // Waith 1s before triggering the interval
                    setTimeout(function() {
                            replaceLinkTimeout();
                    }, timeoutBegin);
            }

    };

Comments

I am unsure why Javascript style promises have not been added to the Grails stack. You can not render or stuff like that in the onComplete. render, redirect and what not are not available.

Something tells me that Grails and Promises/Futures are not there yet. The design of the GPars libraries seems not take into account of the core features which is to resolve later. At least it is not simple to do so.

It would be great if the dispatch() method actually could be invoked with some paramaters to pass from the resolving context. I am able to go around this using static properties.

I might continue to write my own solution and possibly contribute with a more fitting solutions around the AsyncContext class, but for now, this is enough for me.

I just wanted to refresh my less resources automatically.

Phew...

EDIT:

I made it to support several number of files. It is complete now!