Saturday, October 26, 2013

WWW web request manager in Unity3d -Part 2

Standard
Hello,

This post is continuation of this. Many times, in our games we have logic which continuously interact with back-end logic (can be in php, java or C#). In order to minimize number of requests which client makes to server, we can send web request in batches. In this post I will explain how to implement a Request Manager to manage web request in batches.

This post assumes that you have idea of WWW and Co routines.

Let us start...
In this post we will create:

  1. BaseRequest: Class which will be parent of all the requests
  2. IRequest: Interface with basic functionality of request
  3. IRequestContext: Interface having method for processing response
  4. Request: Class which extends IRequest
  5. RequestBatch: Class to contain our batch of requests
  6. Response: Class to contain response from server
  7. RequestQueueManager: Class which has implementation to interact with server and process the response
Create following classes in unity project

  1. Base Request
     
                 public class BaseRequest
        {
            [JsonIgnore]
            public IRequestContext RequestContext
            {
                get;
                set;
            }
    
            public BaseRequest()
            {
    
            }
        }
                
  2. IRequest
     
                  public interface IRequest
        {
            [JsonName("time")]
            long Time { get; set; }
    
            [JsonName("expectedStatus")]
            int ExpectedStatus { get; set; }
    
            [JsonName("requestId")]
            int RequestId { get; set; }
    
            [JsonName("token")]
            string Token { get; set; }
    
            void IncrementRetryCount();
    
            int RetryCount { get; }
    
            void Execute(string response);
        }
                 
  3. IRequestContext
     
                    public interface IRequestContext
        {
            void OnSuccess
    <T>(T response);
    
            void OnFailure<T>(T response);
    
            void Execute(string response);
        }
                 
  4. Request
     
     public class Request
    <T> : IRequest
        {
            public Request(T args)
            {
                Arguments = args;
    
                Time = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; // seconds since Jan 1st 1970
                ExpectedStatus = 0;
                RequestId = 1;
                Token = Time.ToString() + RequestId.ToString();
            }
    
            [JsonName("time")]
            public long Time
            {
                get;
                set;
            }
    
            [JsonName("expectedStatus")]
            public int ExpectedStatus
            {
                get;
                set;
            }
    
            [JsonName("requestId")]
            public int RequestId
            {
                get;
                set;
            }
    
            [JsonName("token")]
            public string Token
            {
                get;
                set;
            }
    
            [JsonName("args")]
            public T Arguments { get; set; }
    
            int retryCount;
            public int RetryCount { get { return retryCount; } }
    
            public void IncrementRetryCount()
            {
                retryCount++;
            }
    
            public void Execute(string response)
            {
                (Arguments as BaseRequest).RequestContext.Execute(response);
            }
        }
                         
    
         
  5. RequestBatch
     
                 public class RequestBatch
        {
            private static int nextBatchId = 100;
            private int id;
            public int Id { get { return id; } }
            int retryCount;
            public int RetryCount { get { return retryCount; } }
            [JsonName("requests")]
            public List
    <IRequest> Requests { get; set; }
            public void IncrementRetryCount() { ++retryCount; }
            [JsonName("sentclienttime")]
            public long SentClientTime { get; set; }
    
            public RequestBatch()
            {
                id = nextBatchId++;
                Requests = new List<IRequest>();
            }
        }
                 
  6. Response
     
                  public class BatchResponse
        {
            [JsonName("data")]
            public Response[] Data { get; set; }
            [JsonName("servertime")]
            public long ServerTime { get; set; }
        }
    
        public class Response
        {
            [JsonName("requestId")]
            public int RequestId { get; set; }
            [JsonName("requestdata")]
            public object RequestData { get; set; }
        }
    
        public class SampleResponse
        {
            [JsonName("status")]
            public int Status { get; set; }
        }
                 
  7. RequestQueueManager
     
                 public class RequestQueueManager : MonoBehaviour
    {
        public static RequestQueueManager Instance;
    
        private static int INSTANCECOUNT = 0;
        // Self tick interval.
        private const int TIMER_MS = 500;
    
       
        public int batchAutoFlushSize = 5;
    
       
        public int batchAutoFlushSec = 15;
    
        public int batchTimeoutSec = 30;
    
        public int maxBatchRetryCount = 3;
    
    
        public bool verbose = true;
    
        // ------------------------------
        // PRIVATE VARIABLES
        // ------------------------------
        static int RequestCount = 1;
    
        // Commands waiting to be batched and sent.
        List
    <IRequest> queue = new List<IRequest>();
        List<RequestBatch> asyncBatchesInFlight = new List<RequestBatch>();
        // The utc time when the first element was added to the current queue.
        private long queueFirstElemTime = -1;
        // Set by call or flush.
        private bool queueReadyToBeSent = false;
        // The synchronous batch currently in flight, or null.
        private RequestBatch activeBatch;
        // The timer we use to tick ourselves.
        private Timer processTimer;
    
        bool isProcessing;
        private static readonly System.DateTime Jan1st1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        void Awake()
        {
            Instance = this;
            INSTANCECOUNT++;
        }
    
        public RequestQueueManager()
        {
        }
    
        public void Initialize()
        {
            if (INSTANCECOUNT > 1)
            {
                Debug.LogError("More than 1 intance of RequestQueueManager");
            }
            processTimer = new Timer(TIMER_MS);
            processTimer.addListener(() =>
            {
                Process();
            });
            processTimer.start();
    
        }
    
        public bool IsRunning
        {
            get
            {
                return null != processTimer;
            }
        }
    
        public void Add(IRequest request)
        {
            if (0 == queue.Count)
            {
                queueFirstElemTime = Helpers.CurrentTime(Jan1st1970);
            }
            request.RequestId = RequestCount++;
            request.Token = GetTimestamp(DateTime.UtcNow);
            queue.Add(request);
            isProcessing = false;
            queueReadyToBeSent = true;
        }
    
        public void AsyncAdd<T>(Request<T> request)
        {
            List<IRequest> asyncQueue = new List<IRequest>();
            request.RequestId = RequestCount++;
            request.Token = GetTimestamp(DateTime.UtcNow);
            asyncQueue.Add(request);
            RequestBatch batch = PostRequests(asyncQueue);
            asyncBatchesInFlight.Add(batch);
        }
    
        void RemoveRequestsFromQueueWhichHaveTimedOut(List<IRequest> localQueue)
        {
            localQueue.RemoveAll(x => x.RetryCount >= maxBatchRetryCount);
        }
    
        protected void Process()
        {
    
            int index = 0;
            // Check async batches for timeouts.
            for (index = asyncBatchesInFlight.Count - 1; index >= 0; index--)
            {
                RequestBatch batch = asyncBatchesInFlight[index];
                if (hasBatchTimedOut(batch))
                {
                    if (incrementRetriesAndIsTooMany(batch))
                    {
                        if (verbose)
                        {
                            Debug.Log("Removing async batch, as max retry count exceeded, batch Retry count: " + batch.RetryCount);
                        }
                        asyncBatchesInFlight.Remove(batch);
                        batch = null;
                    }
                    else
                    {
                        if (verbose)
                        {
                            Debug.Log("Resending async batch with retry count: " + batch.RetryCount);
                        }
                        resendBatch(batch);
                    }
                }
            }
    
    
            // If batch in flight...
            if (null != activeBatch)
            {
                // Check to see if it has timed out.
                if (hasBatchTimedOut(activeBatch))
                {
                    if (verbose)
                    {
                        Debug.Log("Time out occured retrying..");
                    }
                    if (incrementRetriesAndIsTooMany(activeBatch))
                    {
                        if (verbose)
                        {
                            Debug.Log("Removing active batch, as max retry count exceeded, batch Retry count: " + activeBatch.RetryCount);
                        }
                        RemoveRequestsFromQueueWhichHaveTimedOut(queue);
                        activeBatch = null;
    
                    }
                    else
                    {
                        if (verbose)
                        {
                            Debug.Log("Resending acive batch with retry count: " + activeBatch.RetryCount);
                        }
                        resendBatch(activeBatch);
                    }
                }
                // Done: wait for batch to return.
                return;
            }
    
            // If queue is empty, we have nothing to do.
            if (0 == queue.Count || isProcessing)
            {
                return;
            }
    
            double queueAgeSec = (Helpers.CurrentTime(Jan1st1970) - queueFirstElemTime) * 0.001;
    
            // If queue is long enough, or old enough, or has been called,
            // dispatch it.
            if (queue.Count >= batchAutoFlushSize || queueAgeSec > batchTimeoutSec || queueReadyToBeSent)
            {
                if (verbose)
                {
                    Debug.Log("Checking and sending commands to post");
                }
                activeBatch = PostRequests(queue);
                isProcessing = true;
                queueFirstElemTime = -1;
                queueReadyToBeSent = false;
            }
        }
    
        private System.Collections.IEnumerator ExecuteBatch(RequestBatch batch)
        {
            string jsonRequest = JsonWriter.Serialize(batch);
            WWWForm form = new WWWForm();
            form.AddField("batch", jsonRequest);
            if (verbose)
            {
                Debug.Log("jsonRequest: " + jsonRequest);
            }
            WWW www = new WWW("http://www.ashwanik.in/blog/process", form);
            while (!www.isDone)
            {
                if (hasBatchTimedOut(batch))
                {
                    if (verbose)
                    {
                        Debug.Log("ExecuteBatch: Batch timed out");
                    }
                    www.Dispose();
                    www = null;
                    break;
                }
                yield return null;
            }
    
            if (www != null && www.error == null && www.isDone && www.text != null)
            {
                if (verbose)
                {
                    Debug.Log("Starting processing " + www.text);
                }
                ProcessResponse<int>(batch, www.text);
            }
        }
    
        private void ProcessResponse<Result>(RequestBatch batch, string batchResponse)
        {
            try
            {
                if (verbose)
                {
                    Debug.Log("Processing response " + batchResponse);
                }
                Dictionary<int, IRequest> requestDictionary = new Dictionary<int, IRequest>();
                foreach (IRequest request in batch.Requests)
                {
                    requestDictionary.Add(request.RequestId, request);
                }
                JsonObject obj = new JsonObject(batchResponse);
                JsonArray data = obj["data"] as JsonArray;
                int requestId = 0;
                IRequest localRequest = null;
                foreach (JsonObject item in data)
                {
                    requestId = Convert.ToInt32(item["requestId"]);
    
                    localRequest = requestDictionary[requestId];
                    localRequest.Execute(item.ToString());
                    if (requestDictionary.ContainsKey(requestId))
                    {
                        queue.Remove(requestDictionary[requestId]);
                    }
                    else
                    {
                        if (verbose)
                        {
                            Debug.Log("Received response for request which is not in queue: " + requestId + " Response: " + item.ToString());
                        }
                    }
                    if (verbose)
                    {
                        Debug.Log("Each request response: " + item.ToString());
                    }
                }
                if (activeBatch == batch)
                {
                    activeBatch = null;
                }
                else
                {
                    int index = 0;
                    for (index = 0; index < asyncBatchesInFlight.Count; ++index)
                    {
                        if (batch == asyncBatchesInFlight[index])
                        {
                            if (verbose)
                            {
                                Debug.Log("Removed batch from aync batches" + index);
                            }
                            asyncBatchesInFlight.Remove(batch);
                            break;
                        }
                    }
                }
                batch = null;
            }
            catch (Exception ex)
            {
                Debug.Log(ex.Message + ex.StackTrace);
            }
        }
    
    
    
        private RequestBatch PostRequests(List<IRequest> requests, RequestBatch batch = null)
        {
            if (null == batch)
            {
                batch = new RequestBatch();
            }
            batch.SentClientTime = Helpers.CurrentTime(Jan1st1970);
            if (requests != null && requests.Count > 0)
            {
    
                foreach (IRequest request in requests)
                {
                    if (!batch.Requests.Where(x => x.RequestId == request.RequestId).Any())
                    {
                        batch.Requests.Add(request);
                    }
                }
                ///Removing requests from queue only for active batch when retry count is equal to (Max retry count -1)
                if (batch == activeBatch && batch.RetryCount >= (maxBatchRetryCount - 1))
                {
                    RemoveRequestsFromQueueWhichHaveTimedOut(requests);
                }
                ///Remove request from batch requests list
                else if (batch.RetryCount >= (maxBatchRetryCount - 1))
                {
                    RemoveRequestsFromQueueWhichHaveTimedOut(batch.Requests);
                }
            }
            if (batch.Requests.Count > 0)
            {
                StartCoroutine(ExecuteBatch(batch));
            }
            else
            {
                batch = null;
            }
            return batch;
        }
    
        private String GetTimestamp(DateTime value)
        {
            return value.ToString("yyyyMMddHHmmssffff");
        }
    
        private bool hasBatchTimedOut(RequestBatch batch)
        {
            // Check for timeout
            long batchAge = (Helpers.CurrentTime(Jan1st1970) - batch.SentClientTime) / 1000;
            if (batchAge > batchTimeoutSec)
            {
                return true;
            }
    
            return false;
        }
    
        /**
             * Increments the batch's retry count and the returns true if
             * the batch has reached the retry limit.
             */
        private bool incrementRetriesAndIsTooMany(RequestBatch batch)
        {
            batch.IncrementRetryCount();
            foreach (IRequest request in batch.Requests)
            {
                request.IncrementRetryCount();
            }
            if (batch.RetryCount >= maxBatchRetryCount)
            {
                return true;
            }
    
            return false;
        }
    
        private void resendBatch(RequestBatch batch)
        {
            if (activeBatch == batch)
            {
                // Append any new queued commands to this batch and resend.
                PostRequests(queue, batch);
                isProcessing = true;
            }
            else
            {
                // Async, so nothing to append.
                PostRequests(null, batch);
            }
        }
    
    }
                 

In order to test the request manager, create
  1. SampleRequest
     
                    public class SampleRequest : BaseRequest
    {
        #region Methods
        public SampleRequest(IRequestContext context)
        {
            this.RequestContext = context;
        }
    
        [JsonName("type")]
        public RequestType Type { get; set; }
        #endregion
    }
                    
  2. SampleRequestContext
                    public class SampleRequestContext : IRequestContext
    {
    
        public void OnSuccess
    <T>(T response)
        {
            Debug.Log("Sample Request successful!");
        }
    
        public void OnFailure<T>(T response)
        {
            Debug.Log("Sample Request failed!");
        }
    
        public void Execute(string response)
        {
            var serializedResponse = (Helpers.Serialize<SampleResponse>(response));
            if (serializedResponse.Status == 0)
            {
                OnSuccess<SampleResponse>(serializedResponse);
            }
            else
            {
                OnFailure<SampleResponse>(serializedResponse);
            }
        }
    }
                    
  3. RequestManagerTester
       public class RequestManagerTester : MonoBehaviour
    {
        #region Methods
    
        void Awake()
        {
    
        }
        void Start()
        {
            RequestQueueManager.Instance.Initialize();
        }
        void OnGUI()
        {
            GUILayout.BeginArea(new Rect((Screen.width - 310) / 2, 200, 310, 1000));
            if (GUILayout.Button("Send Sync Sample Request", GUILayout.Width(300), GUILayout.Height(20)))
            {
                SampleRequest sampleRequest = new SampleRequest(new SampleRequestContext());
                IRequest request = new Request
    <samplerequest>(sampleRequest);
                RequestQueueManager.Instance.Add(request);
            }
    
            if (GUILayout.Button("Send Aync Sample Request", GUILayout.Width(300), GUILayout.Height(20)))
            {
                SampleRequest sampleRequest = new SampleRequest(new SampleRequestContext());
                Request<SampleRequest> request = new Request<SampleRequest>(sampleRequest);
                RequestQueueManager.Instance.AsyncAdd<SampleRequest>(request);
            }
    
            GUILayout.EndArea();
        }
    
        #endregion
    }
          
                           
Attach RequestQueueManager and RequestManagerTester to Main Camera of the scene and save the scene. Now play the scene in the editor.

You can get the sample project here.

Hope this helps.

Thanks for printing this post. Hope you liked it.
Keep visiting and sharing.
Thanks,
Ashwani.

0 comments :

Post a Comment