Google API Client for .Net: Implement retry when a request fails - c#

How can I implement retries in case a request that is part of a batch request fails when interacting with google's API. In their documentation, they suggest adding "Exponential Backoff" algorithm. I'm using the following code snippet in their documentation:
UserCredential credential;
using (var stream = new FileStream("client_secrets.json", FileMode.Open, FileAccess.Read))
{
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
new[] { CalendarService.Scope.Calendar },
"user", CancellationToken.None, new FileDataStore("Calendar.Sample.Store"));
}
// Create the service.
var service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Google Calendar API Sample",
});
// Create a batch request.
var request = new BatchRequest(service);
request.Queue<CalendarList>(service.CalendarList.List(),
(content, error, i, message) =>
{
// Put your callback code here.
});
request.Queue<Event>(service.Events.Insert(
new Event
{
Summary = "Learn how to execute a batch request",
Start = new EventDateTime() { DateTime = new DateTime(2014, 1, 1, 10, 0, 0) },
End = new EventDateTime() { DateTime = new DateTime(2014, 1, 1, 12, 0, 0) }
}, "YOUR_CALENDAR_ID_HERE"),
(content, error, i, message) =>
{
// Put your callback code here.
});
// You can add more Queue calls here.
// Execute the batch request, which includes the 2 requests above.
await request.ExecuteAsync();

Here is a simple helper class to make it easy to implement exponential backoff for a lot of the situations that Google talks about on their API error page: https://developers.google.com/calendar/v3/errors
How to Use:
Edit the class below to include your client secret and application name as you set up on https://console.developers.google.com
In the startup of your application (or when you ask the user to authorize), call GCalAPIHelper.Instance.Auth();
Anywhere you would call the Google Calendar API (eg Get, Insert, Delete, etc), instead use this class by doing: GCalAPIHelper.Instance.CreateEvent(event, calendarId); (you may need to expand this class to other API endpoints as your needs require)
using Google;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using static Google.Apis.Calendar.v3.CalendarListResource.ListRequest;
/*======================================================================================
* This file is to implement Google Calendar .NET API endpoints WITH exponential backoff.
*
* How to use:
* - Install the Google Calendar .NET API (nuget.org/packages/Google.Apis.Calendar.v3)
* - Edit the class below to include your client secret and application name as you
* set up on https://console.developers.google.com
* - In the startup of your application (or when you ask the user to authorize), call
* GCalAPIHelper.Instance.Auth();
* - Anywhere you would call the Google Calendar API (eg Get, Insert, Delete, etc),
* instead use this class by doing:
* GCalAPIHelper.Instance.CreateEvent(event, calendarId); (you may need to expand
* this class to other API endpoints as your needs require)
*======================================================================================
*/
namespace APIHelper
{
public class GCalAPIHelper
{
#region Singleton
private static GCalAPIHelper instance;
public static GCalAPIHelper Instance
{
get
{
if (instance == null)
instance = new GCalAPIHelper();
return instance;
}
}
#endregion Singleton
#region Private Properties
private CalendarService service { get; set; }
private string[] scopes = { CalendarService.Scope.Calendar };
private const string CLIENTSECRETSTRING = "YOUR_SECRET"; //Paste in your JSON client secret here. Don't forget to escape special characters!
private const string APPNAME = "YOUR_APPLICATION_NAME"; //Paste in your Application name here
#endregion Private Properties
#region Constructor and Initializations
public GCalAPIHelper()
{
}
public void Auth(string credentialsPath)
{
if (service != null)
return;
UserCredential credential;
byte[] byteArray = Encoding.ASCII.GetBytes(CLIENTSECRETSTRING);
using (var stream = new MemoryStream(byteArray))
{
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
scopes,
Environment.UserName,
CancellationToken.None,
new FileDataStore(credentialsPath, true)).Result;
}
service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = APPNAME
});
}
#endregion Constructor and Initializations
#region Private Methods
private TResponse DoActionWithExponentialBackoff<TResponse>(CalendarBaseServiceRequest<TResponse> request)
{
return DoActionWithExponentialBackoff(request, new HttpStatusCode[0]);
}
private TResponse DoActionWithExponentialBackoff<TResponse>(CalendarBaseServiceRequest<TResponse> request, HttpStatusCode[] otherBackoffCodes)
{
int delay = 100;
while (delay < 1000) //If the delay gets above 1 second, give up
{
try
{
return request.Execute();
}
catch (GoogleApiException ex)
{
if (ex.HttpStatusCode == HttpStatusCode.Forbidden || //Rate limit exceeded
ex.HttpStatusCode == HttpStatusCode.ServiceUnavailable || //Backend error
ex.HttpStatusCode == HttpStatusCode.NotFound ||
ex.Message.Contains("That’s an error") || //Handles the Google error pages like https://i.imgur.com/lFDKFro.png
otherBackoffCodes.Contains(ex.HttpStatusCode))
{
Common.Log($"Request failed. Waiting {delay} ms before trying again");
Thread.Sleep(delay);
delay += 100;
}
else
throw;
}
}
throw new Exception("Retry attempts failed");
}
#endregion Private Methods
#region Public Properties
public bool IsAuthorized
{
get { return service != null; }
}
#endregion Public Properties
#region Public Methods
public Event CreateEvent(Event eventToCreate, string calendarId)
{
EventsResource.InsertRequest eventCreateRequest = service.Events.Insert(eventToCreate, calendarId);
return DoActionWithExponentialBackoff(eventCreateRequest);
}
public Event InsertEvent(Event eventToInsert, string calendarId)
{
EventsResource.InsertRequest eventCopyRequest = service.Events.Insert(eventToInsert, calendarId);
return DoActionWithExponentialBackoff(eventCopyRequest);
}
public Event UpdateEvent(Event eventToUpdate, string calendarId, bool sendNotifications = false)
{
EventsResource.UpdateRequest eventUpdateRequest = service.Events.Update(eventToUpdate, calendarId, eventToUpdate.Id);
eventUpdateRequest.SendNotifications = sendNotifications;
return DoActionWithExponentialBackoff(eventUpdateRequest);
}
public Event GetEvent(Event eventToGet, string calendarId)
{
return GetEvent(eventToGet.Id, calendarId);
}
public Event GetEvent(string eventIdToGet, string calendarId)
{
EventsResource.GetRequest eventGetRequest = service.Events.Get(calendarId, eventIdToGet);
return DoActionWithExponentialBackoff(eventGetRequest);
}
public CalendarListEntry GetCalendar(string calendarId)
{
CalendarListResource.GetRequest calendarGetRequest = service.CalendarList.Get(calendarId);
return DoActionWithExponentialBackoff(calendarGetRequest);
}
public Events ListEvents(string calendarId, DateTime? startDate = null, DateTime? endDate = null, string q = null, int maxResults = 250)
{
EventsResource.ListRequest eventListRequest = service.Events.List(calendarId);
eventListRequest.ShowDeleted = false;
eventListRequest.SingleEvents = true;
eventListRequest.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime;
if (startDate != null)
eventListRequest.TimeMin = startDate;
if (endDate != null)
eventListRequest.TimeMax = endDate;
if (!string.IsNullOrEmpty(q))
eventListRequest.Q = q;
eventListRequest.MaxResults = maxResults;
return DoActionWithExponentialBackoff(eventListRequest);
}
public CalendarList ListCalendars(string accessRole)
{
CalendarListResource.ListRequest calendarListRequest = service.CalendarList.List();
calendarListRequest.MinAccessRole = (MinAccessRoleEnum)Enum.Parse(typeof(MinAccessRoleEnum), accessRole);
return DoActionWithExponentialBackoff(calendarListRequest);
}
public void DeleteEvent(Event eventToDelete, string calendarId, bool sendNotifications = false)
{
DeleteEvent(eventToDelete.Id, calendarId, sendNotifications);
}
public void DeleteEvent(string eventIdToDelete, string calendarId, bool sendNotifications = false)
{
EventsResource.DeleteRequest eventDeleteRequest = service.Events.Delete(calendarId, eventIdToDelete);
eventDeleteRequest.SendNotifications = sendNotifications;
DoActionWithExponentialBackoff(eventDeleteRequest, new HttpStatusCode[] { HttpStatusCode.Gone });
}
#endregion Public Methods
}
}

derekantrican has an answer that I based mine off of. Two things, if the resource is "notFound" waiting for it won't do any good. That's them responding to the request with the object not being found, so there's no need for back off. I'm not sure if there are other codes where I need to handle yet, but I will be looking at it closer. According to Google: https://cloud.google.com/iot/docs/how-tos/exponential-backoff all 5xx and 429 should be retried.
Also, Google wants this to be an exponential back off; not linear. So the code below handles it in a exponential way. They also want you to add a random amount of MS to the retry timeout. I don't do this, but it would be easy to do. I just don't think that it matters that much.
I also needed the requests to be async so I updated the work methods to this type. See derekantrican's examples on how to call the methods; these are just the worker methods. Instead of returning "default" on the notFound, you could also re-throw the exception and handle it upstream, too.
private async Task<TResponse> DoActionWithExponentialBackoff<TResponse>(DirectoryBaseServiceRequest<TResponse> request)
{
return await DoActionWithExponentialBackoff(request, new HttpStatusCode[0]);
}
private async Task<TResponse> DoActionWithExponentialBackoff<TResponse>(DirectoryBaseServiceRequest<TResponse> request, HttpStatusCode[] otherBackoffCodes)
{
int timeDelay = 100;
int retries = 1;
int backoff = 1;
while (retries <= 5)
{
try
{
return await request.ExecuteAsync();
}
catch (GoogleApiException ex)
{
if (ex.HttpStatusCode == HttpStatusCode.NotFound)
return default;
else if (ex.HttpStatusCode == HttpStatusCode.Forbidden || //Rate limit exceeded
ex.HttpStatusCode == HttpStatusCode.ServiceUnavailable || //Backend error
ex.Message.Contains("That’s an error") || //Handles the Google error pages like https://i.imgur.com/lFDKFro.png
otherBackoffCodes.Contains(ex.HttpStatusCode))
{
//Common.Log($"Request failed. Waiting {delay} ms before trying again");
Thread.Sleep(timeDelay);
timeDelay += 100 * backoff;
backoff = backoff * (retries++ + 1);
}
else
throw ex; // rethrow exception
}
}
throw new Exception("Retry attempts failed");
}

Related

Best way to create new Event Hub Namespace for every ten Event Hubs

Since I am only allowed 10 event hubs per namespace, I am looking for a good algorithm to create a new namespace for every ten event hubs in the list I have.
NOTE: The list of event hub namespaces are being passed in to the method.
var eventHubResources = GetRequiredService<List<EventHubResource>>();
foreach (var eventHubResource in eventHubResources)
{
eventHubResource.ResourceGroup = resourceGroup;
MyNamespace.IEventHub eventHub = new EventHub(logger);
if (eventHubResource.CaptureSettings == null)
{
if (eventHubResources.IndexOf(eventHubResource) <= 9)
{
await eventHub.CreateEventHubAsync(azure, eventHubNamespace[0], eventHubResource, null);
}
if ((eventHubResources.IndexOf(eventHubResource) > 9) && (eventHubResources.IndexOf(eventHubResource) <= 19))
{
await eventHub.CreateEventHubAsync(azure, eventHubNamespace[1], eventHubResource, null);
}
// and so on....
}
else
{
await eventHub.CreateEventHubAsync(azure, eventHubNamespace, eventHubResource, storageAccount);
}
}
What is a better way to achieve this, compared to what I have above?
I didn't test the code but you can get the idea.
public static async Task CreateNamespaces(List<string> eventhubNames, ServiceClientCredentials creds) {
int totalEventHubsInNamespace = 0;
var ehClient = new EventHubManagementClient(creds)
{
SubscriptionId = "<my subscription id>"
};
foreach (var ehName in eventhubNames)
{
if (totalEventHubsInNamespace == 0)
{
var namespaceParams = new EHNamespace()
{
Location = "<datacenter location>"
};
// Create namespace
var namespaceName = "<populate some unique namespace>";
Console.WriteLine($"Creating namespace... {namespaceName}");
await ehClient.Namespaces.CreateOrUpdateAsync(resourceGroupName, namespaceName, namespaceParams);
Console.WriteLine("Created namespace successfully.");
}
// Create eventhub.
Console.WriteLine($"Creating eventhub {ehName}");
var ehParams = new Eventhub() { }; // Customize you eventhub here if you need.
await ehClient.EventHubs.CreateOrUpdateAsync(resourceGroupName, namespaceName, ehName, ehParams);
Console.WriteLine("Created Event Hub successfully.");
totalEventHubsInNamespace++;
if (totalEventHubsInNamespace >= 10)
{
totalEventHubsInNamespace = 0;
}
}
}

RestSharp returns IRestResponse.StatusCode == 0 to Polly onRetry

I wrote a fairly simple wrapper around the RestSharp client adding some retry logic to it using Polly.
I setup Fiddler to test it and simulated some "Bad" responses.
The problem I have is in the onRetry delegate, the result.Result.StatusCode bit seems to sometimes log as 0 instead of the actual bad status code (502 in some of my testings).
However, with my unit tests it seems that its working perfectly. Race conditions here maybe?
Any idea why this is happening?
Thanks in advance.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Polly;
using RestSharp;
using System;
using System.Collections.Generic;
using System.Net;
namespace FundsAFE.Graphite
{
public class RequestExecutor
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private IRestClient client;
private IRestRequest request;
private Policy<IRestResponse> retryPolicy;
public IRestResponse LastErrorResponse { get; set; }
private static readonly List<HttpStatusCode> invalidStatusCodes = new List<HttpStatusCode> {
HttpStatusCode.BadGateway,
HttpStatusCode.Unauthorized,
HttpStatusCode.InternalServerError,
HttpStatusCode.RequestTimeout,
HttpStatusCode.BadRequest,
HttpStatusCode.Forbidden,
HttpStatusCode.GatewayTimeout
};
public RequestExecutor(IRestClient client, IRestRequest request)
{
this.client = client;
this.request = request;
}
public IRestResponse Execute(int retryCount, int delay)
{
retryPolicy = Policy
.HandleResult<IRestResponse>(resp => invalidStatusCodes.Contains(resp.StatusCode) || !IsValidJson(resp))
.WaitAndRetry(retryCount, i => TimeSpan.FromMilliseconds(delay), (result, timeSpan, currentRetryCount, context) =>
{
//Status code here is sometimes 0???
logger.Error($"Request failed with {result.Result.StatusCode}. Waiting {timeSpan} before next retry. Retry attempt {currentRetryCount}");
LastErrorResponse = result.Result;
});
var policyResponse = retryPolicy.ExecuteAndCapture(() =>
{
var url = client.BuildUri(request);
logger.Debug(url.ToString());
var response = client.Execute(request);
return response;
});
if(policyResponse.Result != null)
{
return policyResponse.Result;
} else
{
return LastErrorResponse;
}
}
public static bool IsValidJson(IRestResponse response)
{
if (response.Content.Length == 0)
{
//Empty response treated as invalid
return false;
}
try
{
var parsed = JObject.Parse(response.Content);
}
catch (JsonReaderException e)
{
//Will catch any mallformed json
return false;
}
return true;
}
}
}
using Microsoft.VisualStudio.TestTools.UnitTesting;
using FundsAFE.Graphite;
using Moq;
using RestSharp;
using System.Net;
using FluentAssertions;
using System;
using FluentAssertions.Extensions;
namespace FundsAFE.Test.Moq
{
[TestClass]
public class MoqUnitTestRequest
{
public Mock<IRestClient> CreateMockClientWithStatusCodeAndContent(HttpStatusCode code, string content)
{
Mock<IRestClient> mockClient = new Mock<IRestClient>();
mockClient.Setup(c => c.Execute(It.IsAny<IRestRequest>())).Returns(
new RestResponse
{
Content = content,
StatusCode = code
}
);
mockClient.Setup(c => c.BuildUri(It.IsAny<IRestRequest>())).Returns(
new Uri("http://fake.fake")
);
return mockClient;
}
[DataTestMethod]
[DataRow(HttpStatusCode.BadGateway)]
[DataRow(HttpStatusCode.Unauthorized)]
[DataRow(HttpStatusCode.InternalServerError)]
[DataRow(HttpStatusCode.RequestTimeout)]
[DataRow(HttpStatusCode.BadRequest)]
[DataRow(HttpStatusCode.Forbidden)]
[DataRow(HttpStatusCode.GatewayTimeout)]
public void TestBadStatusCodesAndRetry(HttpStatusCode httpStatusCode) {
//Arrange
Mock<IRestRequest> mockRequest = new Mock<IRestRequest>();
Mock<IRestClient> mockClient = CreateMockClientWithStatusCodeAndContent(httpStatusCode, "fakecontent");
RequestExecutor requestExecutor = new RequestExecutor(mockClient.Object, mockRequest.Object);
int retries = 10;
int delay = 50;
int totalWaitTime = (retries * delay) - 10; //10ms error margin
//Act and Verify
var response = requestExecutor.Execute(retryCount: retries, delay: 101);
mockClient.Verify(x => x.Execute(It.IsAny<IRestRequest>()), Times.Exactly(retries + 1)); //1st failed attempt + 10 retries = 11
//Assert
requestExecutor.ExecutionTimeOf(re => re.Execute(retries, delay)).Should().BeGreaterOrEqualTo(totalWaitTime.Milliseconds());
response.Should().NotBeNull();
response.StatusCode.Should().Be(httpStatusCode);
requestExecutor.LastErrorResponse.StatusCode.Should().Be(httpStatusCode);
}
[DataTestMethod]
//Empty content
[DataRow("")]
//Missing closing quote
[DataRow("{\"fruit\": \"Apple,\"size\": \"Large\",\"color\": \"Red\"}")]
//Missing angle bracket
[DataRow("\"q1\": {\"question\": \"Which one is correct team name in NBA?\",\"options\": \"New York Bulls\",\"Los Angeles Kings\",\"Golden State Warriros\",\"Huston Rocket\"],\"answer\": \"Huston Rocket\"}")]
//Missing curly bracket
[DataRow("\"sport\": {\"q1\": {\"question\": \"Which one is correct team name in NBA?\",\"options\": \"New York Bulls\",\"Los Angeles Kings\",\"Golden State Warriros\",\"Huston Rocket\"],\"answer\": \"Huston Rocket\"}")]
public void TestBadContentRetries(string content)
{
//Arrange
Mock<IRestRequest> mockRequest = new Mock<IRestRequest>();
Mock<IRestClient> mockClient = CreateMockClientWithStatusCodeAndContent(HttpStatusCode.OK, content);
RequestExecutor requestExecutor = new RequestExecutor(mockClient.Object, mockRequest.Object);
int retries = 10;
int delay = 50;
int totalWaitTime = (retries * delay) - 10; //10ms error margin
//Act and Verify
var response = requestExecutor.Execute(retryCount: retries, delay: delay);
mockClient.Verify(x => x.Execute(It.IsAny<IRestRequest>()), Times.Exactly(retries + 1)); //1st failed attempt + 10 retries = 11
//Assert
requestExecutor.ExecutionTimeOf(re => re.Execute(retries, delay)).Should().BeGreaterOrEqualTo(totalWaitTime.Milliseconds());
response.Should().NotBeNull();
}
}
}
It looks from the RESTsharp source code as if there are cases (eg some exception cases) where RESTsharp might well return you an IRestResponse with resp.StatusCode == 0.
The logging code in onRetry should probably be checking the wider set of status properties on IRestResponse, not just IRestResponse.StatusCode

Translate from C# to PROGRESS OPENEDGE ABL

I received an C# DLL to access an API and another C# to invoke the DLL.
I'm trying to make an ABL program to INVOKE the DLL.
Ive tried the USING, also run it as an EXTERNAL but no luck.
Never used C#, but it looks like a very simple program can't find how to instatiate the DLL from ABL.
Thanks in advance,
Hugo
Here is the C# code, will appreciate any help
Code:
using System;
using System.Windows.Forms;
namespace CayanConnectSample
{
public partial class MainFrm : Form
{
public MainFrm()
{
InitializeComponent();
}
private string merchantName = "Test Dynamic Payments";
private string merchantSiteId = "2Q5JSJH3";
private string merchantKey = "GQPXT-GTJTP-66A1Y-RWT5G-CNULU";
private string terminalIPAddress = "192.168.1.32"; //ip address in CDI Technologies
private int requestTimeout = 60;
private void btnCreateTransaction_Click(object sender, EventArgs e)
{
decimal amount = Convert.ToDecimal(0.09);
string clerkId = "MIKE";
//only transactionType used are sale & refund
CayanConnect.CreateTransaction.Request request = new CayanConnect.CreateTransaction.Request()
{
MerchantName = merchantName,
MerchantSiteId = merchantSiteId,
MerchantKey = merchantKey,
TransactionType = CayanConnect.CreateTransaction.TransactionTypeEnum.SALE,
ClerkId = clerkId,
Dba = merchantName,
Amount = amount,
//[Amount] is always positive. TransactionType has the sign. Sale or Refund
OrderNumber = "1234"
};
CayanConnect.CreateTransaction createTrx = new CayanConnect.CreateTransaction();
CayanConnect.CreateTransaction.Response ctr = createTrx.Process(request, CayanConnect.ThemeEnum.None);
if (ctr.Success)
{
CayanConnect.InitiateTransaction it = new CayanConnect.InitiateTransaction(terminalIPAddress, ctr.TransportKey, null, CayanConnect.ThemeEnum.None, "Waiting for customer...");
CayanConnect.InitiateTransaction.Response response = it.Process(requestTimeout, true);
string emvDetail = response.EMVDetail;
bool approved = false;
if (response.Success)
{
//THERE IS NO TIMEOUT OR ERROR CALLING THE SERVICE
if (response.Status.ToUpper() == "APPROVED")
{
//AN AMOUNT HAS BEEN APPROVED
if (Convert.ToDecimal(Math.Abs(amount)) == response.AmountApproved)
{
//FULL AMOUNT APPROVED
approved = true;
txtResponse.Text = "Good to go!!";
}
else
{
//PARTIALLY APPROVED, HAS TO VOID THIS
string v = this.VoidApprovedTransaction(response.Token);
string em = v.IsEmpty() ? "Transaction was voided succesfully." : v;
txtResponse.Text = $"Invalid approved amount.{Environment.NewLine}Amount: {amount.ToString("C")}{Environment.NewLine}Approved Amount: {response.AmountApproved.ToString("c")}{em}";
}
}
else
{
//AMOUNT WAS DECLINED
txtResponse.Text = response.DeclinedMessage(amount);
}
}
else
{
//THERE WAS A PROBLEM CALLING THE SERVICE
txtResponse.Text = response.ErrorMessage;
}
}
else
{
//THERE WAS A PROBLEM CALLING THE SERVICE
txtResponse.Text = ctr.ErrorMessage;
}
}
private string GetStatus()
{
string rt = string.Empty;
CayanConnect.GetStatus status = new CayanConnect.GetStatus(this.terminalIPAddress, null, CayanConnect.ThemeEnum.None, "Verifying terminal status...");
CayanConnect.GetStatus.Response statusResponse = status.Process(this.requestTimeout);
rt = statusResponse.ToXml();
return rt;
}
private string VoidApprovedTransaction(string token)
{
string rt = string.Empty;
CayanConnect.Void _void = new CayanConnect.Void();
CayanConnect.Void.Request request = new CayanConnect.Void.Request()
{
MerchantName = this.merchantName,
MerchantKey = this.merchantKey,
MerchantSiteId = this.merchantSiteId,
Token = token,
Timeout = this.requestTimeout
};
CayanConnect.Void.Response response = _void.Process(request, CayanConnect.ThemeEnum.None);
if (!response.Success)
{
rt = $"Error voiding transaction.{Environment.NewLine}{Environment.NewLine}{response.ErrorMessage}";
}
return rt;
}
private void btnIsOnLine_Click(object sender, EventArgs e)
{
txtResponse.Text = GetStatus();
}
}
}
============================================================================
You don't need to 'invoke' the DLL. I have found that the DLL's doc is very important to read - you'll need to know things like who's in charge (ABL or the DLL) of memory allocation and deallocation, structure sizes etc. Also, the AVM is not re-entrant (so cannot be registered as a callback for any DLL).
For an example of calling DLL/SO functions from within an ABL class, take a look in the repo at https://github.com/PeterJudge-PSC/abl_odbc_api .
You'll need to create function prototypes (see an example at https://github.com/PeterJudge-PSC/abl_odbc_api/blob/master/src/OpenEdge/Data/ODBC/ODBCConnectionProto.i ) and you can then call those functions from within a method . Take a look at https://github.com/PeterJudge-PSC/abl_odbc_api/blob/master/src/OpenEdge/Data/ODBC/SqlCommonFunctions.cls for examples.

Is it safe to call timer callback method like this?

Please correct me if I have some errors in this logic (not some elegancy things like getting rid of constructor initialization and using Init method instead for Poll). I have not had experience with timer callbacks so far. The code is pretty self-explanatory, I hope. What confuses me a bit is some mix of async things (like connection client creation) and further code - though, I just reused IClient class, it's not mine):
public async Task<WaitForLoanActivationDto> WaitForLoanActivation(string userName, string accountGuid, int timeout)
{
const int dueTime = 0;
const int pollPeriod = 500;
Poll<WaitForLoanActivationDto> state = new Poll<WaitForLoanActivationDto>
{
Client = await _rpcConnectionPool.GetClientAsync(userName),
AutoResetEvent = new AutoResetEvent(false),
StartTime = DateTime.Now,
Timeout = timeout,
Parameters = new Variant[] { accountGuid },
Result = new WaitForLoanActivationDto { Active = false }
};
Timer timer = new Timer(new TimerCallback(WaitForLoanActivationCallback), state, dueTime, pollPeriod);
state.AutoResetEvent.WaitOne();
timer.Dispose(state.AutoResetEvent);
if (state.ThreadException != null)
{
throw state.ThreadException;
}
return state.Result;
}
private void WaitForLoanActivationCallback(object state)
{
Poll<WaitForLoanActivationDto> pollState = (Poll<WaitForLoanActivationDto>)state;
if (pollState.StartTime.AddMilliseconds(pollState.Timeout) >= DateTime.Now)
{
try
{
using (RPCReply reply = ResultHelper.Check(pollState.Client.ExecuteRemoteCommand(WaitForLoanActivationRpcName, pollState.Parameters)))
{
pollState.Result.Active = reply[2].IDList["Active"].AsBoolean().Value;
VariantList statusList = reply[2].IDList["statuses"].List;
if (statusList.Count > 0)
{
var statuses = CustomerInformationConverter.GetStatusesList(statusList);
pollState.Result.Statuses = statuses.ToArray();
}
if (pollState.Result.Active)
{
pollState.AutoResetEvent.Set();
}
}
}
catch (Exception ex)
{
pollState.Result = null;
pollState.ThreadException = ex;
pollState.AutoResetEvent.Set();
}
}
else
{
pollState.AutoResetEvent.Set();
}
}
Thank you guys.
#ckuri, based on your idea I came up with the code below. I have not used Task.Delay though, because as I understood it creates delay even if the task is successfully complete - after it. The objective of my case is to run RPC method each pollPeriod milliseconds during timeout milliseconds. It the method returns Active == false - keep polling, otherwise - return the result. RPC execution time might take more than pollPeriod milliseconds, so if it's already running - no sense to spawn another task.
public async Task<WaitForLoanActivationDto> WaitForLoanActivation(string userName, string accountGuid, int timeout)
{
var cancellationTokenSource = new CancellationTokenSource();
try
{
const int pollPeriod = 500;
IClient client = await _rpcConnectionPool.GetClientAsync(userName);
DateTime startTime = DateTime.Now;
WaitForLoanActivationDto waitForLoanActivationDto = null;
while (startTime.AddMilliseconds(timeout) >= DateTime.Now)
{
waitForLoanActivationDto = await Task.Run(() => WaitForLoanActivationCallback(client, accountGuid), cancellationTokenSource.Token);
if (waitForLoanActivationDto.Active)
{
break;
}
else
{
await Task.Delay(pollPeriod, cancellationTokenSource.Token);
}
}
return waitForLoanActivationDto;
}
catch (AggregateException ex)
{
cancellationTokenSource.Cancel();
throw ex.InnerException;
}
}
private WaitForLoanActivationDto WaitForLoanActivationCallback(IClient client, string accountGuid)
{
using (RPCReply reply = ResultHelper.Check(client.ExecuteRemoteCommand(WaitForLoanActivationRpcName, accountGuid)))
{
var waitForLoanActivationDto = new WaitForLoanActivationDto
{
Active = reply[2].IDList["Active"].AsBoolean().Value
};
VariantList statusList = reply[2].IDList["statuses"].List;
if (statusList.Count > 0)
{
var statuses = CustomerInformationConverter.GetStatusesList(statusList);
waitForLoanActivationDto.Statuses = statuses.ToArray();
}
return waitForLoanActivationDto;
}
}

VersionOne: query.v1 C# OAuth2 gets 401 Unauthorized error but rest-1.oauth.v1/Data/ does work

I am able to do queries using OAuth2 and this:
/rest-1.oauth.v1/Data/Story?sel=Name,Number&Accept=text/json
However I am unable to get the OAuth2 and the new query1.v1 to work against the Sumnmer2013 VersionOne. I am getting (401) Unauthorized and specified method is not supported using two different URLs.
Here is the code that includes the working /rest-1.oauth.v1 and the non-working query1.v1 and non-working query.legacy.v1. Scroll towards the bottom of the code to see the Program Main (starting point of code)
Please advise on what I am missing here.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using OAuth2Client;
namespace ExampleMemberListCSharp
{
class Defaults
{
public static string Scope = "apiv1";
//public static string EndpointUrl = "http://localhost/VersionOne.Web";
public static string EndpointUrl = "https://versionone-test.web.acme.com/summer13_demo";
public static string ApiQueryWorks = "/rest-1.oauth.v1/Data/Member?Accept=text/json";
public static string ApiQuery = "/rest-1.oauth.v1/Data/Story?sel=Name,Number&Accept=text/json";
}
static class WebClientExtensions
{
public static string DownloadStringOAuth2(this WebClient client, IStorage storage, string scope, string path)
{
var creds = storage.GetCredentials();
client.AddBearer(creds);
try
{
return client.DownloadString(path);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.ProtocolError)
{
if (((HttpWebResponse)ex.Response).StatusCode != HttpStatusCode.Unauthorized)
throw;
var secrets = storage.GetSecrets();
var authclient = new AuthClient(secrets, scope);
var newcreds = authclient.refreshAuthCode(creds);
var storedcreds = storage.StoreCredentials(newcreds);
client.AddBearer(storedcreds);
return client.DownloadString(path);
}
throw;
}
}
public static string UploadStringOAuth2(this WebClient client, IStorage storage
, string scope, string path, string pinMethod, string pinQueryBody)
{
var creds = storage.GetCredentials();
client.AddBearer(creds);
client.UseDefaultCredentials = true;
try
{
return client.UploadString(path, pinMethod, pinQueryBody);
}
catch (WebException ex)
{
if (ex.Status == WebExceptionStatus.ProtocolError)
{
if (((HttpWebResponse)ex.Response).StatusCode != HttpStatusCode.Unauthorized)
throw;
var secrets = storage.GetSecrets();
var authclient = new AuthClient(secrets, scope);
var newcreds = authclient.refreshAuthCode(creds);
var storedcreds = storage.StoreCredentials(newcreds);
client.AddBearer(storedcreds);
client.UseDefaultCredentials = true;
return client.UploadString(path, pinMethod, pinQueryBody);
}
throw;
}
}
}
class AsyncProgram
{
private static async Task<string> DoRequestAsync(string path)
{
var httpclient = HttpClientFactory.WithOAuth2("apiv1");
var response = await httpclient.GetAsync(Defaults.EndpointUrl + Defaults.ApiQuery);
var body = await response.Content.ReadAsStringAsync();
return body;
}
public static int MainAsync(string[] args)
{
var t = DoRequestAsync(Defaults.EndpointUrl + Defaults.ApiQuery);
Task.WaitAll(t);
Console.WriteLine(t.Result);
return 0;
}
}
class Program
{
static void Main(string[] args)
{
IStorage storage = Storage.JsonFileStorage.Default;
using (var webclient = new WebClient())
{
// this works:
var body = webclient.DownloadStringOAuth2(storage, "apiv1", Defaults.EndpointUrl + Defaults.ApiQuery);
Console.WriteLine(body);
}
IStorage storage2 = Storage.JsonFileStorage.Default;
using (var webclient2 = new WebClient())
{
// This does NOT work. It throws an exception of (401) Unauthorized:
var body2 = webclient2.UploadStringOAuth2(storage2, "apiv1", Defaults.EndpointUrl + "/query.v1", "SEARCH", QueryBody);
// This does NOT work. It throws an exception of The remote server returned an error: (403): Forbidden."
var body3 = webclient2.UploadStringOAuth2(storage2, "apiv1", Defaults.EndpointUrl + "/query.legacy.v1", "SEARCH", QueryBody);
// These do NOT work. Specified method is not supported:
var body4 = webclient2.UploadStringOAuth2(storage2, "apiv1", Defaults.EndpointUrl + "/oauth.v1/query.legacy.v1", "SEARCH", QueryBody);
var body5 = webclient2.UploadStringOAuth2(storage2, "apiv1", Defaults.EndpointUrl + "/oauth.v1/query.legacy.v1", "SEARCH", QueryBody);
}
Console.ReadLine();
AsyncProgram.MainAsync(args);
}
public const string QueryBody = #"
from: Story
select:
- Name
";
}
}
At this time, the query.v1 endpoint requires the query-api-1.0 scope to be granted.
You'll have to add that to your scope list (it can be simply space-separated e.g. apiv1 query-api-1.0) and visit the grant URL again to authorize the permissions.
This somewhat vital piece of information doesn't seem to appear in the docs on community.versionone.com, so it looks like an update is in order.
Also, only the rest-1.oauth.v1 and query.v1 endpoints respond to OAuth2 headers at this time. A future release will see it apply to all endpoints and remove the endpoint duplication for the two types of authentication
I have had problems in the past trying to use an HTTP method other than POST to transmit the query. Security software, IIS settings, and proxies may all handle such requests in unexpected ways.

Categories

Resources