Skip to content

Commit

Permalink
Separate locations into a new config file
Browse files Browse the repository at this point in the history
Make a copy of locations in ConfigService.GetLocations, because another thread could modify the slice while the caller is using it
Use the lock in GetLocations because copy is not atomic and another thread could update the slice while we are making a copy
Marshall the locations with 2 spaces instead of 4
  • Loading branch information
tedpearson committed Oct 2, 2023
1 parent c817e0d commit bbee883
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.idea
*.iml
forecastmetrics.yaml
locations.yaml
/ForecastMetrics
/forecastmetrics
.DS_Store
80 changes: 54 additions & 26 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"os"
"slices"
"sync"
Expand All @@ -25,7 +26,6 @@ type InfluxConfig struct {

// Config is the configuration for ForecastMetrics.
type Config struct {
Locations []Location
InfluxDB InfluxConfig `yaml:"influxdb"`
ForecastMeasurementName string `yaml:"forecast_measurement_name"`
AstronomyMeasurementName string `yaml:"astronomy_measurement_name"`
Expand All @@ -43,67 +43,95 @@ type Config struct {
}
}

// mustParseConfig parses the config from the given config file, or panics.
func mustParseConfig(configFile string) Config {
// ConfigService provides a way to update and get the latest list of locations that have regular
// forecasts exported to the database.
type ConfigService struct {
Config Config
locationsFile string
lock *sync.Mutex
locations []Location
}

// NewConfigService initializes a ConfigService by parsing the main config and the locations files.
// It panics if it can't read or parse the configs.
func NewConfigService(configFile, locationsFile string) *ConfigService {
// read config
file, err := os.ReadFile(configFile)
cf, err := os.ReadFile(configFile)
if err != nil {
panic(err)
panic(fmt.Sprintf("Error reading config file %s: %s", configFile, err))
}
var config Config
err = yaml.Unmarshal(file, &config)
err = yaml.Unmarshal(cf, &config)
if err != nil {
panic(err)
panic(fmt.Sprintf("Error loading config from %s: %s", configFile, err))
}
lf, err := os.ReadFile(locationsFile)
if err != nil {
panic(fmt.Sprintf("Error reading locations file %s: %s", locationsFile, err))
}
var locations []Location
err = yaml.Unmarshal(lf, &locations)
if err != nil {
panic(fmt.Sprintf("Error loading locations from %s: %s", locationsFile, err))
}
return &ConfigService{
Config: config,
locationsFile: locationsFile,
lock: &sync.Mutex{},
locations: locations,
}
return config
}

// ConfigService provides a way to update and get the latest list of locations that have regular
// forecasts exported to the database.
type ConfigService struct {
Config Config
ConfigFile string
lock *sync.Mutex
}

// HasLocation returns true if this Location is being regularly exported.
func (c *ConfigService) HasLocation(location Location) bool {
return slices.Contains(c.Config.Locations, location)
return slices.Contains(c.locations, location)
}

// GetLocations returns all actively exported locations.
// GetLocations returns a copy of all actively exported locations.
func (c *ConfigService) GetLocations() []Location {
return c.Config.Locations
c.lock.Lock()
defer c.lock.Unlock()
locsCopy := make([]Location, len(c.locations))
copy(locsCopy, c.locations)
return locsCopy
}

// AddLocation adds a new location to be regularly exported. It is saved to the config file.
func (c *ConfigService) AddLocation(location Location) {
c.lock.Lock()
defer c.lock.Unlock()
c.Config.Locations = append(c.Config.Locations, location)
c.locations = append(c.locations, location)
c.marshall()
}

// RemoveLocation removes a location from being regularly exported, and removes it from the config file.
func (c *ConfigService) RemoveLocation(location Location) {
c.lock.Lock()
defer c.lock.Unlock()
locs := c.Config.Locations
locs := c.locations
idx := slices.Index(locs, location)
if idx > -1 {
c.Config.Locations = append(locs[:idx], locs[idx+1:]...)
c.locations = append(locs[:idx], locs[idx+1:]...)
c.marshall()
}
}

// marshall writes the current configuration to the config file.
// It should only be called while holding the lock.
func (c *ConfigService) marshall() {
bytes, err := yaml.Marshal(c.Config)
f, err := os.OpenFile(c.locationsFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
panic(fmt.Sprintf("Error opening locations file %s: %s", c.locationsFile, err))
}
defer f.Close()
encoder := yaml.NewEncoder(f)
encoder.SetIndent(2)
err = encoder.Encode(c.locations)
if err != nil {
panic(err)
panic(fmt.Sprintf("Error saving locations to %s: %s", c.locationsFile, err))
}
err = os.WriteFile(c.ConfigFile, bytes, 0644)
err = encoder.Close()
if err != nil {
panic(err)
panic(fmt.Sprintf("Error saving locations to %s: %s", c.locationsFile, err))
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
locations:
- name: Washington Monument
latitude: 38.8895
longitude: -77.0352

influxdb:
host: http://localhost:8086
# for influx 1.8/VictoriaMetrics, use "user:password"
Expand Down
3 changes: 3 additions & 0 deletions locations.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- name: Washington Monument
latitude: 38.8895
longitude: -77.0352
13 changes: 5 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"os"
"slices"
"sync"

"github.com/gregjones/httpcache"
"github.com/gregjones/httpcache/diskcache"
Expand All @@ -24,19 +23,16 @@ var (
func main() {
// parse flags
configFile := flag.String("config", "forecastmetrics.yaml", "Config file")
locationsFile := flag.String("locations", "locations.yaml", "Locations file")
versionFlag := flag.Bool("v", false, "Show version and exit")
flag.Parse()
fmt.Printf("ForecastMetrics version %s built on %s with %s\n", version, buildDate, goVersion)
if *versionFlag {
os.Exit(0)
}
config := mustParseConfig(*configFile)
configService := NewConfigService(*configFile, *locationsFile)
config := configService.Config
locationService := LocationService{BingToken: config.BingToken}
configService := &ConfigService{
Config: config,
ConfigFile: *configFile,
lock: &sync.Mutex{},
}
forecasters := MakeForecasters(config.Sources.Enabled, config.HttpCacheDir, config.Sources.VisualCrossing.Key)
c := influxdb2.NewClient(config.InfluxDB.Host, config.InfluxDB.AuthToken)
writeApi := c.WriteAPIBlocking(config.InfluxDB.Org, config.InfluxDB.Bucket)
Expand Down Expand Up @@ -96,7 +92,8 @@ func MakeForecasters(enabled []string, cacheDir string, vcKey string) map[string

// todo
// deployment stuff
// increment version
// grafana dashboards
// make influx forwarded token and our required auth token allowed to be different
// update readme
// allow http server functionality to be turned off if desired, by not including a port to listen on or something
// also allow proxy to be turned off

0 comments on commit bbee883

Please sign in to comment.