Parallelise Bulk Import. (#6267)
* Parallelise Bulk Import. * Set worker count through command line flag.
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
36a15925dc
Коммит
302ec17bee
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
l4g "github.com/alecthomas/log4go"
|
l4g "github.com/alecthomas/log4go"
|
||||||
@@ -95,15 +96,40 @@ type PostImportData struct {
|
|||||||
CreateAt *int64 `json:"create_at"`
|
CreateAt *int64 `json:"create_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LineImportWorkerData struct {
|
||||||
|
LineImportData
|
||||||
|
LineNumber int
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineImportWorkerError struct {
|
||||||
|
Error *model.AppError
|
||||||
|
LineNumber int
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// -- Bulk Import Functions --
|
// -- Bulk Import Functions --
|
||||||
// These functions import data directly into the database. Security and permission checks are bypassed but validity is
|
// These functions import data directly into the database. Security and permission checks are bypassed but validity is
|
||||||
// still enforced.
|
// still enforced.
|
||||||
//
|
//
|
||||||
|
|
||||||
func BulkImport(fileReader io.Reader, dryRun bool) (*model.AppError, int) {
|
func bulkImportWorker(dryRun bool, wg *sync.WaitGroup, lines <-chan LineImportWorkerData, errors chan<- LineImportWorkerError) {
|
||||||
|
for line := range lines {
|
||||||
|
if err := ImportLine(line.LineImportData, dryRun); err != nil {
|
||||||
|
errors <- LineImportWorkerError{err, line.LineNumber}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BulkImport(fileReader io.Reader, dryRun bool, workers int) (*model.AppError, int) {
|
||||||
scanner := bufio.NewScanner(fileReader)
|
scanner := bufio.NewScanner(fileReader)
|
||||||
lineNumber := 0
|
lineNumber := 0
|
||||||
|
|
||||||
|
errorsChan := make(chan LineImportWorkerError, (2*workers)+1) // size chosen to ensure it never gets filled up completely.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var linesChan chan LineImportWorkerData
|
||||||
|
lastLineType := ""
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
decoder := json.NewDecoder(strings.NewReader(scanner.Text()))
|
decoder := json.NewDecoder(strings.NewReader(scanner.Text()))
|
||||||
lineNumber++
|
lineNumber++
|
||||||
@@ -121,12 +147,50 @@ func BulkImport(fileReader io.Reader, dryRun bool) (*model.AppError, int) {
|
|||||||
if importDataFileVersion != 1 {
|
if importDataFileVersion != 1 {
|
||||||
return model.NewAppError("BulkImport", "app.import.bulk_import.unsupported_version.error", nil, "", http.StatusBadRequest), lineNumber
|
return model.NewAppError("BulkImport", "app.import.bulk_import.unsupported_version.error", nil, "", http.StatusBadRequest), lineNumber
|
||||||
}
|
}
|
||||||
} else if err := ImportLine(line, dryRun); err != nil {
|
} else {
|
||||||
return err, lineNumber
|
if line.Type != lastLineType {
|
||||||
|
if lastLineType != "" {
|
||||||
|
// Changing type. Clear out the worker queue before continuing.
|
||||||
|
close(linesChan)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Check no errors occurred while waiting for the queue to empty.
|
||||||
|
if len(errorsChan) != 0 {
|
||||||
|
err := <-errorsChan
|
||||||
|
return err.Error, err.LineNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the workers and channel for this type.
|
||||||
|
lastLineType = line.Type
|
||||||
|
linesChan = make(chan LineImportWorkerData, workers)
|
||||||
|
for i := 0; i < workers; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go bulkImportWorker(dryRun, &wg, linesChan, errorsChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case linesChan <- LineImportWorkerData{line, lineNumber}:
|
||||||
|
case err := <-errorsChan:
|
||||||
|
close(linesChan)
|
||||||
|
wg.Wait()
|
||||||
|
return err.Error, err.LineNumber
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No more lines. Clear out the worker queue before continuing.
|
||||||
|
close(linesChan)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Check no errors occurred while waiting for the queue to empty.
|
||||||
|
if len(errorsChan) != 0 {
|
||||||
|
err := <-errorsChan
|
||||||
|
return err.Error, err.LineNumber
|
||||||
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return model.NewLocAppError("BulkImport", "app.import.bulk_import.file_scan.error", nil, err.Error()), 0
|
return model.NewLocAppError("BulkImport", "app.import.bulk_import.file_scan.error", nil, err.Error()), 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1747,13 +1747,13 @@ func TestImportBulkImport(t *testing.T) {
|
|||||||
{"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}
|
{"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}
|
||||||
{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012}}`
|
{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012}}`
|
||||||
|
|
||||||
if err, line := BulkImport(strings.NewReader(data1), false); err != nil || line != 0 {
|
if err, line := BulkImport(strings.NewReader(data1), false, 2); err != nil || line != 0 {
|
||||||
t.Fatalf("BulkImport should have succeeded: %v, %v", err.Error(), line)
|
t.Fatalf("BulkImport should have succeeded: %v, %v", err.Error(), line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run bulk import using a string that contains a line with invalid json.
|
// Run bulk import using a string that contains a line with invalid json.
|
||||||
data2 := `{"type": "version", "version": 1`
|
data2 := `{"type": "version", "version": 1`
|
||||||
if err, line := BulkImport(strings.NewReader(data2), false); err == nil || line != 1 {
|
if err, line := BulkImport(strings.NewReader(data2), false, 2); err == nil || line != 1 {
|
||||||
t.Fatalf("Should have failed due to invalid JSON on line 1.")
|
t.Fatalf("Should have failed due to invalid JSON on line 1.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1762,7 +1762,7 @@ func TestImportBulkImport(t *testing.T) {
|
|||||||
{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
|
{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
|
||||||
{"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}}
|
{"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}}
|
||||||
{"type": "user", "user": {"username": "bwshaim6qnc2ne7oqkd5b2s2rq", "email": "bwshaim6qnc2ne7oqkd5b2s2rq@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}`
|
{"type": "user", "user": {"username": "bwshaim6qnc2ne7oqkd5b2s2rq", "email": "bwshaim6qnc2ne7oqkd5b2s2rq@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}`
|
||||||
if err, line := BulkImport(strings.NewReader(data3), false); err == nil || line != 1 {
|
if err, line := BulkImport(strings.NewReader(data3), false, 2); err == nil || line != 1 {
|
||||||
t.Fatalf("Should have failed due to missing version line on line 1.")
|
t.Fatalf("Should have failed due to missing version line on line 1.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ var bulkImportCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
bulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.")
|
bulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.")
|
||||||
bulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.")
|
bulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.")
|
||||||
|
bulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.")
|
||||||
|
|
||||||
importCmd.AddCommand(
|
importCmd.AddCommand(
|
||||||
bulkImportCmd,
|
bulkImportCmd,
|
||||||
@@ -87,6 +88,11 @@ func bulkImportCmdF(cmd *cobra.Command, args []string) error {
|
|||||||
return errors.New("Validate flag error")
|
return errors.New("Validate flag error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workers, err := cmd.Flags().GetInt("workers")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("Workers flag error")
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return errors.New("Incorrect number of arguments.")
|
return errors.New("Incorrect number of arguments.")
|
||||||
}
|
}
|
||||||
@@ -110,7 +116,7 @@ func bulkImportCmdF(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
CommandPrettyPrintln("")
|
CommandPrettyPrintln("")
|
||||||
|
|
||||||
if err, lineNumber := app.BulkImport(fileReader, !apply); err != nil {
|
if err, lineNumber := app.BulkImport(fileReader, !apply, workers); err != nil {
|
||||||
CommandPrettyPrintln(err.Error())
|
CommandPrettyPrintln(err.Error())
|
||||||
if lineNumber != 0 {
|
if lineNumber != 0 {
|
||||||
CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber))
|
CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber))
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user