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

Add batch request features. #505

Closed
K1vs opened this issue Jun 3, 2015 · 4 comments
Closed

Add batch request features. #505

K1vs opened this issue Jun 3, 2015 · 4 comments

Comments

@K1vs
Copy link

K1vs commented Jun 3, 2015

Hello!
I use ABP with angular for development my SPA web application and get a typical problem. This problem consist of browser limitation for parallel ajax request. For example google chrome can only 6 parallel request and all other request waiting.
I solve this problem using request batching. In js side i write a libraly for implicit batch use.

var abp = abp || {};
(function () {
    //main interface
    abp.batch = {
        //Start a batch. If name non specified it set to guid. Return name of batch
        begin : function(name) {
            return _beginBatch(name);
        },
        //End current open batch. Specify name for control youself. Call of this method can be expected. Return nema of current batch
        end: function (name) {
            return _endBatch(name);
        },
        //Sent batch with name to server. If batch is open can use without name. For send requests normaly use nobatch=true
        send: function (name, nobatch) {
            return _sendBatch(name, nobatch);
        },
        //Close batch with name and remove requests.If batch is open can use without name.
        close: function (name) {
            return _cancelBatch(name);
        },
        //batch query options
        defaultOpts: {
            dataType: 'json',
            type: 'POST',
            contentType: 'application/json',
            url:  'http://localhost:4285/api/batch'
        }
    };

    //fields
    var _batches = {};
    var _currentBatch = null;


    //implantation to abp.ajax
    var origin_abp_ajax = abp.ajax;
    var origin_abp_ajax_defaultOpts = abp.ajax.defaultOpts;
    abp.ajax = function (userOptions) {
        if (_currentBatch) {
            return _interceptRequest(userOptions);
        }
        else {
            return origin_abp_ajax(userOptions);
        }
    };
    abp.ajax.defaultOpts = origin_abp_ajax_defaultOpts;

    //core
    var _beginBatch = function (name) {
        if (_currentBatch)
            throw "Previous batch is open.";
        if (!name)
            name = guid();
        if (_batches[name])
            throw "This name is not free.";
        _currentBatch = newBatch(name);
        return name;
    };
    var _endBatch = function (name) {
        if (!_currentBatch)
            throw "Batch not open.";
        if (name && _currentBatch.name !== name)
            throw "Name mismatch. Begin name is " + _currentBatch.name + ", End name is " + name;
        _batches[_currentBatch.name] = _currentBatch;
        var result = _currentBatch.name;
        _currentBatch = null;
        return result;
    }
    var _sendBatch = function (name,nobatch) {
        if (name) {
            if (_batches[name]) {
                if (nobatch) {
                    _sendNoBatchRequest(_batches[name]);            
                } else {
                    _sendBachRequest(_batches[name]);
                }
                var result = _batches[name];
                delete _batches[name];
                return result;
            }
            else {
                throw "Batch with this name not exist";
            }
        }
        else {
            if (!_currentBatch)
                throw "Batch not open. For send specified batch specify name param.";
            if (nobatch) {
                _sendNoBatchRequest(_currentBatch);
            } else {
                _sendBachRequest(_currentBatch);
            }
            var result = _batches[name];
            _cancelBatch = null;
            return result;
        }
    }
    var _cancelBatch = function (name) {
        if (name) {
            if (_batches[name]) {
                delete _batches[name];
            }
            else {
                throw "Batch with this name not exist";
            }
        }
        else {
            if (!_currentBatch)
                throw "Batch not open. For cancel specified batch specify name param.";
            _currentBatch = null;
        }
    }


    //handle request and add it to current bach entry, return deferred
    var _interceptRequest = function (userOptions) {
        var requestBatchEntry = newEntry(userOptions);
        _currentBatch.entries.push(requestBatchEntry);
        return $.Deferred(function ($dfd) {
            requestBatchEntry.dfd = $dfd;
        });
    };
    //send via ajax request to server with bach data
    var _sendBachRequest = function (batchObject) {
        var batchDataObject = []
        $.each(batchObject.entries, function (index, entry) {
            entry.options.data = JSON.parse(entry.options.data);
            batchDataObject.push(entry.options);
        });
        var options = $.extend({}, abp.batch.defaultOpts, { data: JSON.stringify(batchDataObject) });
        $.ajax(options)
        .done(function (data) {
            _handleBatchResult(batchObject, data);
        })
        .fail(function () {
            _handleBatchResult(batchObject, null, arguments);
        });
    };

    var _handleBatchResult = function (batchObject, batchResult) {
        var isOk = batchResult && batchObject.entries.length === batchObject.entries.length;
        $.each(batchObject.entries, function (index, entry) {
            if (isOk) {
                var resultForEntry = JSON.parse(batchResult[index].data);
                abp.ajaxHelper.handleData(resultForEntry, entry.options, entry.dfd);
            } else {
                entry.dfd.reject.apply(this, arguments);
            }
        });
    }

    //send all request simple parallel, no batch
    var _sendNoBatchRequest = function (batchObject) {
        $.each(batchObject.entries, function (index, entry) {
            var options = $.extend({}, abp.ajax.defaultOpts, entry.options);
            $.ajax(options)
                .done(function (data) {
                    abp.ajaxHelper.handleData(data, entry.options, entry.dfd);
                }).fail(function () {
                    entry.dfd.reject.apply(this, arguments);
                });
        });
    };



    //helpers
    var newBatch = function (batchName) {
        return { name: batchName, entries: [] };
    };
    var newEntry = function (options) {
        var requestGuid = guid();
        return { guid: requestGuid, options: options};
    }




})();

Also, i must added the line into you file abp.jquery.js
abp.ajaxHelper = abpAjaxHelper; //warn vervion update.
Using example

abp.batch.start('batchName');
abp.services.servicName1.methodName1({});
abp.services.servicName1.methodName1({});
abp.services.servicName1.methodName1({});
abp.batch.end('batchName');
abp.batch.send('batchName');

In server side i added configuration

ConfigureAuth(app);
   HttpConfiguration congiguration = new HttpConfiguration();
  congiguration.Routes.MapHttpBatchRoute(
                routeName: "batch",
                routeTemplate: "api/batch",
                batchHandler: new WebApi.Handlers.JsonBatchHandler(GlobalConfiguration.DefaultServer)
             );
            app.UseWebApi(congiguration);

Also, i override DefaultBatchHandler for handle json.

public class JsonBatchHandler : DefaultHttpBatchHandler
{
        private const string APPLICATION_JSON_CONTENT_TYPE = "application/json";
        private Stopwatch sw = new Stopwatch();
        public JsonBatchHandler(HttpServer server)
            : base(server)
        {
            SupportedContentTypes.Add("text/json");
            SupportedContentTypes.Add(APPLICATION_JSON_CONTENT_TYPE);
            ExecutionOrder = BatchExecutionOrder.Sequential;
        }

        public async override Task<IList<HttpRequestMessage>> ParseBatchRequestsAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            string jsonSubRequestsString = await request.Content.ReadAsStringAsync();
            var jsonSubRequests = Newtonsoft.Json.JsonConvert.DeserializeObject<IList<JsonRequestMessage>>(jsonSubRequestsString);
            var subRequests = jsonSubRequests.Select(r =>
            {
                string urlStr = string.Format("{0}://{1}{2}{3}", request.RequestUri.Scheme, request.RequestUri.Host, request.RequestUri.IsDefaultPort ? "" : ":" + request.RequestUri.Port, r.url);
                Uri subRequestUri = new Uri(urlStr);

                var rm = new HttpRequestMessage(new HttpMethod(r.type), subRequestUri);
                foreach (var item in request.Headers)
                {
                    rm.Headers.Add(item.Key, item.Value);
                }
                rm.Content = new StringContent(r.data.ToString(), Encoding.UTF8,
                                    APPLICATION_JSON_CONTENT_TYPE);

                rm.CopyBatchRequestProperties(request);

                return rm;
            });
            return subRequests.ToList();
        }

        public async override Task<HttpResponseMessage> CreateResponseMessageAsync(IList<HttpResponseMessage> responses,
                                                                                   HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {      
            List<JsonResponseMessage> jsonResponses = new List<JsonResponseMessage>();
            foreach (var subResponse in responses)
            {
                var jsonResponse = new JsonResponseMessage
                {
                    code = (int)subResponse.StatusCode
                };
                foreach (var header in subResponse.Headers)
                {
                    jsonResponse.headers.Add(header.Key, String.Join(",", header.Value));
                }
                if (subResponse.Content != null)
                {
                    jsonResponse.data = await subResponse.Content.ReadAsStringAsync();
                    foreach (var header in subResponse.Content.Headers)
                    {
                        jsonResponse.headers.Add(header.Key, String.Join(",", header.Value));
                    }
                }
                jsonResponses.Add(jsonResponse);
            }

            return request.CreateResponse<List<JsonResponseMessage>>(HttpStatusCode.OK, jsonResponses);
        }

        public class JsonResponseMessage
        {
            public JsonResponseMessage()
            {
                headers = new Dictionary<string, string>();
            }

            public int code { get; set; }

            public Dictionary<string, string> headers { get; set; }

            public string data { get; set; }
        }

        public class JsonRequestMessage
        {
            public string type { get; set; }

            public string url { get; set; }

            public JObject data { get; set; }
        }
 }

This idea work very nice! Can you integrate this features to abp in future?

@hikalkan hikalkan modified the milestones: After ABP v1.0.0, ABP v1.0.0 Jun 3, 2015
@hikalkan
Copy link
Member

hikalkan commented Jun 3, 2015

Hi,

Thank you very much for sharing your codes. This is one feature I always wanted to implement. I definetely want to integrate it to ABP. I added it to TODO list and your codes will help while implementing it. Thanks a lot.

@hikalkan hikalkan modified the milestones: v1.0.0, After v1.0.0 Jun 30, 2016
@guilmori
Copy link

I'm really interested in that feature.
Where is it in the planning ?

@hikalkan
Copy link
Member

It's not designed/planned yet.

@guilmori
Copy link

We tend more and more towards micro services, where a screen may be composed of multiple independent ui components. So having a mechanism built into the system to handle a batch request for all components initial load requests would greatly improve performance.

Just throwing out our use case, and casting my vote.

Thanks!

@hikalkan hikalkan removed the feature label Oct 4, 2018
@hikalkan hikalkan removed this from the Backlog milestone Oct 4, 2018
@hikalkan hikalkan closed this as completed Oct 4, 2018
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

No branches or pull requests

3 participants