为什么在使用WaitGroup上传文件时,这个独立的goroutine会发生死锁?

huangapple go评论68阅读模式
英文:

Go | Why does this singular goroutine deadlock when using a WaitGroup while uploading a file?

问题

**编辑:**主要问题实际上是上传过程本身而不是发生的死锁,这只是由于错误放置的wg.Wait()引起的。


我正在尝试通过他们的API将文件上传到在线文件托管服务(https://anonfiles.com/)。有一个20GB的上传文件大小限制。

我可以使用下面的代码上传一个大约2KB的简单文本文件。然而,如果我尝试使用一个更大的文件,比如大约2MB,我会从他们的API得到以下错误:No file chosen

我认为这是因为下面的代码没有等待go例程正确完成,所以我添加了一个等待组。然后我从Go得到了这个错误:fatal error: all goroutines are asleep - deadlock!

我尝试删除下面的WaitGroup,它似乎导致了死锁;但是在go例程实际完成之前,go例程下面的代码将会运行。

删除了WaitGroup后,我仍然可以上传KB大小的文件,但是大文件无法正确上传到文件托管,因为我从他们的API收到了No file chosen错误。

package main

import (
	"fmt"
	"io"
	"log"
	"math/rand"
	"mime/multipart"
	"net/http"
	"os"
	"sync"
	"time"
)

func main() {
	client := http.Client{}

    // 上传一个大于2MB的壁纸。
	file, err := os.Open("./wallpaper.jpg")
	if err != nil {
		log.Fatal(err)
	}

	defer file.Close()

	reader, writer := io.Pipe()
	multipart := multipart.NewWriter(writer)

    /* 
    添加WaitGroup以确保例程正确完成。结果导致死锁。
	wg := new(sync.WaitGroup)
	wg.Add(1) 
    */

	go func() {
		fmt.Println("开始上传...")
		defer wg.Done()
		defer writer.Close()
		defer multipart.Close()

		part, err := multipart.CreateFormFile("file", file.Name())
		if err != nil {
			log.Fatal(err)
		}

		fmt.Println("复制中...")
		if _, err = io.Copy(part, file); err != nil {
			log.Fatal(err)
		}
	}()

    fmt.Println("在WaitGroup完成之前,下面的代码将会运行。")

	req, err := http.NewRequest(http.MethodPost, "https://api.anonfiles.com/upload", reader)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Add("Content-Type", multipart.FormDataContentType())

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}

    wg.Wait()

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(body))
}

我已经研究了几个问题,但没有一个适用于我的问题。是什么导致了这个死锁?有什么不同的方法可以做?也许这是一个新手错误,任何建议或帮助将不胜感激。

英文:

Edit: The main problem turned out to be the actual uploading process and not the deadlock that occured, which was simply caused by a misplaced wg.Wait()
<hr>
I am trying to upload a file to an online file hosting service (https://anonfiles.com/) via their API. There is an upload file size limit of 20GB.

I can upload a simple text file that is around 2KB with the code below. However, if I try to do the same with a larger file, lets say, around 2MB, I get the following error from their API: No file chosen.

I thought this was because the code (below) was not waiting for the go routine to properly finish, so I added a wait group. I then got this error from Go: fatal error: all goroutines are asleep - deadlock!.

I have tried removing the WaitGroup below that seems to be causing the deadlock; but then the code below the go routine will run before the go routine is actually finished.

With the WaitGroup removed, I can still upload files that are KB in size, but files that are larger do not upload to the file hosting correctly, since I receive the No file chosen error from their API.

package main

import (
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;log&quot;
	&quot;math/rand&quot;
	&quot;mime/multipart&quot;
	&quot;net/http&quot;
	&quot;os&quot;
	&quot;sync&quot;
	&quot;time&quot;
)

func main() {
	client := http.Client{}

    // Upload a &gt;2MB wallpaper.
	file, err := os.Open(&quot;./wallpaper.jpg&quot;)
	if err != nil {
		log.Fatal(err)
	}

	defer file.Close()

	reader, writer := io.Pipe()
	multipart := multipart.NewWriter(writer)

    /* 
    Added Waitgroup to make sure the routine properly finishes. Instead, causes deadlock.
	wg := new(sync.WaitGroup)
	wg.Add(1) 
    */

	go func() {
		fmt.Println(&quot;Starting Upload...&quot;)
		defer wg.Done()
		defer writer.Close()
		defer multipart.Close()

		part, err := multipart.CreateFormFile(&quot;file&quot;, file.Name())
		if err != nil {
			log.Fatal(err)
		}

		fmt.Println(&quot;Copying...&quot;)
		if _, err = io.Copy(part, file); err != nil {
			log.Fatal(err)
		}
	}()

    fmt.Println(&quot;The code below will run before the goroutine is finished; without the WaitGroup.&quot;)

	req, err := http.NewRequest(http.MethodPost, &quot;https://api.anonfiles.com/upload&quot;, reader)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Add(&quot;Content-Type&quot;, multipart.FormDataContentType())

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}

    wg.Wait()

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(body))
}

I have researched several issues, but none seem to apply to my problem. What is causing this to lock up? What can be done differently? Perhaps this is some rookie mistake, any suggestions or help would be appreciated.

答案1

得分: 0

TL;DR

设置请求的Content-Length头。

附带了一个工作演示的代码示例。

调试

我认为死锁问题在这里并不重要。你的目的是将文件上传到https://anonfiles.com/。所以我将专注于调试上传问题。

首先,让我们使用curl上传一个文件:

curl -F "file=@test.txt" https://api.anonfiles.com/upload

它可以正常工作。

然后让我们使用你的演示上传相同的文件,但是它失败了,并返回了误导性的响应:

{
  "status": false,
  "error": {
    "message": "No file chosen.",
    "type": "ERROR_FILE_NOT_PROVIDED",
    "code": 10
  }
}

现在让我们将目标地址https://api.anonfiles.com/upload替换为https://httpbin.org/post,这样我们可以比较请求:

  {
   "args": {},
   "data": "",
   "files": {
     "file": "aaaaaaaaaa\n"
   },
   "form": {},
   "headers": {
-    "Accept": "*/*",
-    "Content-Length": "197",
-    "Content-Type": "multipart/form-data; boundary=------------------------bd4a81e725230fa6",
+    "Accept-Encoding": "gzip",
+    "Content-Type": "multipart/form-data; boundary=2d4e7969789ed6ef6ff3e7b815db3aa040fd3994a34fbaedec85240dc5af",
     "Host": "httpbin.org",
-    "User-Agent": "curl/7.81.0",
-    "X-Amzn-Trace-Id": "Root=1-63747739-2c1dab1b122b7e3a4db8ca79"
+    "Transfer-Encoding": "chunked",
+    "User-Agent": "Go-http-client/2.0",
+    "X-Amzn-Trace-Id": "Root=1-63747872-2fbc85f81c6dde7e5b2091c4"
   },
   "json": null,
   "origin": "47.242.15.156",
   "url": "https://httpbin.org/post"
 }

显著的区别是curl发送了"Content-Length": "197",而Go应用程序发送了"Transfer-Encoding": "chunked"

让我们尝试修改Go应用程序以发送Content-Length头:

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"strings"
)

func main() {
	source := strings.NewReader(strings.Repeat("a", 1<<21))

	buf := new(bytes.Buffer)
	multipart := multipart.NewWriter(buf)

	part, err := multipart.CreateFormFile("file", "test.txt")
	if err != nil {
		log.Fatal(err)
	}

	if _, err := io.Copy(part, source); err != nil {
		log.Fatal(err)
	}
	multipart.Close()

	req, err := http.NewRequest(http.MethodPost, "https://api.anonfiles.com/upload", buf)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Add("Content-Type", multipart.FormDataContentType())

	// The following line is not required because the http client will set it
	// because the request body is a bytes.Buffer.
	// req.ContentLength = int64(buf.Len())

	client := http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(body))
}

它可以正常工作!

缺点是它必须先将请求体复制到内存中。在我看来,这是不可避免的,因为它需要知道请求体的大小。

英文:

TL;DR

Set the Content-Length header of the request.

A working demo is attached to the end of this answer.

Debugging

I think the deadlock issue is not important here. Your purpose is to upload files to https://anonfiles.com/. So I will focus on debugging the uploading issue.

First, let's upload a file with curl:

curl -F &quot;file=@test.txt&quot; https://api.anonfiles.com/upload

It works.

Then let's upload the same file with your demo, it fails with the misleading response:

{
  &quot;status&quot;: false,
  &quot;error&quot;: {
    &quot;message&quot;: &quot;No file chosen.&quot;,
    &quot;type&quot;: &quot;ERROR_FILE_NOT_PROVIDED&quot;,
    &quot;code&quot;: 10
  }
}

Now let's replace the target https://api.anonfiles.com/upload with https://httpbin.org/post so that we can compare the requets:

  {
   &quot;args&quot;: {}, 
   &quot;data&quot;: &quot;&quot;, 
   &quot;files&quot;: {
     &quot;file&quot;: &quot;aaaaaaaaaa\n&quot;
   }, 
   &quot;form&quot;: {}, 
   &quot;headers&quot;: {
-    &quot;Accept&quot;: &quot;*/*&quot;, 
-    &quot;Content-Length&quot;: &quot;197&quot;, 
-    &quot;Content-Type&quot;: &quot;multipart/form-data; boundary=------------------------bd4a81e725230fa6&quot;, 
+    &quot;Accept-Encoding&quot;: &quot;gzip&quot;,
+    &quot;Content-Type&quot;: &quot;multipart/form-data; boundary=2d4e7969789ed6ef6ff3e7b815db3aa040fd3994a34fbaedec85240dc5af&quot;,
     &quot;Host&quot;: &quot;httpbin.org&quot;, 
-    &quot;User-Agent&quot;: &quot;curl/7.81.0&quot;, 
-    &quot;X-Amzn-Trace-Id&quot;: &quot;Root=1-63747739-2c1dab1b122b7e3a4db8ca79&quot;
+    &quot;Transfer-Encoding&quot;: &quot;chunked&quot;,
+    &quot;User-Agent&quot;: &quot;Go-http-client/2.0&quot;,
+    &quot;X-Amzn-Trace-Id&quot;: &quot;Root=1-63747872-2fbc85f81c6dde7e5b2091c4&quot;
   }, 
   &quot;json&quot;: null, 
   &quot;origin&quot;: &quot;47.242.15.156&quot;, 
   &quot;url&quot;: &quot;https://httpbin.org/post&quot;
 }

The outstanding difference is that curl sends &quot;Content-Length&quot;: &quot;197&quot; while the go app sends &quot;Transfer-Encoding&quot;: &quot;chunked&quot;.

Let's try to modify the go app to send the Content-Length header:

package main

import (
	&quot;bytes&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;log&quot;
	&quot;mime/multipart&quot;
	&quot;net/http&quot;
	&quot;strings&quot;
)

func main() {
	source := strings.NewReader(strings.Repeat(&quot;a&quot;, 1&lt;&lt;21))

	buf := new(bytes.Buffer)
	multipart := multipart.NewWriter(buf)

	part, err := multipart.CreateFormFile(&quot;file&quot;, &quot;test.txt&quot;)
	if err != nil {
		log.Fatal(err)
	}

	if _, err := io.Copy(part, source); err != nil {
		log.Fatal(err)
	}
	multipart.Close()

	req, err := http.NewRequest(http.MethodPost, &quot;https://api.anonfiles.com/upload&quot;, buf)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Add(&quot;Content-Type&quot;, multipart.FormDataContentType())

	// The following line is not required because the http client will set it
	// because the request body is a bytes.Buffer.
	// req.ContentLength = int64(buf.Len())

	client := http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(body))
}

It works!

The disadvantage is that it has to copy the request body into the memory first. It seems to me that this is unavoidable because it needs to know the size of the request body.

huangapple
  • 本文由 发表于 2022年11月16日 11:46:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/74454887.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定