Extracet the job queue functionality to JobQueue.cs
[pithos-ms-client] / trunk / Pithos.Network / PithosClient.cs
1 // -----------------------------------------------------------------------
2 // <copyright file="PithosClient.cs" company="Microsoft">
3 // TODO: Update copyright text.
4 // </copyright>
5 // -----------------------------------------------------------------------
6
7 using System.Collections.Specialized;
8 using System.Diagnostics;
9 using System.Diagnostics.Contracts;
10 using System.Net;
11 using System.Runtime.Serialization;
12
13 namespace Pithos.Network
14 {
15     using System;
16     using System.Collections.Generic;
17     using System.Linq;
18     using System.Text;
19
20     /// <summary>
21     /// TODO: Update summary.
22     /// </summary>
23     public class PithosClient:WebClient
24     {
25         public int Timeout { get; set; }
26
27         public bool TimedOut { get; set; }
28
29         public HttpStatusCode StatusCode { get; private set; }
30
31         public string StatusDescription { get; set; }
32
33
34         public int Retries { get; set; }
35
36         private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
37         public Dictionary<string, string> Parameters
38         {
39             get { return _parameters; }            
40         }
41
42         public PithosClient():base()
43         {
44             
45         }
46
47        
48         public PithosClient(PithosClient other)
49             : base()
50         {
51             CopyHeaders(other);
52             Timeout = other.Timeout;
53             Retries = other.Retries;
54             BaseAddress = other.BaseAddress;
55             
56
57             foreach (var parameter in other.Parameters)
58             {
59                 Parameters.Add(parameter.Key,parameter.Value);
60             }
61
62             this.Proxy = other.Proxy;
63         }
64
65         protected override WebRequest GetWebRequest(Uri address)
66         {
67             TimedOut = false;
68             HttpWebRequest request = base.GetWebRequest(address) as HttpWebRequest;
69             request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
70             if(Timeout>0)
71                 request.Timeout = Timeout;
72             return request; 
73         }
74
75         protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
76         {
77             var response = (HttpWebResponse) base.GetWebResponse(request, result);
78             StatusCode=response.StatusCode;
79             StatusDescription=response.StatusDescription;
80             return response;
81         }
82
83
84
85         protected override WebResponse GetWebResponse(WebRequest request)
86         {
87             var response = (HttpWebResponse)base.GetWebResponse(request);
88             StatusCode = response.StatusCode;
89             StatusDescription = response.StatusDescription;
90             return response;
91         }
92
93         public string DownloadStringWithRetry(string address,int retries=0)
94         {
95             if (address == null)
96                 throw new ArgumentNullException("address");
97
98             var actualAddress = GetActualAddress(address);
99
100             var actualRetries = (retries == 0) ? Retries : retries;
101             
102
103             var func = Retry(() =>
104             {
105                 var uriString = String.Join("/", BaseAddress, actualAddress);
106                 var content = base.DownloadString(uriString);
107
108                 if (StatusCode == HttpStatusCode.NoContent)
109                     return String.Empty;
110                 return content;
111
112             }, actualRetries);
113
114             var result = func();
115             return result;
116         }
117
118         public void Head(string address,int retries=0)
119         {
120             RetryWithoutContent(address, retries, "HEAD");
121         }
122
123         public void PutWithRetry(string address, int retries = 0)
124         {
125             RetryWithoutContent(address, retries, "PUT");
126         }
127
128         public void DeleteWithRetry(string address,int retries=0)
129         {
130             RetryWithoutContent(address, retries, "DELETE");
131         }
132
133         public string GetHeaderValue(string headerName)
134         {
135             var values=this.ResponseHeaders.GetValues(headerName);
136             if (values == null)
137                 throw new WebException(String.Format("The {0}  header is missing", headerName));
138             else
139                 return values[0];
140         }
141
142         private void RetryWithoutContent(string address, int retries, string method)
143         {
144             if (address == null)
145                 throw new ArgumentNullException("address");
146
147             var actualAddress = GetActualAddress(address);
148
149             var actualRetries = (retries == 0) ? Retries : retries;
150             var func = Retry(() =>
151             {
152                 var uriString = String.Join("/",BaseAddress ,actualAddress);
153                 var uri = new Uri(uriString);
154                 var request =  GetWebRequest(uri);
155                 request.Method = method;
156                 if (ResponseHeaders!=null)
157                     ResponseHeaders.Clear();
158
159                 var response = (HttpWebResponse)GetWebResponse(request);
160                 StatusCode = response.StatusCode;
161                 StatusDescription = response.StatusDescription;                
162                 
163
164                 return 0;
165             }, actualRetries);
166
167             func();
168         }
169
170         private string GetActualAddress(string address)
171         {
172             if (Parameters.Count == 0)
173                 return address;
174             var addressBuilder=new StringBuilder(address);            
175
176             bool isFirst = true;
177             foreach (var parameter in Parameters)
178             {
179                 if(isFirst)
180                     addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
181                 else
182                     addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
183                 isFirst = false;
184             }
185             return addressBuilder.ToString();
186         }
187
188         public string DownloadStringWithRetry(Uri address,int retries=0)
189         {
190             if (address == null)
191                 throw new ArgumentNullException("address");
192
193             var actualRetries = (retries == 0) ? Retries : retries;            
194             var func = Retry(() =>
195             {
196                 var content = base.DownloadString(address);
197
198                 if (StatusCode == HttpStatusCode.NoContent)
199                     return String.Empty;
200                 return content;
201
202             }, actualRetries);
203
204             var result = func();
205             return result;
206         }
207
208       
209         /// <summary>
210         /// Copies headers from another PithosClient
211         /// </summary>
212         /// <param name="source">The PithosClient from which the headers are copied</param>
213         public void CopyHeaders(PithosClient source)
214         {
215             Contract.Requires(source != null, "source can't be null");
216             if (source == null)
217                 throw new ArgumentNullException("source", "source can't be null");
218             CopyHeaders(source.Headers,Headers);
219         }
220         
221         /// <summary>
222         /// Copies headers from one header collection to another
223         /// </summary>
224         /// <param name="source">The source collection from which the headers are copied</param>
225         /// <param name="target">The target collection to which the headers are copied</param>
226         public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
227         {
228             Contract.Requires(source != null, "source can't be null");
229             Contract.Requires(target != null, "target can't be null");
230             if (source == null)
231                 throw new ArgumentNullException("source", "source can't be null");
232             if (target == null)
233                 throw new ArgumentNullException("target", "target can't be null");
234             for (int i = 0; i < source.Count; i++)
235             {
236                 target.Add(source.GetKey(i), source[i]);
237             }            
238         }
239
240         public void AssertStatusOK(string message)
241         {
242             if (StatusCode >= HttpStatusCode.BadRequest)
243                 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
244         }
245
246         private Func<T> Retry<T>(Func< T> original, int retryCount)
247         {
248             return () =>
249             {
250                 while (true)
251                 {
252                     try
253                     {
254                         return original();
255                     }
256                     catch (WebException e)
257                     {
258                         var statusCode = ((HttpWebResponse)e.Response).StatusCode;
259                         this.StatusCode = statusCode;
260
261                         switch (e.Status)
262                         {
263                             case WebExceptionStatus.Timeout:
264
265                                 TimedOut = true;
266                                 if (retryCount == 0)
267                                 {
268                                     Trace.TraceError("[ERROR] Timed out too many times. {0}\n", e);
269                                     throw new RetryException("Timed out too many times.", e);
270                                 }
271                                 retryCount--;
272                                 Trace.TraceError(
273                                     "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
274                                     retryCount, e);
275
276                                 break;
277                             case WebExceptionStatus.ProtocolError:
278                                 switch (statusCode)
279                                 {
280                                     case HttpStatusCode.NotFound:
281                                         {
282                                             return default(T);
283                                         }
284                                     case HttpStatusCode.ServiceUnavailable:
285                                         {
286
287                                             TimedOut = false;
288                                             if (retryCount == 0)
289                                             {
290                                                 Trace.TraceError("[ERROR] Failed too many times. {0}\n", e);
291                                                 throw new RetryException("Failed too many times.", e);
292                                             }
293                                             retryCount--;
294                                             Trace.TraceError(
295                                                 "[RETRY] Failed due to 503. Will retry {0} more times\n{1}", retryCount, e);
296                                             break;
297                                         }
298                                     default:
299                                         throw;
300                                 }
301                                 break;
302                             default:
303                                 throw;
304                         }
305                     }
306                 }
307             };
308         }
309
310        
311     }
312
313     public class RetryException:Exception
314     {
315         public RetryException()
316             :base()
317         {
318             
319         }
320
321         public RetryException(string message)
322             :base(message)
323         {
324             
325         }
326
327         public RetryException(string message,Exception innerException)
328             :base(message,innerException)
329         {
330             
331         }
332
333         public RetryException(SerializationInfo info,StreamingContext context)
334             :base(info,context)
335         {
336             
337         }
338     }
339 }