Анализ и очистка неактуальных данных testrail

Пришло время обновления тестрейла. На момент принятия решения имеем версию 6.5.7.1000 доступная 7.0.2.1016. В инстансе 177 проектов. Размер базы у него на момент принятия решения 137Gb. В тестрейле есть такая особенность что добавление новых кастомных полей вызывают альтер который лочит базу на изменеия и ничего нельзя сохранить, приложение становится доступно по сути только на чтение и в прошлом году такой альтер на добавление нового поля выполнялся часов 8, а две недели назад такие альтеры уже просто перестали до конца выполняться. Новые поля не добавлялись. При обновлении тоже есть альтеры и была вероятность сломаться где то в середине. Даунгрейд выполнять не хотелось бы. С проблемой долгого добавления полей и потенциальными проблемами с незавершением альтеров при обновлении написал в поддержку, и ответ был следующий:

In general, if you're experiencing a delay when adding, editing, or removing test cases (or similar, such as fields) to TestRail, this would most often indicate that you have a large amount of open test runs in your instance.

With this in mind, we would strongly recommend closing any open test runs that are actually complete or no longer in use, which should improve performance and loading time for these actions. We typically recommend closing these as you're finishing them to avoid them piling up and causing issues.

Currently, there is not an option to close existing test runs in bulk, however, you can use the API to find older test runs using get_runs with a created_before filter, then feed those run IDs into the repeating close_run call.

If you are using TestRail with heavy automation, we would also recommend looking into purging older automation test plans and runs. When test plans or runs are closed, this will make the database even larger (and potentially slower) as TestRail needs to copy cases internally so that closed plans/runs are immutable to case changes.

Many teams with database performance issues will find it is often due to bloat caused by automated test runs. In these cases, we would recommend purging old test plans and runs that exceed a certain age (6 months, 1 year, etc.). Similar to the above, you can automate this with the API (get_plans/get_runs with a creation date filter + delete_plan/run).

Databases inevitably get slower the more results, tests, and runs are added, and we recommend purging old automated test plans and runs to keep the database at a manageable size. If you need to retain the data for later auditing, you can export test runs via XML/CSV/Excel prior to purging them and store them separately for your records.

We also have some information on optimization, which can be found here:

https://www.gurock.com/testrail/docs/admin/server/optimizing

I recommend working with a database administrator on the above.

Решено было удалить только раны старше года это данные где-то за 4 года с момента установки до текущего момента, для работы с rest api был выбран golang и библиотека github.com/educlos/testrail

Для оценки количества удаляемых кейсов был написан такой код:

package main

import (
"fmt"
"github.com/educlos/testrail"
)

func main() {

    //username := os.Getenv("")
    //password := os.Getenv("")

    client := testrail.NewClient("https://testrail.com", "username", "password", false)
    //t := true
    type resultsRun struct{
        run int
        project int
        countInRun int
    }
    resultsInRun := []resultsRun{}

    type casesProject struct{
        project int
        countInProject int
    }
    //casesInProject := []casesProject{}
    //filterPlans := testrail.RequestFilterForPlan{
    //  //CreatedBefore: "1509397140",
    //  CreatedBefore: "1604091540", //Fri Oct 30 2020 20:59:00 GMT+0000
    //  //CreatedBefore: "1637751890", //Wed Nov 24 2021 11:04:50 GMT+0000
    //  //IsCompleted:&t,
    //}
    filterRuns := testrail.RequestFilterForRun{
        //CreatedBefore: "1540933140",
        CreatedBefore: "1604091540", //Fri Oct 30 2020 20:59:00 GMT+0000
        //CreatedBefore: "1637751890", //Wed Nov 24 2021 11:04:50 GMT+0000
        //IsCompleted:&t,
    }
    filterForRunResults := testrail.RequestFilterForRunResults{
        //CreatedBefore: "1540933140",
        CreatedBefore: "1604091540", //Fri Oct 30 2020 20:59:00 GMT+0000
        //CreatedBefore: "1637751890", //Wed Nov 24 2021 11:04:50 GMT+0000
        //IsCompleted:&t,
    }

    projects, err := client.GetProjects(false)
    fmt.Println(err)
    //for _, tp := range projects {
    //  fmt.Println(tp.ID)
    //}
    //return

    //allPlans := 0
    allRuns := 0
    allResults := 0

    for _, project := range projects{
        //GetSuites
        /*suites, suitesErr := client.GetSuites(project.ID)
        if (suitesErr!=nil) {
            fmt.Println(suitesErr)
        }
        for _, suit := range suites{
            cases, casesErr := client.GetCases(project.ID, suit.ID)
            if (casesErr!=nil) {
                fmt.Println(casesErr)
            }
            var curCountCaseInProjectBySuit = casesProject{
                project:project.ID,
                countInProject:len(cases),
            }
            fmt.Println(curCountCaseInProjectBySuit)
            casesInProject = append(casesInProject, curCountCaseInProjectBySuit)

        }*/

        //fmt.Printf("Suits:%d, Project:%d\n", len(suites), project.ID)

        //plans, plansErr := client.GetPlans(project.ID, filterPlans)
        //allPlans += len(plans)
        //for _, plan := range plans{
        //  //fmt.Println(plan.ID)
        //  if (plansErr!=nil){
        //      fmt.Println(plansErr)
        //  }
        //  //fmt.Printf("Current plan: %d", plan)
        //}
        runs, runsErr := client.GetRuns(project.ID, filterRuns)
        if (runsErr!=nil) {
            fmt.Println(runsErr)
        }
        allRuns += len(runs)
        for _, run := range runs{
            //fmt.Println(run.ID)
            results, resultErr := client.GetResultsForRun(run.ID, filterForRunResults)
            if (resultErr!=nil){
                fmt.Println(resultErr)
            }
            var curResRun = resultsRun{
                run:run.ID,
                project:run.ProjectID,
                countInRun:len(results),
            }
            resultsInRun = append(resultsInRun, curResRun)
            //fmt.Println(curResRun)
            allResults += len(results)

            fmt.Println(run.ID)
        }
    }
    //countCasesInProjects := map[int]int{}
    //for _, groupedByCP := range casesInProject {
    //////  //fmt.Println(groupedByCP.project)
    //////  //fmt.Println(groupedByCP.countInRun)
    //  countCasesInProjects[groupedByCP.project] += groupedByCP.countInProject
    //}
    //fmt.Println(countCasesInProjects)

    countResultsInProjects := map[int]int{}
    for _, v2 := range resultsInRun {
    //  //fmt.Println(v2.project)
    //  //fmt.Println(v2.countInRun)
        countResultsInProjects[v2.project] += v2.countInRun
    }
    fmt.Println(countResultsInProjects)
}

Получилось 323844 рана к удалению.

Для удаления был написан следующий код:

package main

import (
    "fmt"
    "github.com/educlos/testrail"
    "os"
    "log"
    "sync"
)
func main() {
    //username := os.Getenv("...")
    //password := os.Getenv("...")
    logFile, logErr := os.OpenFile("runsDeleted.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if logErr != nil {
        log.Fatal(logErr)
    }
    defer logFile.Close()
    log.SetOutput(logFile)

    client := testrail.NewClient("https://testrail.com", "username", "password", false)
    filterRuns := testrail.RequestFilterForRun{
        CreatedBefore: "1604091540", //Fri Oct 30 2020 20:59:00 GMT+0000
    }

    projects, err := client.GetProjects(false)
    if err!=nil {
        fmt.Println(err)
    }

    for _, project := range projects{
        runs, runsErr := client.GetRuns(project.ID, filterRuns)
        if runsErr!=nil {
            fmt.Println(runsErr)
        }
        log.Println("lenRuns", len(runs))

        //В batchSize потоков
        batchSize := 4
        batchNum := len(runs) / batchSize
        for i := 0; i < batchNum; i++ {
            fmt.Println(runs[i*batchSize:(i+1)*batchSize])
            quadRunDel(logFile, runs[i*batchSize:(i+1)*batchSize], project, client)
        }
        remainJobsNum := len(runs) % batchSize
        if remainJobsNum != 0 {
            fmt.Println(runs[len(runs)-remainJobsNum:])
            quadRunDel(logFile, runs[len(runs)-remainJobsNum:], project, client)
        }
        //Однопоточно
        //for _, run := range runs{
        //  runDel(run, project, client)
        //}
    }
}
func runDel(run testrail.Run, project testrail.Project, client *testrail.Client) {
    log.Printf("Project:%d run:%d created:%d\\n", project.ID, run.ID, run.CreatedOn)
    fmt.Printf("Project:%d run:%d created:%d\\n", project.ID, run.ID, run.CreatedOn)
    client.DeleteRun(run.ID)
    log.Println("deleted")
    fmt.Println("deleted")
}
func quadRunDel(logFile *os.File,  runs []testrail.Run, project testrail.Project, client *testrail.Client) {
    wg := sync.WaitGroup{}
    wg.Add(len(runs))

    for _, run := range runs {
        go func(run testrail.Run) {
            defer wg.Done()

            // do some job here
            _, err := fmt.Fprintln(logFile, run.ID)
            log.Printf("Project:%d run:%d created:%d\\n", project.ID, run.ID, run.CreatedOn)
            fmt.Printf("Project:%d run:%d created:%d\\n", project.ID, run.ID, run.CreatedOn)
            client.DeleteRun(run.ID)
            log.Println("deleted")
            fmt.Println("deleted")
            if err != nil {
                log.Fatalf("error during printing goroutine ouput to file")
            }
        }(run)
    }
    wg.Wait()
    _, err := fmt.Fprintln(logFile, "######    batch processed     #####")
    if err != nil {
        log.Fatalf("error during printing goroutine ouput to file")
    }
}

После запуска и отработки такой джобы (она работала около трех дней) выяснилось что объем базы как был 137Gb так и остался. Самая жирная табличка как ни странно оказалась cases - 108Gb select count(id) from cases; +-----------+ | count(id) | +-----------+ | 38259244 | +-----------+ 1 row in set (10.01 sec) Решить эту проблему помогли бывалые админы путем запуска optimize на всех таблицах. Это сократило объем базы до 29Gb. Далее выяснилось что по rest api у учетки из под которой вополнялось удаление есть доступ не ко всем проектам. Методов для добавления себя в админы в закрытые проекты не нашел, поэтому пришлось идти в эксель: результат выполнения метода GetProjects с печатью ID пришлось свэпэрить с результатом отработки js скрипта в консоли браузера

$("tr.hoverSensitive").find('td.action a').each(function(){ console.log($(this).attr("href"))})

на странице /index.php?/admin/projects/overview

Добавил себя ручками в 51 выявленный таким обарзом проект и повторно запустил код из первого листинга(он нашел 81380 рана к удалению) а потом из второго листинга.

Потом выполнил код из первого и второго листинга для completed проектов со значением client.GetProjects(true)

Снова выполнили optimize на всех таблицах и объем базы стал ... select count(id) from cases; ...