Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / RestClient.cs @ 65282d58

History | View | Annotate | Download (19.6 kB)

1
// -----------------------------------------------------------------------
2
// <copyright file="RestClient.cs" company="GRNet">
3
// Copyright 2011 GRNET S.A. All rights reserved.
4
// 
5
// Redistribution and use in source and binary forms, with or
6
// without modification, are permitted provided that the following
7
// conditions are met:
8
// 
9
//   1. Redistributions of source code must retain the above
10
//      copyright notice, this list of conditions and the following
11
//      disclaimer.
12
// 
13
//   2. Redistributions in binary form must reproduce the above
14
//      copyright notice, this list of conditions and the following
15
//      disclaimer in the documentation and/or other materials
16
//      provided with the distribution.
17
// 
18
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
// POSSIBILITY OF SUCH DAMAGE.
30
// 
31
// The views and conclusions contained in the software and
32
// documentation are those of the authors and should not be
33
// interpreted as representing official policies, either expressed
34
// or implied, of GRNET S.A.
35
// </copyright>
36
// -----------------------------------------------------------------------
37

    
38
using System.Collections.Specialized;
39
using System.Diagnostics;
40
using System.Diagnostics.Contracts;
41
using System.IO;
42
using System.Net;
43
using System.Runtime.Serialization;
44
using System.Threading.Tasks;
45
using log4net;
46

    
47

    
48
namespace Pithos.Network
49
{
50
    using System;
51
    using System.Collections.Generic;
52
    using System.Linq;
53
    using System.Text;
54

    
55
    /// <summary>
56
    /// TODO: Update summary.
57
    /// </summary>
58
    public class RestClient:WebClient
59
    {
60
        public int Timeout { get; set; }
61

    
62
        public bool TimedOut { get; set; }
63

    
64
        public HttpStatusCode StatusCode { get; private set; }
65

    
66
        public string StatusDescription { get; set; }
67

    
68
        public long? RangeFrom { get; set; }
69
        public long? RangeTo { get; set; }
70

    
71
        public int Retries { get; set; }
72

    
73
        private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
74
        public Dictionary<string, string> Parameters
75
        {
76
            get
77
            {
78
                Contract.Ensures(_parameters!=null);
79
                return _parameters;
80
            }            
81
        }
82

    
83
        private static readonly ILog Log = LogManager.GetLogger("RestClient");
84

    
85

    
86
        [ContractInvariantMethod]
87
        private void Invariants()
88
        {
89
            Contract.Invariant(Headers!=null);    
90
        }
91

    
92
        public RestClient():base()
93
        {
94
            
95
        }
96

    
97
       
98
        public RestClient(RestClient other)
99
            : base()
100
        {
101
            if (other==null)
102
                throw new ArgumentNullException("other");
103
            Contract.EndContractBlock();
104

    
105
            CopyHeaders(other);
106
            Timeout = other.Timeout;
107
            Retries = other.Retries;
108
            BaseAddress = other.BaseAddress;             
109

    
110
            foreach (var parameter in other.Parameters)
111
            {
112
                Parameters.Add(parameter.Key,parameter.Value);
113
            }
114

    
115
            this.Proxy = other.Proxy;
116
        }
117

    
118

    
119
        protected override WebRequest GetWebRequest(Uri address)
120
        {
121
            TimedOut = false;
122
            var webRequest = base.GetWebRequest(address);            
123
            var request = (HttpWebRequest)webRequest;
124
            request.ServicePoint.ConnectionLimit = 50;
125
            if (IfModifiedSince.HasValue)
126
                request.IfModifiedSince = IfModifiedSince.Value;
127
            request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
128
            if(Timeout>0)
129
                request.Timeout = Timeout;
130

    
131
            if (RangeFrom.HasValue)
132
            {
133
                if (RangeTo.HasValue)
134
                    request.AddRange(RangeFrom.Value, RangeTo.Value);
135
                else
136
                    request.AddRange(RangeFrom.Value);
137
            }
138
            return request; 
139
        }
140

    
141
        public DateTime? IfModifiedSince { get; set; }
142

    
143
        //Asynchronous version
144
        protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
145
        {
146
            Log.InfoFormat("ASYNC [{0}] {1}",request.Method, request.RequestUri);
147
            HttpWebResponse response = null;
148

    
149
            try
150
            {
151
                response = (HttpWebResponse)base.GetWebResponse(request, result);
152
            }
153
            catch (WebException exc)
154
            {
155
                if (!TryGetResponse(exc, out response))
156
                    throw;
157
            }
158

    
159
            StatusCode = response.StatusCode;
160
            LastModified = response.LastModified;
161
            StatusDescription = response.StatusDescription;
162
            return response;
163

    
164
        }
165
      
166

    
167
        //Synchronous version
168
        protected override WebResponse GetWebResponse(WebRequest request)
169
        {
170
            HttpWebResponse response = null;
171
            try
172
            {                                
173
                response = (HttpWebResponse)base.GetWebResponse(request);
174
            }
175
            catch (WebException exc)
176
            {
177
                if (!TryGetResponse(exc, out response))
178
                    throw;
179
            }
180

    
181
            StatusCode = response.StatusCode;
182
            LastModified = response.LastModified;
183
            StatusDescription = response.StatusDescription;
184
            return response;
185
        }
186

    
187
        private bool TryGetResponse(WebException exc, out HttpWebResponse response)
188
        {
189
            response = null;
190
            //Fail on empty response
191
            if (exc.Response == null)
192
                return false;
193

    
194
            response = (exc.Response as HttpWebResponse);
195
            //Succeed on allowed status codes
196
            if (AllowedStatusCodes.Contains(response.StatusCode))
197
                return true;
198

    
199
            //Does the response have any content to log?
200
            if (exc.Response.ContentLength > 0)
201
            {
202
                var content = LogContent(exc.Response);
203
                Log.ErrorFormat(content);
204
            }
205
            return false;
206
        }
207

    
208
        private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};        
209

    
210
        public List<HttpStatusCode> AllowedStatusCodes
211
        {
212
            get
213
            {
214
                return _allowedStatusCodes;
215
            }            
216
        }
217

    
218
        public DateTime LastModified { get; private set; }
219

    
220
        private static string LogContent(WebResponse webResponse)
221
        {
222
            if (webResponse == null)
223
                throw new ArgumentNullException("webResponse");
224
            Contract.EndContractBlock();
225

    
226
            //The response stream must be copied to avoid affecting other code by disposing of the 
227
            //original response stream.
228
            var stream = webResponse.GetResponseStream();            
229
            using(var memStream=new MemoryStream())
230
            using (var reader = new StreamReader(memStream))
231
            {
232
                stream.CopyTo(memStream);                
233
                string content = reader.ReadToEnd();
234

    
235
                stream.Seek(0,SeekOrigin.Begin);
236
                return content;
237
            }
238
        }
239

    
240
        public string DownloadStringWithRetry(string address,int retries=0)
241
        {
242
            
243
            if (address == null)
244
                throw new ArgumentNullException("address");
245

    
246
            var actualAddress = GetActualAddress(address);
247

    
248
            TraceStart("GET",actualAddress);            
249
            
250
            var actualRetries = (retries == 0) ? Retries : retries;
251

    
252
            var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);
253

    
254
            var task = Retry(() =>
255
            {                
256
                var content = base.DownloadString(uriString);
257

    
258
                if (StatusCode == HttpStatusCode.NoContent)
259
                    return String.Empty;
260
                return content;
261

    
262
            }, actualRetries);
263

    
264
            try
265
            {
266
                var result = task.Result;
267
                return result;
268

    
269
            }
270
            catch (AggregateException exc)
271
            {
272
                //If the task fails, propagate the original exception
273
                if (exc.InnerException!=null)
274
                    throw exc.InnerException;
275
                throw;
276
            }
277
        }
278

    
279
        public void Head(string address,int retries=0)
280
        {
281
            AllowedStatusCodes.Add(HttpStatusCode.NotFound);
282
            RetryWithoutContent(address, retries, "HEAD");
283
        }
284

    
285
        public void PutWithRetry(string address, int retries = 0)
286
        {
287
            RetryWithoutContent(address, retries, "PUT");
288
        }
289

    
290
        public void DeleteWithRetry(string address,int retries=0)
291
        {
292
            RetryWithoutContent(address, retries, "DELETE");
293
        }
294

    
295
        public string GetHeaderValue(string headerName,bool optional=false)
296
        {
297
            if (this.ResponseHeaders==null)
298
                throw new InvalidOperationException("ResponseHeaders are null");
299
            Contract.EndContractBlock();
300

    
301
            var values=this.ResponseHeaders.GetValues(headerName);
302
            if (values != null)
303
                return values[0];
304

    
305
            if (optional)            
306
                return null;            
307
            //A required header was not found
308
            throw new WebException(String.Format("The {0}  header is missing", headerName));
309
        }
310

    
311
        public void SetNonEmptyHeaderValue(string headerName, string value)
312
        {
313
            if (String.IsNullOrWhiteSpace(value))
314
                return;
315
            Headers.Add(headerName,value);
316
        }
317

    
318
        private void RetryWithoutContent(string address, int retries, string method)
319
        {
320
            if (address == null)
321
                throw new ArgumentNullException("address");
322

    
323
            var actualAddress = GetActualAddress(address);            
324
            var actualRetries = (retries == 0) ? Retries : retries;
325

    
326
            var task = Retry(() =>
327
            {
328
                var uriString = String.Join("/",BaseAddress ,actualAddress);
329
                var uri = new Uri(uriString);
330
                var request =  GetWebRequest(uri);
331
                request.Method = method;
332
                if (ResponseHeaders!=null)
333
                    ResponseHeaders.Clear();
334

    
335
                TraceStart(method, uriString);
336
                if (method == "PUT")
337
                    request.ContentLength = 0;
338

    
339
                //Have to use try/finally instead of using here, because WebClient needs a valid WebResponse object
340
                //in order to return response headers
341
                var response = (HttpWebResponse)GetWebResponse(request);
342
                try
343
                {
344
                    LastModified = response.LastModified;
345
                    StatusCode = response.StatusCode;
346
                    StatusDescription = response.StatusDescription;
347
                }
348
                finally
349
                {
350
                    response.Close();
351
                }
352
                
353

    
354
                return 0;
355
            }, actualRetries);
356

    
357
            try
358
            {
359
                task.Wait();
360
            }
361
            catch (AggregateException ex)
362
            {
363
                var exc = ex.InnerException;
364
                if (exc is RetryException)
365
                {
366
                    Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
367
                }
368
                else
369
                {
370
                    Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);
371
                }
372
                throw exc;
373

    
374
            }
375
            catch(Exception ex)
376
            {
377
                Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);
378
                throw;
379
            }
380
        }
381
        
382
        private static void TraceStart(string method, string actualAddress)
383
        {
384
            Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
385
        }
386

    
387
        private string GetActualAddress(string address)
388
        {
389
            if (Parameters.Count == 0)
390
                return address;
391
            var addressBuilder=new StringBuilder(address);            
392

    
393
            bool isFirst = true;
394
            foreach (var parameter in Parameters)
395
            {
396
                if(isFirst)
397
                    addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
398
                else
399
                    addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
400
                isFirst = false;
401
            }
402
            return addressBuilder.ToString();
403
        }
404

    
405
        public string DownloadStringWithRetry(Uri address,int retries=0)
406
        {
407
            if (address == null)
408
                throw new ArgumentNullException("address");
409

    
410
            var actualRetries = (retries == 0) ? Retries : retries;            
411
            var task = Retry(() =>
412
            {
413
                var content = base.DownloadString(address);
414

    
415
                if (StatusCode == HttpStatusCode.NoContent)
416
                    return String.Empty;
417
                return content;
418

    
419
            }, actualRetries);
420

    
421
            var result = task.Result;
422
            return result;
423
        }
424

    
425
      
426
        /// <summary>
427
        /// Copies headers from another RestClient
428
        /// </summary>
429
        /// <param name="source">The RestClient from which the headers are copied</param>
430
        public void CopyHeaders(RestClient source)
431
        {
432
            if (source == null)
433
                throw new ArgumentNullException("source", "source can't be null");
434
            Contract.EndContractBlock();
435
            //The Headers getter initializes the property, it is never null
436
            Contract.Assume(Headers!=null);
437
                
438
            CopyHeaders(source.Headers,Headers);
439
        }
440
        
441
        /// <summary>
442
        /// Copies headers from one header collection to another
443
        /// </summary>
444
        /// <param name="source">The source collection from which the headers are copied</param>
445
        /// <param name="target">The target collection to which the headers are copied</param>
446
        public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
447
        {
448
            if (source == null)
449
                throw new ArgumentNullException("source", "source can't be null");
450
            if (target == null)
451
                throw new ArgumentNullException("target", "target can't be null");
452
            Contract.EndContractBlock();
453

    
454
            for (int i = 0; i < source.Count; i++)
455
            {
456
                target.Add(source.GetKey(i), source[i]);
457
            }            
458
        }
459

    
460
        public void AssertStatusOK(string message)
461
        {
462
            if (StatusCode >= HttpStatusCode.BadRequest)
463
                throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
464
        }
465

    
466

    
467
        private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
468
        {
469
            if (original==null)
470
                throw new ArgumentNullException("original");
471
            Contract.EndContractBlock();
472

    
473
            if (tcs == null)
474
                tcs = new TaskCompletionSource<T>();
475
            Task.Factory.StartNew(original).ContinueWith(_original =>
476
                {
477
                    if (!_original.IsFaulted)
478
                        tcs.SetFromTask(_original);
479
                    else 
480
                    {
481
                        var e = _original.Exception.InnerException;
482
                        var we = (e as WebException);
483
                        if (we==null)
484
                            tcs.SetException(e);
485
                        else
486
                        {
487
                            var statusCode = GetStatusCode(we);
488

    
489
                            //Return null for 404
490
                            if (statusCode == HttpStatusCode.NotFound)
491
                                tcs.SetResult(default(T));
492
                            //Retry for timeouts and service unavailable
493
                            else if (we.Status == WebExceptionStatus.Timeout ||
494
                                (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
495
                            {
496
                                TimedOut = true;
497
                                if (retryCount == 0)
498
                                {                                    
499
                                    Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);
500
                                    tcs.SetException(new RetryException("Timed out too many times.", e));                                    
501
                                }
502
                                else
503
                                {
504
                                    Log.ErrorFormat(
505
                                        "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
506
                                        retryCount, e);
507
                                    Retry(original, retryCount - 1, tcs);
508
                                }
509
                            }
510
                            else
511
                                tcs.SetException(e);
512
                        }
513
                    };
514
                });
515
            return tcs.Task;
516
        }
517

    
518
        private HttpStatusCode GetStatusCode(WebException we)
519
        {
520
            if (we==null)
521
                throw new ArgumentNullException("we");
522
            var statusCode = HttpStatusCode.RequestTimeout;
523
            if (we.Response != null)
524
            {
525
                statusCode = ((HttpWebResponse) we.Response).StatusCode;
526
                this.StatusCode = statusCode;
527
            }
528
            return statusCode;
529
        }
530

    
531
        public UriBuilder GetAddressBuilder(string container, string objectName)
532
        {
533
            var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));
534
            return builder;
535
        }
536

    
537
        public Dictionary<string, string> GetMeta(string metaPrefix)
538
        {
539
            if (String.IsNullOrWhiteSpace(metaPrefix))
540
                throw new ArgumentNullException("metaPrefix");
541
            Contract.EndContractBlock();
542

    
543
            var keys = ResponseHeaders.AllKeys.AsQueryable();
544
            var dict = (from key in keys
545
                        where key.StartsWith(metaPrefix)
546
                        let name = key.Substring(metaPrefix.Length)
547
                        select new { Name = name, Value = ResponseHeaders[key] })
548
                        .ToDictionary(t => t.Name, t => t.Value);
549
            return dict;
550
        }
551
    }
552

    
553
    public class RetryException:Exception
554
    {
555
        public RetryException()
556
            :base()
557
        {
558
            
559
        }
560

    
561
        public RetryException(string message)
562
            :base(message)
563
        {
564
            
565
        }
566

    
567
        public RetryException(string message,Exception innerException)
568
            :base(message,innerException)
569
        {
570
            
571
        }
572

    
573
        public RetryException(SerializationInfo info,StreamingContext context)
574
            :base(info,context)
575
        {
576
            
577
        }
578
    }
579
}