github.com/nvkalinin/business-calendar@v1.0.2-0.20220515154925-e7df8a3d0c34/cmd/server.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"net/http/cookiejar"
     8  	"os"
     9  	"os/signal"
    10  	"strconv"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/nvkalinin/business-calendar/calendar"
    15  	"github.com/nvkalinin/business-calendar/log"
    16  	"github.com/nvkalinin/business-calendar/rest"
    17  	"github.com/nvkalinin/business-calendar/source"
    18  	"github.com/nvkalinin/business-calendar/source/parser"
    19  	"github.com/nvkalinin/business-calendar/store"
    20  	"github.com/nvkalinin/business-calendar/store/engine"
    21  	"golang.org/x/net/publicsuffix"
    22  	"golang.org/x/sync/errgroup"
    23  )
    24  
    25  type EngineType string
    26  
    27  var (
    28  	EngineMemory EngineType = "memory"
    29  	EngineBolt   EngineType = "bolt"
    30  )
    31  
    32  type ParserType string
    33  
    34  var (
    35  	ParserNone       ParserType = "none"
    36  	ParserConsultant ParserType = "consultant"
    37  	ParserSuperJob   ParserType = "superjob"
    38  )
    39  
    40  type Server struct {
    41  	SyncAt      string   `long:"sync-at" env:"SYNC_AT" value-name:"hh:mm[:ss]" description:"В какое время синхронизировать производственный календарь со всеми источниками. Обновление происходит один раз в сутки. Если не указано, то автоматическое обновление отключено."`
    42  	SyncOnStart []string `long:"sync-on-start" env:"SYNC_ON_START" env-delim:"," value-name:"year" default:"current" default:"next" description:"За какие годы синхронизировать календарь при запуске программы. Можно указывать числа, 'current' — текущий год, 'next' — следующий год. 'none' — отключить синхронизацию при запуске."`
    43  
    44  	Web struct {
    45  		Listen      string `long:"listen" env:"LISTEN" value-name:"addr" default:"0.0.0.0:80" description:"Сетевой адрес для веб-сервера."`
    46  		AccessLog   bool   `long:"access-log" env:"ACCESS_LOG" description:"Логировать все HTTP-запросы."`
    47  		AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" description:"Пароль пользователя admin для вызова /api/admin/*."`
    48  
    49  		ReadTimeout       time.Duration `long:"read-timeout" env:"READ_TIMEOUT" value-name:"duration" default:"5s" description:"http.Server ReadTimeout"`
    50  		ReadHeaderTimeout time.Duration `long:"read-header-timeout" env:"READ_HEADER_TIMEOUT" value-name:"duration" default:"5s" description:"http.Server ReadHeaderTimeout"`
    51  		IdleTimeout       time.Duration `long:"idle-timeout" env:"IDLE_TIMEOUT" value-name:"duration" default:"30s" description:"http.Server IdleTimeout"`
    52  
    53  		// Запросы к /admin могут выполняться долго, поэтому WriteTimout должен быть достаточно большим.
    54  		WriteTimeout time.Duration `long:"write-timeout" env:"WRITE_TIMEOUT" value-name:"duration" default:"60s" description:"http.Server WriteTimeout"`
    55  
    56  		RateLimiter struct {
    57  			ReqLimit    int           `long:"reqs" env:"REQS" value-name:"num" default:"100" description:"Количество запросов с одного IP. Если 0 — rate limiter отключен."`
    58  			LimitWindow time.Duration `long:"window" env:"WINDOW" value-name:"duration" default:"1s" description:"Интервал времени, за который разврешено указанное кол-во запросов."`
    59  		} `group:"Rate Limiter" namespace:"ratelim" env-namespace:"RATE_LIM"`
    60  	} `group:"Web" namespace:"web" env-namespace:"WEB"`
    61  
    62  	Store struct {
    63  		Engine EngineType `long:"engine" env:"ENGINE" value-name:"type" choice:"memory" choice:"bolt" default:"bolt" description:"Тип хранилища для данных, собранных пармерами."`
    64  
    65  		Bolt struct {
    66  			File string `long:"file" env:"FILE" value-name:"path" default:"cal.bolt" description:"Путь к файлу БД."`
    67  		} `group:"Настройки хранилища bolt" namespace:"bolt" env-namespace:"BOLT"`
    68  	} `group:"Хранилище" namespace:"store" env-namespace:"STORE"`
    69  
    70  	Source struct {
    71  		Parser ParserType `long:"parser" env:"PARSER" value-name:"type" choice:"consultant" choice:"superjob" choice:"none" default:"consultant" description:"Внешний источник производственного календаря, который нужно парсить."`
    72  
    73  		Consultant struct {
    74  			Timeout   time.Duration `long:"timeout" env:"TIMEOUT" value-name:"duration" default:"30s" description:"Максимальное время выполнения запроса к сайту."`
    75  			UserAgent string        `long:"user-agent" env:"USER_AGENT" description:"Значение заголовка User-Agent во всех запросах к сайту."`
    76  		} `group:"Парсер consultant.ru" namespace:"consultant" env-namespace:"CONSULTANT"`
    77  
    78  		SuperJob struct {
    79  			Timeout   time.Duration `long:"timeout" env:"TIMEOUT" value-name:"duration" default:"30s" description:"Максимальное время выполнения запроса к сайту."`
    80  			UserAgent string        `long:"user-agent" env:"USER_AGENT" description:"Значение заголовка User-Agent во всех запросах к сайту."`
    81  		} `group:"Парсер superjob.ru" namespace:"superjob" env-namespace:"SUPERJOB"`
    82  
    83  		Override string `long:"override" env:"OVERRIDE" value-name:"file.yml" description:"Путь к файлу с локальными изменениями производственного календаря. Если задан, используется всегда, вне зависимости от выбранного парсера."`
    84  	} `group:"Источник данных" namespace:"source" env-namespace:"SOURCE"`
    85  }
    86  
    87  func (s *Server) Execute(args []string) error {
    88  	a, err := s.makeApp()
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	go func() {
    94  		sigChan := make(chan os.Signal, 1)
    95  		signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    96  		<-sigChan
    97  		a.shutdown()
    98  	}()
    99  
   100  	a.run()
   101  	a.wait()
   102  	return nil
   103  }
   104  
   105  type app struct {
   106  	srv             *rest.Server
   107  	proc            *calendar.Processor
   108  	autoSync        bool
   109  	syncYears       []int
   110  	syncYearsFinish chan struct{}
   111  	stopped         bool
   112  }
   113  
   114  func (s *Server) makeApp() (*app, error) {
   115  	log.Printf("[DEBUG] server opts: %+v", s)
   116  
   117  	a := &app{
   118  		syncYearsFinish: make(chan struct{}),
   119  	}
   120  
   121  	store, err := s.makeStore()
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	src, err := s.makeSources()
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	var syncAt time.Time
   132  	if s.SyncAt != "" {
   133  		syncAt, err = parseSyncAt(s.SyncAt)
   134  		if err != nil {
   135  			return nil, fmt.Errorf("sync at: %w", err)
   136  		}
   137  		a.autoSync = true
   138  	}
   139  
   140  	syncYears, err := parseYears(s.SyncOnStart)
   141  	if err != nil {
   142  		return nil, fmt.Errorf("sync on start: %w", err)
   143  	}
   144  	a.syncYears = syncYears
   145  
   146  	a.proc = calendar.NewProcessor(calendar.ProcOpts{
   147  		Src:      src,
   148  		Store:    calendar.Store(store),
   149  		UpdateAt: syncAt,
   150  	})
   151  
   152  	a.srv = &rest.Server{
   153  		Store:   store,
   154  		Updater: a.proc,
   155  		Opts: rest.Opts{
   156  			Listen:      s.Web.Listen,
   157  			LogRequests: s.Web.AccessLog,
   158  			AdminPasswd: s.Web.AdminPasswd,
   159  
   160  			ReadTimeout:       s.Web.ReadTimeout,
   161  			ReadHeaderTimeout: s.Web.ReadHeaderTimeout,
   162  			WriteTimeout:      s.Web.WriteTimeout,
   163  			IdleTimeout:       s.Web.IdleTimeout,
   164  
   165  			RateLimiter: s.Web.RateLimiter.ReqLimit > 0,
   166  			ReqLimit:    s.Web.RateLimiter.ReqLimit,
   167  			LimitWindow: s.Web.RateLimiter.LimitWindow,
   168  		},
   169  	}
   170  
   171  	return a, nil
   172  }
   173  
   174  type Store interface {
   175  	FindDay(y int, mon time.Month, d int) (*store.Day, bool)
   176  	FindMonth(y int, mon time.Month) (store.Days, bool)
   177  	FindYear(y int) (store.Months, bool)
   178  	PutYear(y int, data store.Months) error
   179  }
   180  
   181  func (s *Server) makeStore() (Store, error) {
   182  	switch s.Store.Engine {
   183  	case EngineMemory:
   184  		return engine.NewMemory(), nil
   185  	case EngineBolt:
   186  		return engine.NewBolt(s.Store.Bolt.File)
   187  	default:
   188  		return nil, fmt.Errorf("unknown store engine %s", s.Store.Engine)
   189  	}
   190  }
   191  
   192  func (s *Server) makeSources() ([]calendar.Source, error) {
   193  	src := make([]calendar.Source, 0, 3)
   194  	src = append(src, source.NewGeneric())
   195  
   196  	switch s.Source.Parser {
   197  	case ParserNone:
   198  	case ParserConsultant:
   199  		ua := s.Source.Consultant.UserAgent
   200  		if ua == "" {
   201  			ua = "Go-http-client"
   202  		}
   203  
   204  		p := &parser.Consultant{
   205  			Client: &http.Client{
   206  				Timeout: s.Source.Consultant.Timeout,
   207  			},
   208  			UserAgent: ua,
   209  		}
   210  
   211  		src = append(src, p)
   212  	case ParserSuperJob:
   213  		ua := s.Source.SuperJob.UserAgent
   214  		if ua == "" {
   215  			ua = "Go-http-client"
   216  		}
   217  
   218  		jar, err := cookiejar.New(&cookiejar.Options{
   219  			PublicSuffixList: publicsuffix.List,
   220  		})
   221  		if err != nil {
   222  			return nil, fmt.Errorf("cannot create cookie jar: %w", err)
   223  		}
   224  
   225  		p := &parser.SuperJob{
   226  			Client: &http.Client{
   227  				Timeout: s.Source.SuperJob.Timeout,
   228  				Jar:     jar,
   229  			},
   230  			UserAgent: ua,
   231  		}
   232  
   233  		src = append(src, p)
   234  	default:
   235  		return nil, fmt.Errorf("unknown parser %s", s.Source.Parser)
   236  	}
   237  
   238  	if s.Source.Override != "" {
   239  		src = append(src, &source.Override{
   240  			Path: s.Source.Override,
   241  		})
   242  	}
   243  
   244  	return src, nil
   245  }
   246  
   247  func parseSyncAt(val string) (time.Time, error) {
   248  	if t, err := time.Parse("15:04", val); err == nil {
   249  		return t, nil
   250  	}
   251  
   252  	t, err := time.Parse("15:04:05", val)
   253  	if err != nil {
   254  		return time.Time{}, fmt.Errorf("invalid time '%s', it must match pattern hh:mm[:ss]", val)
   255  	}
   256  	return t, nil
   257  }
   258  
   259  func parseYears(vals []string) ([]int, error) {
   260  	if len(vals) == 1 && vals[0] == "none" {
   261  		return nil, nil
   262  	}
   263  
   264  	years := make(map[int]bool, len(vals))
   265  	for _, val := range vals {
   266  		switch val {
   267  		case "current":
   268  			y := time.Now().Year()
   269  			years[y] = true
   270  		case "next":
   271  			y := time.Now().Year() + 1
   272  			years[y] = true
   273  		default:
   274  			y, err := strconv.Atoi(val)
   275  			if err != nil {
   276  				return nil, fmt.Errorf("invalid year '%s': %w", val, err)
   277  			}
   278  			if y < 0 {
   279  				return nil, fmt.Errorf("invalid year %d", y)
   280  			}
   281  			years[y] = true
   282  		}
   283  	}
   284  
   285  	ylist := make([]int, 0, len(years))
   286  	for y := range years {
   287  		ylist = append(ylist, y)
   288  	}
   289  
   290  	return ylist, nil
   291  }
   292  
   293  func (a *app) run() {
   294  	g, _ := errgroup.WithContext(context.Background())
   295  
   296  	if a.autoSync {
   297  		g.Go(func() error {
   298  			a.proc.RunUpdates()
   299  			return nil
   300  		})
   301  	}
   302  
   303  	g.Go(func() error {
   304  		syncOnRun(a.proc, a.syncYears, a.syncYearsFinish)
   305  		return nil
   306  	})
   307  
   308  	g.Go(func() error {
   309  		if err := a.srv.Run(); err != nil && err != http.ErrServerClosed {
   310  			log.Printf("[ERROR] startup: %v", err)
   311  			return err
   312  		}
   313  		return nil
   314  	})
   315  
   316  	if g.Wait() != nil {
   317  		a.shutdown()
   318  	}
   319  }
   320  
   321  func (a *app) shutdown() {
   322  	if a.stopped {
   323  		return
   324  	}
   325  
   326  	log.Printf("[INFO] shutting down...")
   327  
   328  	ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) //nolint:govet // Не нужен cancel, все равно завершение программы.
   329  	g, _ := errgroup.WithContext(ctx)
   330  
   331  	if a.autoSync {
   332  		g.Go(func() error {
   333  			return a.proc.Shutdown(ctx)
   334  		})
   335  	}
   336  	g.Go(func() error {
   337  		return a.srv.Shutdown(ctx)
   338  	})
   339  	g.Go(func() error {
   340  		select {
   341  		case <-ctx.Done():
   342  			return fmt.Errorf("sync on run: %w", ctx.Err())
   343  		case <-a.syncYearsFinish:
   344  			return nil
   345  		}
   346  	})
   347  
   348  	if err := g.Wait(); err != nil {
   349  		log.Printf("[ERROR] app shutdown: %v", err)
   350  	}
   351  	a.stopped = true
   352  }
   353  
   354  func (a *app) wait() {
   355  	for !a.stopped {
   356  		time.Sleep(10 * time.Millisecond)
   357  	}
   358  }
   359  
   360  func syncOnRun(proc *calendar.Processor, years []int, finished chan<- struct{}) {
   361  	for _, y := range years {
   362  		log.Printf("[INFO] sync on run: year %d...", y)
   363  		if err := proc.UpdateCalendar(y); err != nil {
   364  			log.Printf("[WARN] sync on run, year %d: %+v", y, err)
   365  		}
   366  	}
   367  	close(finished)
   368  }