FuelSDK-CSharp icon indicating copy to clipboard operation
FuelSDK-CSharp copied to clipboard

[BUG] Multiple Singleton ETClients not supported due to refresh token design/implementation issue

Open TraGicCode opened this issue 3 years ago • 0 comments

Describe the bug We have a Single Application that needs to send emails for Multiple BU's. Unfortunately, due to how the ETClient is designed/implemented, Creating Multiple ETClient's as a singleton causes them to fight with each other when the OAuth Access Token is refreshed ( most likely due to the static members being used ). This means that we are unable to follow best practices and cache the OAuth Tokens using this provided library and instead must request a new OAuth Token on every request potentially hitting a rate limit and experience a performance impact due to the OAuth web request.

To Reproduce Below is a simple Unit Test to reproduce the issue. Simply wait for the token refresh to occur.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using FuelSDK;
using NUnit.Framework;

namespace ETClientThreadingBug
{
    
    public static class SubscriberProperties
    {
        public const string EmailAddress = "Email Address";
        public const string FullName = "Full Name";
        public const string FirstName = "First Name";
        public const string LastName = "Last Name";
    }
    [TestFixture]
    public class Tests
    {
        [Test]
        public void SingleEtClientSingletonWorksFineOnTokenRefresh()
        {
            var etClientOneBca = CreateEtClientOne();
            IList<Tuple<ETClient, int>> etClients = new List<Tuple<ETClient, int>>
            {
                new Tuple<ETClient, int>(etClientOneBca, OneListId),
            };
            while (true)
            {
                var etClientToUse = etClients[0];
                DoWork(etClientToUse);
            }
        }
        
        [Test]
        public void DoubleEtClientSingletonFailsOnTokenRefresh()
        {
            var etClientOneBca = CreateEtClientOne();
            var etClientTwoBcom = CreateEtClientTwo();
            IList<Tuple<ETClient, int>> etClients = new List<Tuple<ETClient, int>>
            {
                new Tuple<ETClient, int>(etClientOneBca, OneListId),
                new Tuple<ETClient, int>(etClientTwoBcom, TwoListId)
            };
            while (true)
            {
                var random = (DateTime.Now.Ticks % 2) == 0L ? 0 : 1;
                var etClientToUse = etClients[0];
                DoWork(etClientToUse);
            }
        }

        private void DoWork(Tuple<ETClient, int> etClientToUse)
        {
            var emailAddress = "[email protected]";
            var subscriber = new ETSubscriber
            {
                AuthStub = etClientToUse.Item1,
                SubscriberKey = emailAddress,
                EmailAddress = emailAddress,
                Lists = new SubscriberList[] {new ETSubscriberList {ID = etClientToUse.Item2, IDSpecified = true}},
                Attributes = new FuelSDK.Attribute[]
                {
                    new ETProfileAttribute {Name = SubscriberProperties.EmailAddress, Value = emailAddress},
                    new ETProfileAttribute
                        {Name = SubscriberProperties.FullName, Value = "boby dehop"},
                    new ETProfileAttribute
                        {Name = SubscriberProperties.FirstName, Value = "boby"},
                    new ETProfileAttribute
                        {Name = SubscriberProperties.LastName, Value = "dehop"}
                }
            };
            Console.WriteLine($"====================================================");
            Console.WriteLine($"Error OrgId: {etClientToUse.Item1.OrganizationId}");
            Console.WriteLine($"Error AuthToken: {etClientToUse.Item1.AuthToken}");
            Console.WriteLine($"Error InternalAuthToken: {etClientToUse.Item1.InternalAuthToken}");
            Console.WriteLine($"====================================================");
            var postReturn = subscriber.Post();
            var didItWork = postReturn.Status;
            //dws: check for specific error (already exists) on create
            //dws: there should be a better way to do this, this is what SF has in their sample code
            if (postReturn.Results.Any() && postReturn.Results[0].ErrorCode == 12014)
            {
                var patchReturn = subscriber.Patch();
                didItWork = patchReturn.Status;
                if (!didItWork)
                {
                    // AddErrorResult(listId.ToString(), patchReturn.Results[0]);
                    // Log.Error(GetErrorMessages() + $"Uploading subscriber information {customer.EmailAddress} to Exact Target list {listId} for site #{siteId}");
                    Debug.WriteLine($"====================================================");
                    Debug.WriteLine($"Error OrgId: {etClientToUse.Item1.OrganizationId}");
                    Debug.WriteLine($"Error AuthToken: {etClientToUse.Item1.AuthToken}");
                    Debug.WriteLine($"Error InternalAuthToken: {etClientToUse.Item1.InternalAuthToken}");
                    Debug.WriteLine($"====================================================");
                }
            }
            else
            {
                if (!didItWork)
                {
                    Debug.WriteLine($"====================================================");
                    Debug.WriteLine($"Error OrgId: {etClientToUse.Item1.OrganizationId}");
                    Debug.WriteLine($"Error AuthToken: {etClientToUse.Item1.AuthToken}");
                    Debug.WriteLine($"Error InternalAuthToken: {etClientToUse.Item1.InternalAuthToken}");
                    Debug.WriteLine($"====================================================");
                }
            }

            Thread.Sleep(5000);
        }

        private int OneListId = 5555;
        private ETClient CreateEtClientOne()
        {
            var settings = new NameValueCollection
            {
                {"clientId", "xx"},
                {"clientSecret", "xx"},
                {"authEndPoint", "https://mc1cglt-xx.auth.marketingcloudapis.com"},
                {"useOAuth2Authentication", "true"},
                {"accountId", "xx"}
            };
            return new ETClient(settings);
        }
        
        private int TwoListId = 5556;
        private ETClient CreateEtClientTwo()
        {
            var settings = new NameValueCollection
            {
                {"clientId", "xxxx"},
                {"clientSecret", "xxxx"},
                {"authEndPoint", "https://mc1cglt-xxxx.auth.marketingcloudapis.com"},
                {"useOAuth2Authentication", "true"},
                {"accountId", "xxxx"}
            };
            return new ETClient(settings);
        }

    }
}

Expected behavior ETClient should be able to allow OAuth Access Token caching correctly across multiple ETClients for applications that need to communicate with multiple BU's.

Screenshots N/A

Environment

  • SFMC.FueldSDK 1.3.0
  • .NET Framework version 4.7.1

The bug has the severity

  • [X] Critical: The defect affects critical functionality or critical data. It does not have a workaround.
  • [ ] Major: The defect affects major functionality or major data. It has a workaround but is not obvious and is difficult.
  • [ ] Minor: The defect affects minor functionality or non-critical data. It has an easy workaround.
  • [ ] Trivial: The defect does not affect functionality or data. It does not even need a workaround. It does not impact productivity or efficiency. It is merely an inconvenience.

TraGicCode avatar Jun 17 '21 14:06 TraGicCode