Skip to content
This repository has been archived by the owner on Jan 19, 2024. It is now read-only.

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

Open
1 of 4 tasks
TraGicCode opened this issue Jun 17, 2021 · 0 comments
Labels

Comments

@TraGicCode
Copy link

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

  • 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 TraGicCode added the bug label Jun 17, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

1 participant