none
Unexpected ADLS Gen2 REST API PATCH File ETag Behavior RRS feed

  • Question

  • Hi,

    I'm seeing some strange behavior when using the ADLS Gen2 REST API, specifically when using the PATCH File operations on files that already exist. In short, the API allows you to do multiple PATCH and FLUSH operations to overlapping byte ranges and returns a success code.

    If you call flush again with the same offset you get an OK response along with a new ETag. However, the underlying file does not change in this case, and the returned ETag is not valid.

    I would expect the following:

    - Calling PATCH/append against a committed offset returns an error

    - Calling PATCH/flush either returns an error OR returns the correct ETag when called against the current offset

    Here's a test example and a test:

    [Test]
    public async Task Multiple_Commit_Behavior_Test()
    {
    	string file = "testfile.txt";
    	var createResult = await _adlsClient.CreateFileAsync(file);
    	createResult.EnsureSuccessStatusCode();
    
    	string firstEtag = createResult.ETag;
    
    	var myContent = Encoding.UTF8.GetBytes("my content");
    
    	var writeBlockResult =  await _adlsClient.WriteUncommittedBlockAsync(file, myContent, 0);
    	writeBlockResult.EnsureSuccessStatusCode();
    	writeBlockResult.ETag.Should().BeNullOrWhiteSpace();  // PATCH/Append does not return an ETAG
    
    	var commitBlockResult = await _adlsClient.CommitBlocksAsync(file, (ulong)myContent.Length, firstEtag);
    	commitBlockResult.EnsureSuccessStatusCode();
    
    	string etagAfterCommit = commitBlockResult.ETag;
    	etagAfterCommit.Should().NotBe(firstEtag);
    
    	// We did a write, now we can use the returned etag. it's the same
    	var propertiesResult = await _adlsClient.GetPropertiesAsync(file, etagAfterCommit);
    	propertiesResult.ETag.Should().Be(etagAfterCommit);
    	
    
    	// We wrote some bytes then flushed, write at the same length at the same offset.
    	var myContent2 = Encoding.UTF8.GetBytes("newcontent");
    	var writeBlockResult2 =  await _adlsClient.WriteUncommittedBlockAsync(file, myContent2, 0);
    	writeBlockResult2.EnsureSuccessStatusCode();
    	writeBlockResult2.ETag.Should().BeNullOrWhiteSpace();
    
    	
    	// Why does this return OK? isn't this invalid since we are overwriting previously flushed data? I would expect a 400.
    	var commitBlockResult2 = await _adlsClient.CommitBlocksAsync(file, (ulong)myContent2.Length, etagAfterCommit);
    	commitBlockResult2.EnsureSuccessStatusCode();
    	string etagAfterCommit2 = commitBlockResult2.ETag;
    
    	etagAfterCommit2.Should().NotBe(etagAfterCommit); // we get a new etag here...
    
    	var proprtiesResult2 = await _adlsClient.GetPropertiesAsync(file, etagAfterCommit2);
    	Assert.Throws<Exception>(() => proprtiesResult2.EnsureSuccessStatusCode()); // ... but it's not valid. This is a 409 - Conflict on the bad etag.
    
    	var propertiesResult3 = await _adlsClient.GetPropertiesAsync(file, etagAfterCommit); // we have to use the etag after the first commit. This works.
    	propertiesResult3.EnsureSuccessStatusCode();
    
    	Debug.WriteLine($@"
    First ETag: {firstEtag}
    ETag after Commit: {etagAfterCommit}
    First Properties ETag {propertiesResult.ETag}
    ETag after Second Commit: {etagAfterCommit2}
    Third properties ETag: {propertiesResult3.ETag}");
    
    	
    	// If we write content of different length we _do_ get a 400.
    	var myContent3 = Encoding.UTF8.GetBytes("newcontentdifferentlength");
    	var writeBlockResult3 =  await _adlsClient.WriteUncommittedBlockAsync(file, myContent3, 0);
    	writeBlockResult3.EnsureSuccessStatusCode();
    	writeBlockResult3.ETag.Should().BeNullOrWhiteSpace();
    
    	
    	// We do get a 400 BadRequest if the content length changes!
    	var crashes = await _adlsClient.CommitBlocksAsync(file, (ulong)myContent3.Length, etagAfterCommit);
    	Assert.Throws<Exception>(() => crashes.EnsureSuccessStatusCode()); // expected behavior
    }

    Here is the HTTP trace of the calls:

    Create File:
    Method: PUT, RequestUri: 'https://**SNIP**.dfs.core.windows.net/720cf61e-c7a6-45d1-83c1-c7613e840aac/testfile.txt?resource=file', Version: 1.1, Content: <null>, Headers:
    {
      x-ms-version: 2019-02-02
      x-ms-date: Wed, 20 Nov 2019 16:26:25 GMT
      If-None-Match: *
      Authorization: SharedKey **SNIP**
    }
    Create File Response:
    StatusCode: 201, ReasonPhrase: 'Created', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
    {
      ETag: "0x8D76DD663B1D8DF"
      Server: Windows-Azure-HDFS/1.0
      Server: Microsoft-HTTPAPI/2.0
      x-ms-request-id: 78235f18-d01f-0047-21bf-9f3ae8000000
      x-ms-version: 2019-02-02
      Date: Wed, 20 Nov 2019 16:26:25 GMT
      Last-Modified: Wed, 20 Nov 2019 16:26:25 GMT
      Content-Length: 0
    }
    
    
    ----------
    
    First Append:
    Method: PATCH, RequestUri: 'https://**SNIP**.dfs.core.windows.net/720cf61e-c7a6-45d1-83c1-c7613e840aac/testfile.txt?action=append&position=0', Version: 1.1, Content: System.Net.Http.ReadOnlyMemoryContent, Headers:
    {
      x-ms-version: 2019-02-02
      x-ms-date: Wed, 20 Nov 2019 16:26:26 GMT
      Authorization: SharedKey **SNIP**
      Content-Length: 10
    }
    Append Response:
    StatusCode: 202, ReasonPhrase: 'Accepted', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
    {
      Server: Windows-Azure-HDFS/1.0
      Server: Microsoft-HTTPAPI/2.0
      x-ms-request-server-encrypted: true
      x-ms-request-id: 78235f1a-d01f-0047-22bf-9f3ae8000000
      x-ms-version: 2019-02-02
      Date: Wed, 20 Nov 2019 16:26:25 GMT
      Content-Length: 0
    }
    
    ---------
    
    
    First Flush:
    Method: PATCH, RequestUri: 'https://**SNIP**.dfs.core.windows.net/720cf61e-c7a6-45d1-83c1-c7613e840aac/testfile.txt?action=flush&position=10&close=true', Version: 1.1, Content: <null>, Headers:
    {
      x-ms-version: 2019-02-02
      x-ms-date: Wed, 20 Nov 2019 16:26:26 GMT
      If-Match: "0x8D76DD663B1D8DF"
      Authorization: SharedKey **SNIP**
    }
    Flush Response:
    StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
    {
      ETag: "0x8D76DD663C57480"
      Server: Windows-Azure-HDFS/1.0
      Server: Microsoft-HTTPAPI/2.0
      x-ms-request-server-encrypted: true
      x-ms-request-id: 78235f1b-d01f-0047-23bf-9f3ae8000000
      x-ms-version: 2019-02-02
      Date: Wed, 20 Nov 2019 16:26:25 GMT
      Last-Modified: Wed, 20 Nov 2019 16:26:26 GMT
      Content-Length: 0
    }
    
    
    --------
    
    
    First Get Metadata:
    Method: HEAD, RequestUri: 'https://**SNIP**.dfs.core.windows.net/720cf61e-c7a6-45d1-83c1-c7613e840aac/testfile.txt', Version: 1.1, Content: <null>, Headers:
    {
      x-ms-version: 2019-02-02
      x-ms-date: Wed, 20 Nov 2019 16:26:26 GMT
      If-Match: "0x8D76DD663C57480"
      Authorization: SharedKey **SNIP**
    }
    Get Metadata Response:
    StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
    {
      Accept-Ranges: bytes
      ETag: "0x8D76DD663C57480"
      Server: Windows-Azure-HDFS/1.0
      Server: Microsoft-HTTPAPI/2.0
      x-ms-owner: $superuser
      x-ms-group: $superuser
      x-ms-permissions: rw-r-----
      x-ms-properties: 
      x-ms-resource-type: file
      x-ms-lease-state: available
      x-ms-lease-status: unlocked
      x-ms-content-crc64: AAAAAAAAAAA=
      x-ms-server-encrypted: false
      x-ms-request-id: 78235f1c-d01f-0047-24bf-9f3ae8000000
      x-ms-version: 2019-02-02
      Date: Wed, 20 Nov 2019 16:26:25 GMT
      Content-Length: 10
      Content-Type: application/octet-stream
      Last-Modified: Wed, 20 Nov 2019 16:26:26 GMT
    }
    
    
    
    
    ----------
    
    
    Second Append:
    Method: PATCH, RequestUri: 'https://**SNIP**.dfs.core.windows.net/720cf61e-c7a6-45d1-83c1-c7613e840aac/testfile.txt?action=append&position=0', Version: 1.1, Content: System.Net.Http.ReadOnlyMemoryContent, Headers:
    {
      x-ms-version: 2019-02-02
      x-ms-date: Wed, 20 Nov 2019 16:26:26 GMT
      Authorization: SharedKey **SNIP**
      Content-Length: 10
    }
    Append Response:
    StatusCode: 202, ReasonPhrase: 'Accepted', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
    {
      Server: Windows-Azure-HDFS/1.0
      Server: Microsoft-HTTPAPI/2.0
      x-ms-request-server-encrypted: true
      x-ms-request-id: 78235f1d-d01f-0047-25bf-9f3ae8000000
      x-ms-version: 2019-02-02
      Date: Wed, 20 Nov 2019 16:26:25 GMT
      Content-Length: 0
    }
    
    
    -------------
    
    
    
    Second Flush:
    Method: PATCH, RequestUri: 'https://**SNIP**.dfs.core.windows.net/720cf61e-c7a6-45d1-83c1-c7613e840aac/testfile.txt?action=flush&position=10&close=true', Version: 1.1, Content: <null>, Headers:
    {
      x-ms-version: 2019-02-02
      x-ms-date: Wed, 20 Nov 2019 16:26:26 GMT
      If-Match: "0x8D76DD663C57480"
      Authorization: SharedKey **SNIP**
    }
    Flush Response:
    StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
    {
      ETag: "0x8D76DD663D4AC32"
      Server: Windows-Azure-HDFS/1.0
      Server: Microsoft-HTTPAPI/2.0
      x-ms-request-server-encrypted: true
      x-ms-request-id: 78235f1e-d01f-0047-26bf-9f3ae8000000
      x-ms-version: 2019-02-02
      Date: Wed, 20 Nov 2019 16:26:25 GMT
      Last-Modified: Wed, 20 Nov 2019 16:26:26 GMT
      Content-Length: 0
    }
    
    
    -----
    
    Second Get Metadata:
    Method: HEAD, RequestUri: 'https://**SNIP**.dfs.core.windows.net/720cf61e-c7a6-45d1-83c1-c7613e840aac/testfile.txt', Version: 1.1, Content: <null>, Headers:
    {
      x-ms-version: 2019-02-02
      x-ms-date: Wed, 20 Nov 2019 16:26:26 GMT
      If-Match: "0x8D76DD663D4AC32"
      Authorization: SharedKey **SNIP**
    }
    Get Metadata Response:
    StatusCode: 412, ReasonPhrase: 'The condition specified using HTTP conditional header(s) is not met.', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
    {
      Server: Windows-Azure-HDFS/1.0
      Server: Microsoft-HTTPAPI/2.0
      x-ms-error-code: ConditionNotMet
      x-ms-request-id: 78235f1f-d01f-0047-27bf-9f3ae8000000
      x-ms-version: 2019-02-02
      Date: Wed, 20 Nov 2019 16:26:25 GMT
    }
    
    -------------
    
    
    Third Get Metadata (no ETag):
    Method: HEAD, RequestUri: 'https://**SNIP**.dfs.core.windows.net/720cf61e-c7a6-45d1-83c1-c7613e840aac/testfile.txt', Version: 1.1, Content: <null>, Headers:
    {
      x-ms-version: 2019-02-02
      x-ms-date: Wed, 20 Nov 2019 16:26:29 GMT
      If-Match: "0x8D76DD663C57480"
      Authorization: SharedKey **SNIP**
    }
    Get Metadata Response:
    StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
    {
      Accept-Ranges: bytes
      ETag: "0x8D76DD663C57480"
      Server: Windows-Azure-HDFS/1.0
      Server: Microsoft-HTTPAPI/2.0
      x-ms-owner: $superuser
      x-ms-group: $superuser
      x-ms-permissions: rw-r-----
      x-ms-properties: 
      x-ms-resource-type: file
      x-ms-lease-state: available
      x-ms-lease-status: unlocked
      x-ms-content-crc64: AAAAAAAAAAA=
      x-ms-server-encrypted: false
      x-ms-request-id: 78235f20-d01f-0047-28bf-9f3ae8000000
      x-ms-version: 2019-02-02
      Date: Wed, 20 Nov 2019 16:26:29 GMT
      Content-Length: 10
      Content-Type: application/octet-stream
      Last-Modified: Wed, 20 Nov 2019 16:26:26 GMT
    }
    
    First ETag: "0x8D76DD663B1D8DF"
    ETag after Commit: "0x8D76DD663C57480"
    First Properties ETag "0x8D76DD663C57480"
    ETag after Second Commit: "0x8D76DD663D4AC32"
    Third properties ETag: "0x8D76DD663C57480"


    Wednesday, November 20, 2019 4:49 PM

All replies