gitea源码

cleanup.go 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package actions
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "time"
  9. actions_model "code.gitea.io/gitea/models/actions"
  10. "code.gitea.io/gitea/models/db"
  11. actions_module "code.gitea.io/gitea/modules/actions"
  12. "code.gitea.io/gitea/modules/container"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/setting"
  15. "code.gitea.io/gitea/modules/storage"
  16. "code.gitea.io/gitea/modules/timeutil"
  17. "xorm.io/builder"
  18. )
  19. // Cleanup removes expired actions logs, data, artifacts and used ephemeral runners
  20. func Cleanup(ctx context.Context) error {
  21. // clean up expired artifacts
  22. if err := CleanupArtifacts(ctx); err != nil {
  23. return fmt.Errorf("cleanup artifacts: %w", err)
  24. }
  25. // clean up old logs
  26. if err := CleanupExpiredLogs(ctx); err != nil {
  27. return fmt.Errorf("cleanup logs: %w", err)
  28. }
  29. // clean up old ephemeral runners
  30. if err := CleanupEphemeralRunners(ctx); err != nil {
  31. return fmt.Errorf("cleanup old ephemeral runners: %w", err)
  32. }
  33. return nil
  34. }
  35. // CleanupArtifacts removes expired add need-deleted artifacts and set records expired status
  36. func CleanupArtifacts(taskCtx context.Context) error {
  37. if err := cleanExpiredArtifacts(taskCtx); err != nil {
  38. return err
  39. }
  40. return cleanNeedDeleteArtifacts(taskCtx)
  41. }
  42. func cleanExpiredArtifacts(taskCtx context.Context) error {
  43. artifacts, err := actions_model.ListNeedExpiredArtifacts(taskCtx)
  44. if err != nil {
  45. return err
  46. }
  47. log.Info("Found %d expired artifacts", len(artifacts))
  48. for _, artifact := range artifacts {
  49. if err := actions_model.SetArtifactExpired(taskCtx, artifact.ID); err != nil {
  50. log.Error("Cannot set artifact %d expired: %v", artifact.ID, err)
  51. continue
  52. }
  53. if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
  54. log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
  55. // go on
  56. }
  57. log.Info("Artifact %d is deleted (due to expiration)", artifact.ID)
  58. }
  59. return nil
  60. }
  61. // deleteArtifactBatchSize is the batch size of deleting artifacts
  62. const deleteArtifactBatchSize = 100
  63. func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
  64. for {
  65. artifacts, err := actions_model.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize)
  66. if err != nil {
  67. return err
  68. }
  69. log.Info("Found %d artifacts pending deletion", len(artifacts))
  70. for _, artifact := range artifacts {
  71. if err := actions_model.SetArtifactDeleted(taskCtx, artifact.ID); err != nil {
  72. log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err)
  73. continue
  74. }
  75. if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
  76. log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
  77. // go on
  78. }
  79. log.Info("Artifact %d is deleted (due to pending deletion)", artifact.ID)
  80. }
  81. if len(artifacts) < deleteArtifactBatchSize {
  82. log.Debug("No more artifacts pending deletion")
  83. break
  84. }
  85. }
  86. return nil
  87. }
  88. const deleteLogBatchSize = 100
  89. func removeTaskLog(ctx context.Context, task *actions_model.ActionTask) {
  90. if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil {
  91. log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err)
  92. // do not return error here, go on
  93. }
  94. }
  95. // CleanupExpiredLogs removes logs which are older than the configured retention time
  96. func CleanupExpiredLogs(ctx context.Context) error {
  97. olderThan := timeutil.TimeStampNow().AddDuration(-time.Duration(setting.Actions.LogRetentionDays) * 24 * time.Hour)
  98. count := 0
  99. for {
  100. tasks, err := actions_model.FindOldTasksToExpire(ctx, olderThan, deleteLogBatchSize)
  101. if err != nil {
  102. return fmt.Errorf("find old tasks: %w", err)
  103. }
  104. for _, task := range tasks {
  105. removeTaskLog(ctx, task)
  106. task.LogIndexes = nil // clear log indexes since it's a heavy field
  107. task.LogExpired = true
  108. if err := actions_model.UpdateTask(ctx, task, "log_indexes", "log_expired"); err != nil {
  109. log.Error("Failed to update task %v: %v", task.ID, err)
  110. // do not return error here, continue to next task
  111. continue
  112. }
  113. count++
  114. log.Trace("Removed log %s of task %v", task.LogFilename, task.ID)
  115. }
  116. if len(tasks) < deleteLogBatchSize {
  117. break
  118. }
  119. }
  120. log.Info("Removed %d logs", count)
  121. return nil
  122. }
  123. // CleanupEphemeralRunners removes used ephemeral runners which are no longer able to process jobs
  124. func CleanupEphemeralRunners(ctx context.Context) error {
  125. subQuery := builder.Select("`action_runner`.id").
  126. From(builder.Select("*").From("`action_runner`"), "`action_runner`"). // mysql needs this redundant subquery
  127. Join("INNER", "`action_task`", "`action_task`.`runner_id` = `action_runner`.`id`").
  128. Where(builder.Eq{"`action_runner`.`ephemeral`": true}).
  129. And(builder.NotIn("`action_task`.`status`", actions_model.StatusWaiting, actions_model.StatusRunning, actions_model.StatusBlocked))
  130. b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
  131. res, err := db.GetEngine(ctx).Exec(b)
  132. if err != nil {
  133. return fmt.Errorf("find runners: %w", err)
  134. }
  135. affected, _ := res.RowsAffected()
  136. log.Info("Removed %d runners", affected)
  137. return nil
  138. }
  139. // CleanupEphemeralRunnersByPickedTaskOfRepo removes all ephemeral runners that have active/finished tasks on the given repository
  140. func CleanupEphemeralRunnersByPickedTaskOfRepo(ctx context.Context, repoID int64) error {
  141. subQuery := builder.Select("`action_runner`.id").
  142. From(builder.Select("*").From("`action_runner`"), "`action_runner`"). // mysql needs this redundant subquery
  143. Join("INNER", "`action_task`", "`action_task`.`runner_id` = `action_runner`.`id`").
  144. Where(builder.And(builder.Eq{"`action_runner`.`ephemeral`": true}, builder.Eq{"`action_task`.`repo_id`": repoID}))
  145. b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
  146. res, err := db.GetEngine(ctx).Exec(b)
  147. if err != nil {
  148. return fmt.Errorf("find runners: %w", err)
  149. }
  150. affected, _ := res.RowsAffected()
  151. log.Info("Removed %d runners", affected)
  152. return nil
  153. }
  154. // DeleteRun deletes workflow run, including all logs and artifacts.
  155. func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error {
  156. if !run.Status.IsDone() {
  157. return errors.New("run is not done")
  158. }
  159. repoID := run.RepoID
  160. jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
  161. if err != nil {
  162. return err
  163. }
  164. jobIDs := container.FilterSlice(jobs, func(j *actions_model.ActionRunJob) (int64, bool) {
  165. return j.ID, true
  166. })
  167. tasks := make(actions_model.TaskList, 0)
  168. if len(jobIDs) > 0 {
  169. if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).In("job_id", jobIDs).Find(&tasks); err != nil {
  170. return err
  171. }
  172. }
  173. artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
  174. RepoID: repoID,
  175. RunID: run.ID,
  176. })
  177. if err != nil {
  178. return err
  179. }
  180. var recordsToDelete []any
  181. recordsToDelete = append(recordsToDelete, &actions_model.ActionRun{
  182. RepoID: repoID,
  183. ID: run.ID,
  184. })
  185. recordsToDelete = append(recordsToDelete, &actions_model.ActionRunJob{
  186. RepoID: repoID,
  187. RunID: run.ID,
  188. })
  189. for _, tas := range tasks {
  190. recordsToDelete = append(recordsToDelete, &actions_model.ActionTask{
  191. RepoID: repoID,
  192. ID: tas.ID,
  193. })
  194. recordsToDelete = append(recordsToDelete, &actions_model.ActionTaskStep{
  195. RepoID: repoID,
  196. TaskID: tas.ID,
  197. })
  198. recordsToDelete = append(recordsToDelete, &actions_model.ActionTaskOutput{
  199. TaskID: tas.ID,
  200. })
  201. }
  202. recordsToDelete = append(recordsToDelete, &actions_model.ActionArtifact{
  203. RepoID: repoID,
  204. RunID: run.ID,
  205. })
  206. if err := db.WithTx(ctx, func(ctx context.Context) error {
  207. // TODO: Deleting task records could break current ephemeral runner implementation. This is a temporary workaround suggested by ChristopherHX.
  208. // Since you delete potentially the only task an ephemeral act_runner has ever run, please delete the affected runners first.
  209. // one of
  210. // call cleanup ephemeral runners first
  211. // delete affected ephemeral act_runners
  212. // I would make ephemeral runners fully delete directly before formally finishing the task
  213. //
  214. // See also: https://github.com/go-gitea/gitea/pull/34337#issuecomment-2862222788
  215. if err := CleanupEphemeralRunners(ctx); err != nil {
  216. return err
  217. }
  218. return db.DeleteBeans(ctx, recordsToDelete...)
  219. }); err != nil {
  220. return err
  221. }
  222. // Delete files on storage
  223. for _, tas := range tasks {
  224. removeTaskLog(ctx, tas)
  225. }
  226. for _, art := range artifacts {
  227. if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil {
  228. log.Error("remove artifact file %q: %v", art.StoragePath, err)
  229. }
  230. }
  231. return nil
  232. }