Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for #153 #155

Merged
merged 5 commits into from
Aug 6, 2019
Merged

Fix for #153 #155

merged 5 commits into from
Aug 6, 2019

Conversation

stegus64
Copy link
Contributor

@stegus64 stegus64 commented Aug 2, 2019

This fixes issue #153 by adding ConfigureAwait(false) to all await points in the library.
It also modifies the unit tests so any future problems like this will cause unit tests to fail.

stegus64 and others added 4 commits August 2, 2019 10:11
Note that this might cause backward compatibility problems. It is possible that someone has noted this problem and added compensating code in the client.
Note that this is only a problem for users where the client is after GMT such as Stockholm or Helsinki
Fix for snowflakedb#154. Removed incorrect call to ToUniversalTime()
@stegus64
Copy link
Contributor Author

stegus64 commented Aug 2, 2019

Added fix also for #154

@stegus64 stegus64 changed the title Fix for #153 Fix for #153 and #154 Aug 2, 2019
@smtakeda smtakeda requested a review from ChTimTsubasa August 2, 2019 17:17
Copy link
Contributor

@ChTimTsubasa ChTimTsubasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the awesome mocking test case to recreate deadlock!
Let's separate the date problem from the deadlock fix and try to solve one at a time.

@@ -205,8 +205,7 @@ private static DateTimeOffset ConvertToDateTimeOffset(string srcVal, SFDataType
}
else
{
long millis = (long)((DateTime)srcVal).ToUniversalTime().Subtract(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably a good idea to change the behavior. We can change the test case to specify the input as UTC time with a Z.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really sure what you mean by this.

Please note that the problem is not in the test case. I dont think you should change the test case.
The issue is that if your client computer is in a timezone after UTC and you send in a normal datetime (with only a date and no timezone specified), the wrong date will be stored in snowflake.
As I said, this can be fixed simply by removing the call to ToUniversalTime()

It is not easy to write a unit test for this scenario since there really is no good way to change the timezone programmatically. The time zone setting in windows is system-wide - it can not be changed per thread or per process. But you can easily recreate the scenario by manually changing the timezone of your development computer to UTC+1 before running the unit tests.

Copy link
Contributor Author

@stegus64 stegus64 Aug 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest that you modify csharpTypeValToSfTypeVal to actually return a Date only, so regardless of the time part specified and the kind of Datetime you should always return a date only.

This can be done with the following change:

                case DbType.Date:
                    destType = SFDataType.DATE.ToString();
                    if (srcVal.GetType() != typeof(DateTime))
                    {
                        throw new SnowflakeDbException(SFError.INVALID_DATA_CONVERSION, srcVal, 
                            srcVal.GetType().ToString(), DbType.Date.ToString());
                    }
                    else
                    {
                        DateTime dt = ((DateTime)srcVal).Date;
                        var ts = dt.Subtract(UnixEpoch);
                        long millis = (long)(ts.TotalMilliseconds);
                        destVal = millis.ToString();
                    }
                    break;

I also suggest adding the following test case to make sure it works regardless of the kind of datetime used and the timezone the computer is in:

        [Test]
        [TestCase("2100-12-31 23:59:59.9999999", DateTimeKind.Utc)]
        [TestCase("2100-12-31 23:59:59.9999999", DateTimeKind.Local)]
        [TestCase("2100-12-31 23:59:59.9999999", DateTimeKind.Unspecified)]
        [TestCase("2200-01-01 00:00:00.0000000", DateTimeKind.Utc)]
        [TestCase("2200-01-01 00:00:00.0000000", DateTimeKind.Local)]
        [TestCase("2200-01-01 00:00:00.0000000", DateTimeKind.Unspecified)]
        [TestCase("1960-01-01 00:00:00.0000000", DateTimeKind.Unspecified)]
        [TestCase("9999-12-31 23:59:59.9999999", DateTimeKind.Unspecified)]
        [TestCase("1982-01-18 16:20:00.6666666", DateTimeKind.Unspecified)]
        [TestCase("1982-01-18 23:59:59.0000000", DateTimeKind.Unspecified)]
        [TestCase(null, DateTimeKind.Unspecified)]
        public void TestConvertDate(string inputTimeStr, object kind=null)
        {
            if (kind == null)
                kind = 0;
            DateTime inputTime;
            if (inputTimeStr == null)
            {
                inputTime = DateTime.Now;
            }
            else
            {
                inputTime = DateTime.ParseExact(inputTimeStr, "yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture);
            }
            var dtExpected = inputTime.Date;
            internalTestConvertDate(dtExpected, DateTime.SpecifyKind(inputTime, (DateTimeKind)kind));
        }

        private void internalTestConvertDate(DateTime dtExpected, DateTime testValue)
        {
            var result = SFDataConverter.csharpTypeValToSfTypeVal(System.Data.DbType.Date, testValue);
            // Convert result to DateTime for easier interpretation
            var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            DateTime dtResult = unixEpoch.AddMilliseconds(Int64.Parse(result.Item2));
            Assert.AreEqual(dtExpected, dtResult);
        }

using System.Text;
using System.Threading;

namespace Snowflake.Data.Tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome mock case!

@ChTimTsubasa
Copy link
Contributor

Other than adding ConfigureAwait(false), we may also want to consider the method described in https://devblogs.microsoft.com/premier-developer//one-user-scenario-to-rule-them-all/ to have a custom awaiter. 2 benefits are:

  1. We don't need to attach ConfigureAwait(false) to every line of await;
  2. For 2nd and 3rd party code that we used but don't have control on, we can avoid having deadlock.

@stegus64
Copy link
Contributor Author

stegus64 commented Aug 6, 2019

Other than adding ConfigureAwait(false), we may also want to consider the method described in https://devblogs.microsoft.com/premier-developer//one-user-scenario-to-rule-them-all/ to have a custom awaiter. 2 benefits are:

  1. We don't need to attach ConfigureAwait(false) to every line of await;
  2. For 2nd and 3rd party code that we used but don't have control on, we can avoid having deadlock.

Interesting idea. I had not seen that article.

But note that you would have to add
await Awaiters.DetachCurrentSyncContext();
To every async method in the library that could possibly be called directly or indirectly from a public method. This might be almost as much work as adding ConfigureAwait() at all await points in the library.

But I agree that it is very good that this will work even for 3rd party code.

In the end I think it is mostly a matter of taste.

@ChTimTsubasa
Copy link
Contributor

My understanding is that we only need to call await Awaiters.DetachCurrentSyncContext(); at the start of each public method or the starting point where we call async methods inside a sync method. Once the customer awaiter swaps in, the following awaits in the same methods or in the called methods won't have deadlock anymore. Otherwise the 3rd party code case won't work.

@stegus64
Copy link
Contributor Author

stegus64 commented Aug 6, 2019

My understanding is that we only need to call await Awaiters.DetachCurrentSyncContext(); at the start of each public method or the starting point where we call async methods inside a sync method. Once the customer awaiter swaps in, the following awaits in the same methods or in the called methods won't have deadlock anymore. Otherwise the 3rd party code case won't work.

Yes, this was what i meant.

But it is not always so easy to find which async methods are called by sync methods. It is much easier to just make sure that all async methods have this line at the beginning. Just like it is much easier to add ConfigureAwait() to all await points rather than trying to figure out where it is actually needed.

@ChTimTsubasa
Copy link
Contributor

My understanding is that we only need to call await Awaiters.DetachCurrentSyncContext(); at the start of each public method or the starting point where we call async methods inside a sync method. Once the customer awaiter swaps in, the following awaits in the same methods or in the called methods won't have deadlock anymore. Otherwise the 3rd party code case won't work.

Yes, this was what i meant.

But it is not always so easy to find which async methods are called by sync methods. It is much easier to just make sure that all async methods have this line at the beginning. Just like it is much easier to add ConfigureAwait() to all await points rather than trying to figure out where it is actually needed.

I see what you meant. Would the testcase you added failed if there is a 3rd party code that we used does not follow the ConfigureAwait(false) style? If yes then I would agree with the ConfigureAwait(false) solution

@stegus64
Copy link
Contributor Author

stegus64 commented Aug 6, 2019

I see what you meant. Would the testcase you added failed if there is a 3rd party code that we used does not follow the ConfigureAwait(false) style? If yes then I would agree with the ConfigureAwait(false) solution

Yes, the testcase should fail even if the problem is in 3rd party code. I think it would fail for any situation that would cause an async deadlock.

@ChTimTsubasa
Copy link
Contributor

@stegus64 Can you remove the fix to #154 in this PR and open another PR for that? We need to make an announcement before hand since that would be an behavior change and let's don't delay this one.

I will submit a testcase PR of your fix for #153 and if that passes we can check in your change.

@stegus64
Copy link
Contributor Author

stegus64 commented Aug 6, 2019

This is actually the first time I use a fork and pull requests with a public github project.
I originally tried to create a separate pull request, but I did something wrong so it ended up on the same pr.

Do you have any tips for how I should do this?

This reverts commit c95f529, reversing
changes made to 0b2f1d6.
@stegus64 stegus64 changed the title Fix for #153 and #154 Fix for #153 Aug 6, 2019
@stegus64
Copy link
Contributor Author

stegus64 commented Aug 6, 2019

I have now tried to separate the fix for #154 into its own pr. I hope it is correct.

Copy link
Contributor

@ChTimTsubasa ChTimTsubasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once tests for #157 clears, I would commit this PR.

@ChTimTsubasa ChTimTsubasa merged commit 17841ce into snowflakedb:prep-1.0.13 Aug 6, 2019
@ChTimTsubasa
Copy link
Contributor

@stegus64 Committed to prep-1.0.13 and will be release through 1.0.13 next week. Thanks for your contribution!

ankit-bhatnagar167 pushed a commit that referenced this pull request Aug 12, 2019
* Fixes #153 by adding ConfigureAwait(false) to all await points in the library

* Modified test base class so problems like #153 will cause tests to fail
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants