package integration import ( "context" "encoding/json" "errors" "fmt" "log/slog" "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache" cache2 "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache" "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database" dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors" ) type Client interface { HandleVacancyResponse(request HandleVacancy) error } type ( CompanySecrets any client struct { cache cache2.Client db database.Client logger *slog.Logger secrets map[string]CompanySecrets } Config struct { Cache cache2.Client Db database.Client Logger *slog.Logger Secrets map[string]CompanySecrets } ) func New(c Config) (Client, error) { return &client{ cache: c.Cache, db: c.Db, logger: c.Logger, secrets: c.Secrets, }, nil } // TODO: need to think about possible errors // e.g. cache is down, connection lost etc. func (c *client) HandleVacancyResponse(request HandleVacancy) error { const handlerName = "integration.HandleVacancyResponse" companyId, vacancyExtraFields, err := c.getVacancyIntegrationInfo(context.Background(), request.VacancyId) if err != nil { return err } if vacancyExtraFields == nil { c.logger.Debug("vacancy extra fields are empty", slog.String("vacancy_id", request.VacancyId), slog.String("handler", handlerName), ) return nil } companyMetadata, companyExtraFieldsTemplate, err := c.getCompanyIntegrationInfo(context.Background(), companyId) if err != nil { return err } if companyMetadata == nil { c.logger.Debug("company metadata is empty", slog.String("company_id", companyId), slog.String("handler", handlerName), ) return nil } if companyExtraFieldsTemplate == nil { c.logger.Debug("company extra fields template are empty", slog.String("company_id", companyId), slog.String("handler", handlerName), ) return nil } companyName, ok := (*companyMetadata)[companyNameMetadataKey] if !ok { return fmt.Errorf("company name not found in metadata") } switch companyName { case VkusvillCompanyName: return c.handleVkusvillIntegration( context.Background(), request, vacancyExtraFields, companyMetadata, companyExtraFieldsTemplate, ) default: return ErrUnknownIntegration } } //nolint:gocognit // not that hard func (c *client) getVacancyIntegrationInfo( ctx context.Context, vacancyId string, ) (string, *VacancyExtraFields, error) { const handlerName = "integration.getVacancyIntegrationInfo" var ( companyId string vacancyExtraFields VacancyExtraFields gotCached = true ) // get company id and vacancy filled template cachedIntegrationInfo, err := c.cache.Get(context.Background(), vacancyId, cache.IntegrationVacancyValueType) if err != nil && !errors.Is(err, cache.ErrKeyNotFound) { if err := tryRepeat(context.Background(), func() error { cachedIntegrationInfo, err = c.cache.Get(context.Background(), vacancyId, cache.IntegrationCompanyValueType) return err }, defaultRetryAmount); err != nil { // TODO: maybe we should process further and try DB instead // Need to handle the error deeper // e.g. if cache is down we can try to get info from DB // on the other hand, if there is any other fatal error, we should return it return "", nil, fmt.Errorf("error getting vacancy integration info from cache: %w", err) } } if errors.Is(err, cache.ErrKeyNotFound) { // try get data from DB dbCompanyId, dbFields, err := c.db.GetVacancyIntegrationInfoById(context.Background(), vacancyId) if err != nil { if errors.Is(err, dberrors.ErrNotFound) { return "", nil, ErrVacancyNotFound } ctx, cancel := context.WithTimeout(context.Background(), defaultRetryTimeout) defer cancel() if err := tryRepeat(ctx, func() error { dbCompanyId, dbFields, err = c.db.GetVacancyIntegrationInfoById(context.Background(), vacancyId) return err }, 0); err != nil { return "", nil, fmt.Errorf("error getting vacancy integration info from DB: %w", err) } } companyId = dbCompanyId vacancyExtraFields = *new(VacancyExtraFields).From(dbFields) gotCached = false // store obtained info in cache fullMetadata := VacancyIntegrationInfo{ CompanyId: companyId, ExtraFields: vacancyExtraFields, } jsonMetadata, err := json.Marshal(fullMetadata) if err != nil { c.logger.Error("error marshalling vacancy integration info", slog.String("error", err.Error()), slog.String("vacancy_id", vacancyId), slog.String("handler", handlerName)) } else { if err := tryRepeat(context.Background(), func() error { return c.cache.Set(context.Background(), vacancyId, cache.IntegrationVacancyValueType, string(jsonMetadata), 0) }, defaultRetryAmount); err != nil { c.logger.Error("error saving vacancy integration info to cache", slog.String("error", err.Error()), slog.String("vacancy_id", vacancyId), slog.String("handler", handlerName)) } } } if gotCached { var fullMetadata VacancyIntegrationInfo if err := json.Unmarshal([]byte(cachedIntegrationInfo), &fullMetadata); err != nil { return "", nil, fmt.Errorf("error unmarshalling cached metadata: %w", err) } companyId = fullMetadata.CompanyId vacancyExtraFields = fullMetadata.ExtraFields } return companyId, &vacancyExtraFields, nil } //nolint:gocognit // not that hard func (c *client) getCompanyIntegrationInfo( ctx context.Context, companyId string, ) (*CompanyMetadata, *CompanyExtraFieldsTemplate, error) { const handlerName = "integration.getCompanyIntegrationInfo" var ( companyMetadata CompanyMetadata companyFieldsTemplate CompanyExtraFieldsTemplate gotCached = true ) // get company metadata and template cachedIntegrationInfo, err := c.cache.Get(context.Background(), companyId, cache.IntegrationCompanyValueType) if err != nil && !errors.Is(err, cache.ErrKeyNotFound) { if err := tryRepeat(context.Background(), func() error { cachedIntegrationInfo, err = c.cache.Get(context.Background(), companyId, cache.IntegrationCompanyValueType) return err }, defaultRetryAmount); err != nil { // TODO: maybe we should process further and try DB instead // Need to handle the error deeper // e.g. if cache is down we can try to get info from DB // on the other hand, if there is any other fatal error, we should return it return nil, nil, fmt.Errorf("error getting company metadata from cache: %w", err) } } if errors.Is(err, cache.ErrKeyNotFound) { // try get data from DB dbMetadata, dbFieldsTemplate, err := c.db.GetCompanyMetadataById(context.Background(), companyId) if err != nil { if errors.Is(err, dberrors.ErrNotFound) { return nil, nil, ErrCompanyNotFound } // TODO: think about other retry policies ctx, cancel := context.WithTimeout(context.Background(), defaultRetryTimeout) defer cancel() if err := tryRepeat(ctx, func() error { dbMetadata, dbFieldsTemplate, err = c.db.GetCompanyMetadataById(context.Background(), companyId) return err }, 0); err != nil { return nil, nil, fmt.Errorf("error getting company integration info from DB: %w", err) } } companyMetadata = *new(CompanyMetadata).From(dbMetadata) companyFieldsTemplate = *new(CompanyExtraFieldsTemplate).From(dbFieldsTemplate) gotCached = false // store obtained info in cache fullMetadata := CompanyIntegrationInfo{ Metadata: companyMetadata, ExtraFieldsTemplate: companyFieldsTemplate, } jsonMetadata, err := json.Marshal(fullMetadata) if err != nil { c.logger.Error("error marshalling company integration info", slog.String("error", err.Error()), slog.String("company_id", companyId), slog.String("handler", handlerName)) } else { if err := tryRepeat(context.Background(), func() error { return c.cache.Set(context.Background(), companyId, cache.IntegrationCompanyValueType, string(jsonMetadata), 0) }, defaultRetryAmount); err != nil { c.logger.Error("error saving company integration info to cache", slog.String("error", err.Error()), slog.String("company_id", companyId), slog.String("handler", handlerName)) } } } if gotCached { var fullMetadata CompanyIntegrationInfo if err := json.Unmarshal([]byte(cachedIntegrationInfo), &fullMetadata); err != nil { return nil, nil, fmt.Errorf("error unmarshalling cached metadata: %w", err) } companyMetadata = fullMetadata.Metadata companyFieldsTemplate = fullMetadata.ExtraFieldsTemplate } return &companyMetadata, &companyFieldsTemplate, nil } // tryRepeat tries to execute function N times. // If number of attempts is 0, the function will be executed until context is canceled func tryRepeat(ctx context.Context, fn func() error, attempts uint) error { ch := make(chan error, 1) for iter := uint(0); iter < attempts || attempts == 0; iter++ { go func() { ch <- fn() }() select { case err := <-ch: if err == nil { return nil } if attempts != 0 && iter == attempts-1 { return err } case <-ctx.Done(): return ctx.Err() } } return nil }