Understanding the Spring MVC class DeferredResult in the context of the github spring -mvc-chat application

I am trying to better understand how the following spring mvc 3.2 application works: https://github.com/rstoyanchev/spring-mvc-chat

My question is about deferredResult spring MVC class . I noticed that at the moment on the chatRequests map chatRequests are as many records as there are users connected to the chat application.

Say there are 3 users connected to the chat application. You will see that when user # 3 sends a message (see postMessage method below), the for loop (in the postMessage method) is repeated three times . I can’t understand why this is so.

I include the sample code below.

Code for controller:

 @Controller @RequestMapping("/mvc/chat") public class ChatController { private final ChatRepository chatRepository; private final Map<DeferredResult<List<String>>, Integer> chatRequests = new ConcurrentHashMap<DeferredResult<List<String>>, Integer>(); @Autowired public ChatController(ChatRepository chatRepository) { this.chatRepository = chatRepository; } @RequestMapping(method = RequestMethod.GET) @ResponseBody public DeferredResult<List<String>> getMessages(@RequestParam int messageIndex) { final DeferredResult<List<String>> deferredResult = new DeferredResult<List<String>>(null, Collections.emptyList()); this.chatRequests.put(deferredResult, messageIndex); deferredResult.onCompletion(new Runnable() { @Override public void run() { chatRequests.remove(deferredResult); } }); List<String> messages = this.chatRepository.getMessages(messageIndex); if (!messages.isEmpty()) { deferredResult.setResult(messages); } return deferredResult; } @RequestMapping(method = RequestMethod.POST) @ResponseBody public void postMessage(@RequestParam String message) { this.chatRepository.addMessage(message); // Update all chat requests as part of the POST request // See Redis branch for a more sophisticated, non-blocking approach for (Entry<DeferredResult<List<String>>, Integer> entry : this.chatRequests.entrySet()) { List<String> messages = this.chatRepository.getMessages(entry.getValue()); entry.getKey().setResult(messages); } } } 

Javascript Code:

 $(document).ready(function() { function ChatViewModel() { var that = this; that.userName = ko.observable(''); that.chatContent = ko.observable(''); that.message = ko.observable(''); that.messageIndex = ko.observable(0); that.activePollingXhr = ko.observable(null); var keepPolling = false; that.joinChat = function() { if (that.userName().trim() != '') { keepPolling = true; pollForMessages(); } } function pollForMessages() { if (!keepPolling) { return; } var form = $("#joinChatForm"); that.activePollingXhr($.ajax({url: form.attr("action"), type: "GET", data: form.serialize(), cache: false, success: function(messages) { console.log(messages); for (var i = 0; i < messages.length; i++) { that.chatContent(that.chatContent() + messages[i] + "\n"); that.messageIndex(that.messageIndex() + 1); } }, error: function(xhr) { if (xhr.statusText != "abort" && xhr.status != 503) { resetUI(); console.error("Unable to retrieve chat messages. Chat ended."); } }, complete: pollForMessages })); $('#message').focus(); } that.postMessage = function() { if (that.message().trim() != '') { var form = $("#postMessageForm"); $.ajax({url: form.attr("action"), type: "POST", data: "message=[" + that.userName() + "] " + $("#postMessageForm input[name=message]").val(), error: function(xhr) { console.error("Error posting chat message: status=" + xhr.status + ", statusText=" + xhr.statusText); } }); that.message(''); } } that.leaveChat = function() { that.activePollingXhr(null); resetUI(); this.userName(''); } function resetUI() { keepPolling = false; that.activePollingXhr(null); that.message(''); that.messageIndex(0); that.chatContent(''); } } //Activate knockout.js ko.applyBindings(new ChatViewModel()); }); 

and html page:

 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Chat</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <h1>Chat</h1> <form id="joinChatForm" th:action="@{/mvc/chat}" data-bind="visible: activePollingXhr() == null"> <p> <label for="user">User: </label> <input id="user" name="user" type="text" data-bind="value: userName"/> <input name="messageIndex" type="hidden" data-bind="value: messageIndex"/> <button id="start" type="submit" data-bind="click: joinChat">Join Chat</button> </p> </form> <form id="leaveChatForm" th:action="@{/mvc/chat}" data-bind="visible: activePollingXhr() != null"> <p> You're chatting as <strong data-bind="text: userName"></strong> <button id="leave" type="submit" data-bind="click: leaveChat">Leave Chat</button> </p> </form> <div data-bind="visible: activePollingXhr() != null"> <textarea rows="15" cols="60" readonly="readonly" data-bind="text: chatContent"></textarea> </div> <form id="postMessageForm" th:action="@{/mvc/chat}" data-bind="visible: activePollingXhr() != null"> <p> <input id="message" name="message" type="text" data-bind="value: message" /> <button id="post" type="submit" data-bind="click: postMessage">Post</button> </p> </form> </body> <script type="text/javascript" src="../../../resources/js/jquery-1.7.2.min.js" th:src="@{/resources/js/jquery-1.7.2.min.js}"></script> <script type="text/javascript" src="../../../resources/js/knockout-2.0.0.js" th:src="@{/resources/js/knockout-2.0.0.js}"></script> <script type="text/javascript" src="../../../resources/js/chat.js" th:src="@{/resources/js/chat.js}"></script> </html> 
+7
source share
3 answers

I discussed this issue in detail with the author of the Spring DeferredResult class, and here is the relevant part of our conversation:

To quote Rossen Stoyanchev:

Roughly speaking. The pending request is associated with DeferredResult. When the request completes, DeferredResult is removed from the card, and then the client issues a new long poll request, which adds a new instance of DeferredResult

+6
source

To understand what DeferredResult does, you need to understand the Async concept from Servlet 3.0.

Using Servlet 3.0, you can take AsyncContext from the request, save it as a collection.

 AsyncContext aCtx = request.startAsync(request, response); 

as a result, your Application Container Thread will be released.

Do some operation in a separate thread and return the result to the Servlet response:

 aCtx.getResponse().getWriter().print(result); 

From this point on, your DeferredResult works exactly the same.

A small example:

Now think that every 5 seconds you get a quote from a third-party service. And you have clients who examine each server for a long time to update something.

You have your controller method:

  /** put deferred result to some HashSet. This is the same logic as you store async context in servlet 3.0, those are clients who are waiting for response **/ @RequestMapping(value="/getQuote.do", method=RequestMethod.GET) @ResponseBody public DeferredResult<String> getQuote(){ final DeferredResult<String> deferredResult = new DeferredResult<String>(); someMap.put(deferredResult); return deferredResult; } 

Now you can see the method outside the controller, which receives a quote and returns a response to the client.

 function getQuoteAndUpdateClients(){ String quote = getUpdatedQuoteFromThirdPartyService(); for (DeferredResult<String> deferredResult: someMap){ deferredResult.setResult(quote); } } 
+11
source

When a client connects, DeferredResult is stored for this client in this.chatRequests. When a client sends a message, it iterates over all DeferredResults (reading clients) to set the result. It is logical that this happens 3 times when 3 clients are connected.

+1
source

All Articles