wingedpig.com
Open in
urlscan Pro
2600:3c03::f03c:92ff:fe6e:ce0c
Public Scan
URL:
http://wingedpig.com/
Submission: On February 24 via api from US — Scanned from DE
Submission: On February 24 via api from US — Scanned from DE
Form analysis
0 forms found in the DOMText Content
wingedpig * About * Posts MARK FLETCHER'S BLOG MIGRATED TO HUGO By Mark Fletcher December 24, 2022 - 2 minutes read - 362 words Baobab Tree with ostriches in the distrance, Tarangire National Park, Tanzania There's a new sun arisin' (In your eyes) I can see a new horizon (Realize) That will keep me realizin' You're the biggest part of me Biggest Part Of Me, Ambrosia I have migrated this blog from Wordress to Hugo. I did so mainly because Wordpress wouldn’t let me do a few things. I wanted to add a rel=me link so that Mastodon would associate my @wingedpig@hachyderm.io account with this blog. And, I wanted to be able to customize the blog more than Wordpress would allow me to do. These are my rough notes on how I did the migration. First, I exported my Wordpress blog. Then I used the Blogger To Markdown tool to convert the exported Wordpress files to markdown files compatible with Hugo. I am currently hosting the blog using Linode’s block storage system. I followed the instructions in Deploy a Static Site using Hugo and Object Storage to set that up. I am using the Ananke theme, but I have customized it. The biggest change was making the home page display full blog posts, instead of summaries. I did that by creating a new layouts/index.html file, using the themes/ananke/layouts/index.html file as a template. Another important change I made was with the RSS file. Hugo defaults to having the RSS file at /index.xml, but my Wordpress blog’s RSS feed was at /feed. I didn’t want to lose my existing RSS subscribers. To change the RSS URL, I had to make two changes. In the config.toml file, I had to add the following: [mediaTypes] [mediaTypes.'application/rss'] suffixes = [] [outputFormats] [outputFormats.RSS] mediatype = "application/rss" baseName = "feed" I also had to copy the internal Hugo RSS template into the file layouts/index.rss. The final change I had to make was to set the blog post URL structure to match the old Wordpress blog structure, so that links to old blog posts would not be broken. To do that, I added the following to the config.toml file: [permalinks] posts = '/:year/:month/:day/:title/' -------------------------------------------------------------------------------- Groups.io is the best tool to get a bunch of people organized and sharing knowledge. Start a free trial group today. -------------------------------------------------------------------------------- JSON CONFIGURATION INCLUDES By Mark Fletcher December 13, 2022 - 2 minutes read - 298 words Ceiling of a building in the Citadel, Amman, Jordan But the drum-beat strains of the night remain In the rhythm of the newborn day You know sometime you're bound to leave her But for now you're going to stay In the year of the cat - Year Of The Cat, Al Stewart Everything needs configuration data. Some people use .toml files, others use .ini or .yaml. I don’t like any of those file formats and I also just prefer JSON for data in general. So at groups.io all our configuration data is in JSON formatted files. But I wanted the ability for config files to include other config files, which is not something that JSON normally supports, so I wrote a small utility function to support including JSON files in other JSON files. This function is used by every program that runs the groups.io backend. This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters // ReadConfig reads in a JSON-formatted configuration file, looking for any // include directives, and recursively reading those files as well. Include directives // are specified as a list keyed on the "Includes" key. func ReadConfig(filename string, configuration interface{}) error { // grab any path to the config file to add to the include names var prefix string slashindex := strings.LastIndex(filename, "/") if slashindex != -1 { prefix = filename[:slashindex+1] } file, err := os.Open(filename) if err != nil { log.Println("Can't read", filename) return err } decoder := json.NewDecoder(file) if err := decoder.Decode(configuration); err != nil { log.Printf("Error in %s\n", filename) log.Println(err) return err } v := reflect.ValueOf(configuration).Elem() f := v.FieldByName("Includes") // make sure that this field is defined, and can be changed. if !f.IsValid() { // configuration doesn't have an Includes slice return nil } if f.Kind() != reflect.Slice { return nil } myincludes, ok := f.Interface().([]string) if !ok { // configuration doesn't have an Includes slice return nil } // clear out the slice f.Set(reflect.MakeSlice(f.Type(), 0, f.Cap())) for _, include := range myincludes { var incfilename string if slashindex != -1 { incfilename = prefix + include } else { incfilename = include } if err := ReadConfig(incfilename, configuration); err != nil { return err } } return nil } view raw readconfig.go hosted with ❤ by GitHub What this function does is take the name of a JSON file as well as a struct to read it into. In the struct, it looks for a string slice called Includes. That slice contains the names of other JSON files to read in. It does this recursively. I’ll illustrate with an example. We have a web server that needs to talk to the user database. Here is the configuration data needed for the web server. This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters package main type Config struct { Includes []string UserDbConfig userdb.Config Port int } view raw main.go hosted with ❤ by GitHub And here is the user database configuration data. This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters package userdb type Config struct { URL string MaxIdleConnections int MaxOpenConnections int ConnMaxLifetime int Replica bool } view raw userdb.go hosted with ❤ by GitHub Here is a config file for the user database. This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters { "UserDbConfig": { "URL": "user=groupsio password=dev host=127.0.0.1 dbname=userdb", "MaxIdleConnections": 1, "MaxOpenConnections": 10, "ConnMaxLifetime": 0, "Replica": false } } view raw userdb_conf.json hosted with ❤ by GitHub And here is the config file for the webserver. This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters Show hidden characters { "Includes": [ "userdb_conf.json" ], "Port": 3000 } view raw web_conf.json hosted with ❤ by GitHub You see that the webserver config file has an Includes slice that references the userdb config file. Calling ReadConfiguration() will populate the webserver config struct with the userdb config information. -------------------------------------------------------------------------------- Groups.io is the best tool to get a bunch of people organized and sharing knowledge. Start a free trial group today. -------------------------------------------------------------------------------- UPGRADING POSTGRES USING PGLOGICAL By Mark Fletcher December 8, 2022 - 5 minutes read - 903 words On a ferry to Balestrad, Norway I was born the son of a lawless man Always spoke my mind with a gun in my hand Lived nine lives Gunned down ten Gonna ride like the wind Ride Like The Wind, Christopher Cross Recently, I upgraded the Groups.io database from postgres version 9.6 to 14, using pglogical to do so. These are my rough notes, mainly for myself to refer to the next time I need to do an upgrade, but perhaps they will be useful to others as well. Postgres major versions are supported for 5 years. We had been on 9.6 since it was released; support ended a year ago. Upgrading Postgres major versions can be done in place, with a bit of downtime. But I wanted to be able to test the upgrade before switching over, and an in place upgrade would not allow that easily. Enter pglogical, an extension that streams SQL commands between different Postgres instances. That means that you can sync different major versions of Postgres. The plan was to create a new Postgres 14 cluster, and replicate to it from our main cluster using pglogical. We could then test the new cluster over several weeks, by routing read-only production traffic to it, and see how it behaved in production. Once we were convinced that the new cluster was behaving well, we would have a short downtime and switch production writes over to it, and retire the old cluster. In the end, this worked out very well for us. SETTING UP PGLOGICAL Install the pglogical extension on your current database and make sure it’s included in the shared_preload_libraries line of postgresql.conf. Set up a new Postgres 14 instance, also with pglogical installed. Ensure that the 14 instance is listed in the 9.6 database’s pg_hba.conf file with replication privileges. Run the following on the 14 instance for each database, to get the database schema: pg_dump -h <ORIGINAL DB IP> -U <user> --schema-only --exclude-schema=pglogical userdb > userdb.schema.sql Import that schema into the 14 instance: psql -d userdb < userdb.schema.sql On the 9.6 database: create extension pglogical; SELECT pglogical.create_node( node_name := 'userdb_pg96', dsn := 'host=<NEW DB IP> port=5432 dbname=userdb'); SELECT pglogical.replication_set_add_all_tables('default', ARRAY['public']); SELECT pglogical.replication_set_add_all_sequences('default', ARRAY['public']); Then on the 14 database: create extension pglogical; SELECT pglogical.create_node( node_name := 'userdb_pg14', dsn := 'host=<ORIGINAL DB IP> port=5432 dbname=userdb' ); SELECT pglogical.create_subscription( subscription_name := 'userdb_pg96_sub', provider_dsn := 'host=<ORIGINAL DB IP> user=replicator password=root port=5432 dbname=userdb' ); SELECT pglogical.wait_for_subscription_sync_complete('userdb_pg96_sub'); This creates the sync link between databases and starts the sync process, and waits until the initial sync is complete. You can monitor the sync process via the log on the 14 instance. At this point, I replicated the 14 instance to create the new production cluster. I then gradually moved read-only production queries over to it and monitored how it behaved over the course of a couple of weeks. One thing that surprised me is that autovacuum will not be triggered on the 14 cluster and none of the database stats that you can monitor related to dead tuples will be updated. This is ok. Once you break the pglogical sync, autovacuum will proceed as expected. LOGICAL REPLICATION We use logical replication to stream database changes to our elasticsearch cluster. Our setup is detailed here, and it’s worked great over the years. Unfortunately, the logical slots used for this are not replicated using pglogical, so you have to recreate them on the 14 instance. This is not difficult, but there was one thing that tripped me up. I created a small utility to recreate the logical slots on the new instance and used it to recreate the slots on the 14 instance. When connecting to a logical slot, you provide a starting LSN to begin replication. What tripped me up was that I was initially using the LSN from the 9.6 instance, and was seeing no replication at all. But LSNs are different on the new instance. So when initially connecting to the logical slot on the new instance, use 0 for the starting LSN. I wrote an upgrade checklist for things to do to move to the new instance. Once I was confident in the new cluster, I announced a one hour downtime on the site. FINAL PRODUCTION CUTOVER Bring up a test web server that talks to the new cluster, and ensure that it works. Take the site down. Turn off pgbouncer to ensure no one was talking with the 9.6 database. On the 9.6 instance you need to do a final sync of all replicated sequences: SELECT pglogical.synchronize_sequence( seqoid ) FROM pglogical.sequence_state; I then ran a script that dumped a bunch of values from the 9.6 and 14 instances and compared them, as a sanity check. Turn off pglogical replication, on the 14 instance: SELECT pglogical.alter_subscription_disable('userdb_pg96_sub'); SELECT pglogical.drop_subscription('userdb_pg96_sub'); SELECT pglogical.drop_node('userdb_pg14'); DROP extension pglogical; Breaking the sync should cause autovacuum to run immediately; check to make sure it does. Log into the test web server and make some changes, ensure that they work. Turn off the 9.6 cluster. Update the database config files to point to the new 14 cluster. Bring up the site. THE END The downtime lasted about half an hour (it was announced as an hour downtime). I took the rest of the day off to decompress (upgrades are nerve wracking!). -------------------------------------------------------------------------------- Groups.io is the best tool to get a bunch of people organized and sharing knowledge. Start a free trial group today. -------------------------------------------------------------------------------- MORE THE GROUPS.IO ENGINEERING PHILOSOPHY WHY WE'RE NOT LEAVING THE CLOUD RE: INTRODUCTION REACTIVATING THE BLOG All Posts © wingedpig 2023