314 lines
9.0 KiB
Go
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
|
|
}
|