Files
test_deploy/internal/integration/client.go
Alex Shevchuk d84487d238 1
2025-08-18 17:12:04 +03:00

314 lines
9.0 KiB
Go

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
}