2026-06-23 09:54:27 +00:00
|
|
|
package usecase
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"haixun-backend/internal/library/clock"
|
|
|
|
|
"haixun-backend/internal/model/job/domain/entity"
|
|
|
|
|
"haixun-backend/internal/model/job/domain/enum"
|
|
|
|
|
domrepo "haixun-backend/internal/model/job/domain/repository"
|
|
|
|
|
domusecase "haixun-backend/internal/model/job/domain/usecase"
|
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type memoryTemplateRepo struct {
|
|
|
|
|
template *entity.Template
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *memoryTemplateRepo) EnsureIndexes(context.Context) error { return nil }
|
|
|
|
|
func (m *memoryTemplateRepo) List(context.Context) ([]*entity.Template, error) {
|
|
|
|
|
if m.template == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return []*entity.Template{m.template}, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryTemplateRepo) FindByType(context.Context, string) (*entity.Template, error) {
|
|
|
|
|
return m.template, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryTemplateRepo) Upsert(_ context.Context, template *entity.Template) (*entity.Template, error) {
|
|
|
|
|
m.template = template
|
|
|
|
|
return template, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type memoryRunRepo struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
run *entity.Run
|
|
|
|
|
succeeded map[string]*entity.Run
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newMemoryRunRepo(run *entity.Run) *memoryRunRepo {
|
|
|
|
|
return &memoryRunRepo{run: run, succeeded: map[string]*entity.Run{}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *memoryRunRepo) EnsureIndexes(context.Context) error { return nil }
|
|
|
|
|
func (m *memoryRunRepo) Create(ctx context.Context, run *entity.Run) (*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if run.ID.IsZero() {
|
|
|
|
|
run.ID = primitive.NewObjectID()
|
|
|
|
|
}
|
|
|
|
|
now := clock.NowUnixNano()
|
|
|
|
|
run.CreateAt = now
|
|
|
|
|
run.UpdateAt = now
|
|
|
|
|
m.run = run
|
|
|
|
|
return run, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) Update(ctx context.Context, run *entity.Run) (*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
run.UpdateAt = clock.NowUnixNano()
|
|
|
|
|
m.run = run
|
|
|
|
|
if run.Status == enum.RunStatusSucceeded && run.DedupeKey != "" {
|
|
|
|
|
m.succeeded[run.TemplateType+":"+run.DedupeKey] = run
|
|
|
|
|
}
|
|
|
|
|
return run, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) UpdateIfStatus(ctx context.Context, run *entity.Run, allowed []enum.RunStatus) (*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if m.run != nil {
|
|
|
|
|
ok := false
|
|
|
|
|
for _, status := range allowed {
|
|
|
|
|
if m.run.Status == status {
|
|
|
|
|
ok = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, errors.New("job state changed; update rejected")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
run.UpdateAt = clock.NowUnixNano()
|
|
|
|
|
m.run = run
|
|
|
|
|
if run.Status == enum.RunStatusSucceeded && run.DedupeKey != "" {
|
|
|
|
|
m.succeeded[run.TemplateType+":"+run.DedupeKey] = run
|
|
|
|
|
}
|
|
|
|
|
return run, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) UpdateIfLocked(ctx context.Context, run *entity.Run, workerID string, allowed []enum.RunStatus) (*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if m.run != nil {
|
|
|
|
|
if m.run.LockedBy != "" && m.run.LockedBy != workerID {
|
|
|
|
|
return nil, errors.New("job lock held by another worker")
|
|
|
|
|
}
|
|
|
|
|
ok := false
|
|
|
|
|
for _, status := range allowed {
|
|
|
|
|
if m.run.Status == status {
|
|
|
|
|
ok = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, errors.New("job state changed; update rejected")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
run.UpdateAt = clock.NowUnixNano()
|
|
|
|
|
m.run = run
|
|
|
|
|
if run.Status == enum.RunStatusSucceeded && run.DedupeKey != "" {
|
|
|
|
|
m.succeeded[run.TemplateType+":"+run.DedupeKey] = run
|
|
|
|
|
}
|
|
|
|
|
return run, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) FindByID(context.Context, string) (*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
return cloneRun(m.run), nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) List(context.Context, domrepo.RunListFilter, int64, int64) ([]*entity.Run, int64, error) {
|
|
|
|
|
return nil, 0, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) FindActiveByScope(context.Context, string, string, string) ([]*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if m.run == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
switch m.run.Status {
|
|
|
|
|
case enum.RunStatusPending, enum.RunStatusQueued, enum.RunStatusRunning, enum.RunStatusWaitingWorker, enum.RunStatusCancelRequested:
|
|
|
|
|
return []*entity.Run{m.run}, nil
|
|
|
|
|
default:
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) FindSucceededByDedupeKey(_ context.Context, templateType, dedupeKey string) (*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
return m.succeeded[templateType+":"+dedupeKey], nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) FindPendingDue(_ context.Context, now int64, _ int64) ([]*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if m.run == nil || m.run.Status != enum.RunStatusPending || m.run.ScheduledAt == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if *m.run.ScheduledAt <= now {
|
|
|
|
|
return []*entity.Run{m.run}, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) FindCancelRequestedBefore(_ context.Context, before int64, _ int64) ([]*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if m.run == nil || m.run.Status != enum.RunStatusCancelRequested || m.run.CancelRequestedAt == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if *m.run.CancelRequestedAt <= before {
|
|
|
|
|
return []*entity.Run{m.run}, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryRunRepo) FindRunningTimedOut(_ context.Context, now int64, _ int64) ([]*entity.Run, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if m.run == nil || m.run.LockedUntil == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if m.run.Status != enum.RunStatusRunning && m.run.Status != enum.RunStatusWaitingWorker {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if *m.run.LockedUntil <= now {
|
|
|
|
|
return []*entity.Run{m.run}, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type memoryScheduleRepo struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
schedules []*entity.Schedule
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *memoryScheduleRepo) EnsureIndexes(context.Context) error { return nil }
|
|
|
|
|
func (m *memoryScheduleRepo) Create(_ context.Context, schedule *entity.Schedule) (*entity.Schedule, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if schedule.ID.IsZero() {
|
|
|
|
|
schedule.ID = primitive.NewObjectID()
|
|
|
|
|
}
|
|
|
|
|
now := clock.NowUnixNano()
|
|
|
|
|
schedule.CreateAt = now
|
|
|
|
|
schedule.UpdateAt = now
|
|
|
|
|
m.schedules = append(m.schedules, schedule)
|
|
|
|
|
return schedule, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryScheduleRepo) Update(_ context.Context, schedule *entity.Schedule) (*entity.Schedule, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
schedule.UpdateAt = clock.NowUnixNano()
|
|
|
|
|
for i, item := range m.schedules {
|
|
|
|
|
if item.ID == schedule.ID {
|
|
|
|
|
m.schedules[i] = schedule
|
|
|
|
|
return schedule, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m.schedules = append(m.schedules, schedule)
|
|
|
|
|
return schedule, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryScheduleRepo) FindByID(_ context.Context, id string) (*entity.Schedule, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
objectID, _ := primitive.ObjectIDFromHex(id)
|
|
|
|
|
for _, item := range m.schedules {
|
|
|
|
|
if item.ID == objectID {
|
|
|
|
|
return item, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2026-06-25 09:34:28 +00:00
|
|
|
func (m *memoryScheduleRepo) Delete(_ context.Context, id string) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
objectID, err := primitive.ObjectIDFromHex(id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for i, item := range m.schedules {
|
|
|
|
|
if item.ID == objectID {
|
|
|
|
|
m.schedules = append(m.schedules[:i], m.schedules[i+1:]...)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-06-23 09:54:27 +00:00
|
|
|
func (m *memoryScheduleRepo) List(_ context.Context, _ domrepo.ScheduleListFilter, _, _ int64) ([]*entity.Schedule, int64, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
return m.schedules, int64(len(m.schedules)), nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryScheduleRepo) FindDue(_ context.Context, now int64, _ int64) ([]*entity.Schedule, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
var due []*entity.Schedule
|
|
|
|
|
for _, item := range m.schedules {
|
|
|
|
|
if item.Enabled && item.NextRunAt <= now {
|
|
|
|
|
due = append(due, item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return due, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type memoryEventRepo struct {
|
|
|
|
|
events []*entity.Event
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *memoryEventRepo) EnsureIndexes(context.Context) error { return nil }
|
|
|
|
|
func (m *memoryEventRepo) Append(_ context.Context, event *entity.Event) error {
|
|
|
|
|
m.events = append(m.events, event)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryEventRepo) ListByJobID(_ context.Context, jobID string, limit int64) ([]*entity.Event, error) {
|
|
|
|
|
if limit <= 0 {
|
|
|
|
|
limit = 50
|
|
|
|
|
}
|
|
|
|
|
var out []*entity.Event
|
|
|
|
|
for _, event := range m.events {
|
|
|
|
|
if event.JobID == jobID {
|
|
|
|
|
out = append(out, event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if int64(len(out)) > limit {
|
|
|
|
|
out = out[:limit]
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type memoryQueueRepo struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
queues map[string][]string
|
|
|
|
|
cancel map[string]string
|
|
|
|
|
locks map[string]string
|
|
|
|
|
dedupe map[string]string
|
|
|
|
|
schedulerLock string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newMemoryQueueRepo() *memoryQueueRepo {
|
|
|
|
|
return &memoryQueueRepo{
|
|
|
|
|
queues: map[string][]string{},
|
|
|
|
|
cancel: map[string]string{},
|
|
|
|
|
locks: map[string]string{},
|
|
|
|
|
dedupe: map[string]string{},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *memoryQueueRepo) Enqueue(_ context.Context, workerType, jobID string) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
m.queues[workerType] = append(m.queues[workerType], jobID)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) Dequeue(context.Context, string, int) (string, error) { return "", nil }
|
|
|
|
|
func (m *memoryQueueRepo) RemoveJob(_ context.Context, workerType, jobID string) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
items := m.queues[workerType]
|
|
|
|
|
filtered := make([]string, 0, len(items))
|
|
|
|
|
for _, item := range items {
|
|
|
|
|
if item != jobID {
|
|
|
|
|
filtered = append(filtered, item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m.queues[workerType] = filtered
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) SetCancelSignal(_ context.Context, jobID, reason string) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
m.cancel[jobID] = reason
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) GetCancelSignal(_ context.Context, jobID string) (bool, string, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
reason, ok := m.cancel[jobID]
|
|
|
|
|
return ok, reason, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) ClearCancelSignal(_ context.Context, jobID string) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
delete(m.cancel, jobID)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) TryLock(_ context.Context, jobID, workerID string, _ int) (bool, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if current, ok := m.locks[jobID]; ok && current != workerID {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
m.locks[jobID] = workerID
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) RefreshLock(_ context.Context, jobID, workerID string, _ int) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if current, ok := m.locks[jobID]; ok && current == workerID {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return errors.New("job lock is not held by worker")
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) ReleaseLock(_ context.Context, jobID, workerID string) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if current, ok := m.locks[jobID]; ok && current == workerID {
|
|
|
|
|
delete(m.locks, jobID)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) TryAcquireDedupe(_ context.Context, templateType, dedupeHash, jobID string, _ int) (bool, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
key := templateType + ":" + dedupeHash
|
|
|
|
|
if _, ok := m.dedupe[key]; ok {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
m.dedupe[key] = jobID
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) ReleaseDedupe(_ context.Context, templateType, dedupeHash string) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
delete(m.dedupe, templateType+":"+dedupeHash)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) TrySchedulerLock(_ context.Context, holder string, _ int) (bool, error) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if m.schedulerLock != "" && m.schedulerLock != holder {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
m.schedulerLock = holder
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
func (m *memoryQueueRepo) ReleaseSchedulerLock(_ context.Context, holder string) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if m.schedulerLock == holder {
|
|
|
|
|
m.schedulerLock = ""
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *memoryQueueRepo) queued(workerType string) []string {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
return append([]string(nil), m.queues[workerType]...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testUseCase(template *entity.Template, run *entity.Run) domusecase.UseCase {
|
|
|
|
|
return NewUseCase(
|
|
|
|
|
&memoryTemplateRepo{template: template},
|
|
|
|
|
newMemoryRunRepo(run),
|
|
|
|
|
&memoryScheduleRepo{},
|
|
|
|
|
&memoryEventRepo{},
|
|
|
|
|
newMemoryQueueRepo(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testUseCaseFull(template *entity.Template, runs *memoryRunRepo, schedules *memoryScheduleRepo, queue *memoryQueueRepo) domusecase.UseCase {
|
|
|
|
|
if schedules == nil {
|
|
|
|
|
schedules = &memoryScheduleRepo{}
|
|
|
|
|
}
|
|
|
|
|
if queue == nil {
|
|
|
|
|
queue = newMemoryQueueRepo()
|
|
|
|
|
}
|
|
|
|
|
return NewUseCase(
|
|
|
|
|
&memoryTemplateRepo{template: template},
|
|
|
|
|
runs,
|
|
|
|
|
schedules,
|
|
|
|
|
&memoryEventRepo{},
|
|
|
|
|
queue,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cloneRun(run *entity.Run) *entity.Run {
|
|
|
|
|
if run == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
out := *run
|
|
|
|
|
if run.Progress.Steps != nil {
|
|
|
|
|
out.Progress.Steps = append([]entity.StepProgress(nil), run.Progress.Steps...)
|
|
|
|
|
}
|
|
|
|
|
if run.Payload != nil {
|
|
|
|
|
out.Payload = map[string]any{}
|
|
|
|
|
for k, v := range run.Payload {
|
|
|
|
|
out.Payload[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if run.Result != nil {
|
|
|
|
|
out.Result = map[string]any{}
|
|
|
|
|
for k, v := range run.Result {
|
|
|
|
|
out.Result[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return &out
|
|
|
|
|
}
|